An approach and example for converting custom, local MCP servers to remote, Cloudflare servers
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
- Overview
- Prerequisites
- Step-by-Step Conversion Guide
- Key Transformations Required
- Authentication and Secrets Management
- Testing and Validation
- Production Deployment
- Common Pitfalls and Solutions
- 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
- Cloudflare Account (free tier sufficient)
- Wrangler CLI installed globally:
npm install -g wrangler - Node.js 18+ for local development
- Git for version control
- 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
- Copy your existing MCP tools and business logic to the new project
- Create appropriate directory structure (e.g.,
src/api/,src/tools/,src/types/) - 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
- Never commit secrets to version control
- Use different values for development vs production
- Rotate secrets regularly
- Use least-privilege principles for API access
- 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.varsexcluded 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
- Endpoint accessibility: Test all HTTP endpoints
- Tool functionality: Verify each MCP tool works correctly
- Authentication: Confirm secrets are accessible
- Error handling: Test error scenarios
- 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
.jsextensions 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.jsoncdurable_objects configuration
Debugging Techniques
- Local Development: Use
console.logandwrangler dev - Production Logs: Use
wrangler tailfor real-time logs - MCP Inspector: Test tools in isolation
- Claude Desktop: Test end-to-end integration
Performance Optimization
- Minimize Bundle Size: Import only what you need
- Use Edge Caching: Cache static responses when possible
- Optimize API Calls: Batch requests where feasible
- 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.