MCP Servers

A collection of Model Context Protocol servers, templates, tools and more.

An approach and example for converting custom, local MCP servers to remote, Cloudflare servers

Created 8/18/2025
Updated 8 months ago
Repository documentation and setup instructions

Converting Local MCP Servers to Remote Cloudflare Workers Endpoints

This README provides a comprehensive guide for converting any local Model Context Protocol (MCP) server from stdio transport to a fully remote HTTP/SSE endpoint running on Cloudflare Workers.

Table of Contents

  1. Overview
  2. Prerequisites
  3. Step-by-Step Conversion Guide
  4. Key Transformations Required
  5. Authentication and Secrets Management
  6. Testing and Validation
  7. Production Deployment
  8. Common Pitfalls and Solutions
  9. Troubleshooting

Overview

What We're Accomplishing

Converting a local MCP server (that uses stdio transport for Claude Desktop integration) into a remote HTTP endpoint that:

  • Runs on Cloudflare Workers edge infrastructure
  • Uses HTTP/SSE (Server-Sent Events) transport instead of stdio
  • Maintains all original MCP tool functionality
  • Handles authentication securely via Cloudflare secrets
  • Can be accessed by Claude Desktop or any MCP client over HTTP

Why Make This Conversion

  • Scalability: Edge deployment with global distribution
  • Reliability: Cloudflare's infrastructure and uptime
  • Cost: Generous free tier for most use cases
  • Performance: Reduced latency via edge computing
  • Sharing: Enable team access to your MCP tools
  • Stateless: No local dependencies or environment setup required

Prerequisites

Required Accounts and Tools

  1. Cloudflare Account (free tier sufficient)
  2. Wrangler CLI installed globally: npm install -g wrangler
  3. Node.js 18+ for local development
  4. Git for version control
  5. Working local MCP server (your starting point)

Required Knowledge

  • Basic understanding of MCP protocol
  • Familiarity with TypeScript/JavaScript
  • Basic knowledge of HTTP and WebSockets/SSE
  • Understanding of environment variables and secrets

Step-by-Step Conversion Guide

Step 1: Initialize Cloudflare Workers Project

Start with Cloudflare's official MCP template:

# Create new project from template
npm create cloudflare@latest your-mcp-server-name -- --template https://github.com/cloudflare/mcp-server-template

# Navigate to project
cd your-mcp-server-name

# Install dependencies
npm install

# Authenticate with Cloudflare
wrangler auth login

Step 2: Project Structure Setup

Your new project should have this structure:

your-mcp-server-name/
├── src/
│   └── index.ts                 # Main entry point
├── package.json
├── wrangler.jsonc              # Cloudflare configuration
├── tsconfig.json
├── worker-configuration.d.ts   # Type definitions
└── .dev.vars                   # Local environment variables

Step 3: Convert Your Local MCP Server

A. Copy Your Business Logic

  1. Copy your existing MCP tools and business logic to the new project
  2. Create appropriate directory structure (e.g., src/api/, src/tools/, src/types/)
  3. Update import paths to use relative imports ending in .js (Workers requirement)

B. Replace the Template's Main File

Replace the default src/index.ts with your MCP server logic. Here's the basic structure:

import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

// Import your business logic
import { YourAPIClient } from "./api/client.js";
import type { YourConfig } from "./types/config.js";

export class YourMCP extends McpAgent {
  server = new McpServer({
    name: "your-mcp-server",
    version: "1.0.0",
    description: "Your MCP server description"
  });

  private client!: YourAPIClient;
  private workerEnv!: Env;

  constructor(state: DurableObjectState, env: Env) {
    super(state, env);
    this.workerEnv = env;
  }

  async init() {
    // Initialize your client with environment variables
    if (this.workerEnv) {
      const config = this.loadConfig(this.workerEnv);
      this.client = new YourAPIClient(config);
    }

    // Register your tools
    this.server.tool("your_tool_name", {
      param1: z.string().describe("Description"),
      param2: z.number().optional().describe("Optional parameter"),
    }, async ({ param1, param2 }) => {
      try {
        if (!this.client) {
          return {
            content: [{ 
              type: "text", 
              text: "Client not initialized. Check environment variables." 
            }],
            isError: true
          };
        }

        const result = await this.client.yourMethod(param1, param2);
        
        return {
          content: [{
            type: "text",
            text: JSON.stringify(result, null, 2)
          }]
        };
      } catch (error: any) {
        return {
          content: [{
            type: "text",
            text: `Error: ${error.message}`
          }],
          isError: true
        };
      }
    });
  }

  private loadConfig(env: Env): YourConfig {
    const config = {
      apiKey: env.YOUR_API_KEY || '',
      apiSecret: env.YOUR_API_SECRET || '',
      // ... other config values
    };

    // Validate required configuration
    const missing = Object.entries(config)
      .filter(([_, value]) => !value)
      .map(([key, _]) => key);

    if (missing.length > 0) {
      throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
    }

    return config;
  }
}

// Export default handler for Cloudflare Workers
export default {
  fetch(request: Request, env: Env, ctx: ExecutionContext) {
    const url = new URL(request.url);

    if (url.pathname === "/sse" || url.pathname === "/sse/message") {
      return YourMCP.serveSSE("/sse").fetch(request, env, ctx);
    }

    if (url.pathname === "/mcp") {
      return YourMCP.serve("/mcp").fetch(request, env, ctx);
    }

    return new Response("Your MCP Server - Available endpoints: /sse, /mcp", { 
      status: 200,
      headers: { "Content-Type": "text/plain" }
    });
  },
};

Step 4: Configure Wrangler

Update wrangler.jsonc for your specific needs:

{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "your-mcp-server",
  "main": "src/index.ts",
  "compatibility_date": "2025-03-10",
  "compatibility_flags": ["nodejs_compat"],
  "migrations": [
    {
      "new_sqlite_classes": ["YourMCP"],
      "tag": "v1"
    }
  ],
  "durable_objects": {
    "bindings": [
      {
        "class_name": "YourMCP",
        "name": "MCP_OBJECT"
      }
    ]
  },
  "observability": {
    "enabled": true
  }
}

Step 5: Update Type Definitions

Update worker-configuration.d.ts environment interface:

declare namespace Cloudflare {
  interface Env {
    // Your environment variables
    YOUR_API_KEY: string;
    YOUR_API_SECRET: string;
    // ... add all your required env vars
    
    // Durable Object binding
    MCP_OBJECT: DurableObjectNamespace<import("./src/index").YourMCP>;
  }
}
interface Env extends Cloudflare.Env {}

Key Transformations Required

1. Transport Layer: stdio → HTTP/SSE

Before (stdio):

import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';

const server = new Server(/* ... */);
const transport = new StdioServerTransport();
await server.connect(transport);

After (HTTP/SSE):

import { McpAgent } from "agents/mcp";

export class YourMCP extends McpAgent {
  // Agent framework handles HTTP/SSE transport automatically
}

2. Cryptography: Node.js crypto → Web Crypto API

Before (Node.js):

import crypto from 'crypto';

function createSignature(data: string, secret: string): string {
  return crypto
    .createHmac('sha256', secret)
    .update(data)
    .digest('base64');
}

After (Web Crypto):

async function createSignature(data: string, secret: string): Promise<string> {
  const encoder = new TextEncoder();
  const key = await crypto.subtle.importKey(
    'raw',
    encoder.encode(secret),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign']
  );
  
  const signature = await crypto.subtle.sign(
    'HMAC',
    key,
    encoder.encode(data)
  );
  
  return btoa(String.fromCharCode(...new Uint8Array(signature)));
}

3. HTTP Client: axios/node-fetch → fetch

Before (axios):

import axios from 'axios';

const response = await axios.post(url, data, {
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json'
  }
});

After (fetch):

const response = await fetch(url, {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(data)
});

if (!response.ok) {
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}

const result = await response.json();

4. Environment Variables: process.env → env parameter

Before (Node.js):

const apiKey = process.env.API_KEY;
const config = {
  apiKey: apiKey || '',
  // ...
};

After (Workers):

// In your MCP class
private loadConfig(env: Env): YourConfig {
  const config = {
    apiKey: env.API_KEY || '',
    // ...
  };
  return config;
}

Authentication and Secrets Management

Local Development (.dev.vars)

Create .dev.vars file for local development:

# .dev.vars (never commit this file)
YOUR_API_KEY=your_local_api_key
YOUR_API_SECRET=your_local_api_secret
OTHER_CONFIG=your_local_value

Production Secrets (Cloudflare)

Set production secrets using Wrangler:

# Set each secret individually
wrangler secret put YOUR_API_KEY
# Enter your production API key when prompted

wrangler secret put YOUR_API_SECRET
# Enter your production API secret when prompted

# Verify secrets are set
wrangler secret list

Security Best Practices

  1. Never commit secrets to version control
  2. Use different values for development vs production
  3. Rotate secrets regularly
  4. Use least-privilege principles for API access
  5. Monitor secret usage via Cloudflare dashboard

Testing and Validation

Local Testing

# Start local development server
npm run dev
# or
wrangler dev

# Test endpoints
curl http://localhost:8787/sse
curl http://localhost:8787/mcp

Claude Desktop Integration

Add to your Claude Desktop configuration (claude_desktop_config.json):

{
  "mcpServers": {
    "your-server-name": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/inspector"],
      "env": {
        "MCP_SERVER_URL": "http://localhost:8787/sse"
      }
    }
  }
}

Production Testing

# Deploy to production
npm run deploy
# or
wrangler deploy

# Test production endpoints
curl https://your-mcp-server.your-subdomain.workers.dev/sse
curl https://your-mcp-server.your-subdomain.workers.dev/mcp

Update Claude Desktop config for production:

{
  "mcpServers": {
    "your-server-name": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/inspector"],
      "env": {
        "MCP_SERVER_URL": "https://your-mcp-server.your-subdomain.workers.dev/sse"
      }
    }
  }
}

Production Deployment

Pre-Deployment Checklist

  • [ ] All secrets configured in Cloudflare
  • [ ] .dev.vars excluded from deployment (check .gitignore)
  • [ ] Environment-specific configuration validated
  • [ ] Type checking passes (npm run type-check)
  • [ ] Local testing completed successfully
  • [ ] Wrangler configuration reviewed

Deployment Commands

# Type check
npm run type-check

# Deploy to production
wrangler deploy

# Monitor deployment
wrangler tail

# View logs (if issues occur)
wrangler tail --format=pretty

Post-Deployment Validation

  1. Endpoint accessibility: Test all HTTP endpoints
  2. Tool functionality: Verify each MCP tool works correctly
  3. Authentication: Confirm secrets are accessible
  4. Error handling: Test error scenarios
  5. Performance: Check response times and resource usage

Common Pitfalls and Solutions

1. Import Path Issues

Problem: Module not found errors in Workers Solution: Use relative imports ending in .js:

// Wrong
import { Client } from './api/client';

// Correct
import { Client } from './api/client.js';

2. Environment Variable Access

Problem: undefined values for environment variables Solution: Ensure proper constructor pattern:

export class YourMCP extends McpAgent {
  private workerEnv!: Env;

  constructor(state: DurableObjectState, env: Env) {
    super(state, env);
    this.workerEnv = env; // Store env for later use
  }
}

3. Crypto API Differences

Problem: crypto.createHmac is not a function Solution: Use Web Crypto API with async/await pattern (see transformations section)

4. Fetch Response Handling

Problem: Assuming successful responses without checking Solution: Always check response.ok:

const response = await fetch(url);
if (!response.ok) {
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}

5. Durable Object Configuration

Problem: DurableObject binding not found Solution: Ensure Wrangler configuration matches class export:

// wrangler.jsonc
"durable_objects": {
  "bindings": [
    {
      "class_name": "YourMCP",  // Must match exported class name
      "name": "MCP_OBJECT"
    }
  ]
}

6. MCP Inspector vs Claude Desktop

Problem: Confusion between testing tools Solution:

  • MCP Inspector: For development/debugging (shows raw MCP protocol)
  • Claude Desktop: For end-user experience (integrates tools naturally)

Troubleshooting

Common Error Messages and Solutions

"NetSuite client not initialized"

  • Cause: Environment variables not accessible
  • Solution: Check constructor pattern and secret configuration

"Module not found"

  • Cause: Incorrect import paths
  • Solution: Use .js extensions in imports

"Fetch failed"

  • Cause: Network/API issues
  • Solution: Add proper error handling and retry logic

"DurableObject binding not found"

  • Cause: Wrangler configuration mismatch
  • Solution: Verify wrangler.jsonc durable_objects configuration

Debugging Techniques

  1. Local Development: Use console.log and wrangler dev
  2. Production Logs: Use wrangler tail for real-time logs
  3. MCP Inspector: Test tools in isolation
  4. Claude Desktop: Test end-to-end integration

Performance Optimization

  1. Minimize Bundle Size: Import only what you need
  2. Use Edge Caching: Cache static responses when possible
  3. Optimize API Calls: Batch requests where feasible
  4. Monitor Usage: Use Cloudflare analytics

Conclusion

This guide provides a complete framework for converting any local MCP server to a remote Cloudflare Workers endpoint. The key is understanding the fundamental differences between Node.js and Workers runtime environments, properly handling authentication, and maintaining the MCP protocol semantics while changing the transport layer.

For additional support:

Remember that this conversion opens up new possibilities for scaling, sharing, and deploying your MCP tools globally while maintaining the same functionality that users expect from local implementations.

Quick Setup
Installation guide for this server

Install Package (if required)

npx @modelcontextprotocol/server-cloudflare_mcp

Cursor configuration (mcp.json)

{ "mcpServers": { "forayconsulting-cloudflare-mcp": { "command": "npx", "args": [ "forayconsulting-cloudflare-mcp" ] } } }