| name | typescript-mcp |
| description | Build MCP servers with TypeScript on Cloudflare Workers. Covers tools, resources, prompts, tasks, authentication (API keys, OAuth, Zero Trust), and Cloudflare service integrations. Prevents 20 documented errors.
Use when exposing APIs to LLMs or troubleshooting export syntax errors, transport leaks, server instance reuse bugs, CORS misconfigurations, or task validation errors.
|
| user-invocable | true |
| allowed-tools | ["Read","Write","Edit","Bash","Grep","Glob"] |
TypeScript MCP on Cloudflare Workers
Last Updated: 2026-01-21
Versions: @modelcontextprotocol/sdk@1.25.3, hono@4.11.3, zod@4.3.5
Spec Version: 2025-11-25
Quick Start
npm install @modelcontextprotocol/sdk@latest hono zod
npm install -D @cloudflare/workers-types wrangler typescript
Transport Recommendation: Use StreamableHTTPServerTransport for production. SSE transport is deprecated and maintained for backwards compatibility only. Streamable HTTP provides better error recovery, bidirectional communication, and simplified deployment.
Basic MCP Server:
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { Hono } from 'hono';
import { z } from 'zod';
const server = new McpServer({ name: 'my-mcp-server', version: '1.0.0' });
server.registerTool(
'echo',
{
description: 'Echoes back input',
inputSchema: z.object({ text: z.string() })
},
async ({ text }) => ({ content: [{ type: 'text', text }] })
);
const app = new Hono();
app.post('/mcp', async (c) => {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
enableJsonResponse: true
});
transport.onerror = (error) => {
console.error('MCP transport error:', error);
};
c.res.raw.on('close', () => transport.close());
await server.connect(transport);
await transport.handleRequest(c.req.raw, c.res.raw, await c.req.json());
return c.body(null);
});
export default app;
Deploy: wrangler deploy
Authentication
API Key (KV-based):
app.use('/mcp', async (c, next) => {
const apiKey = c.req.header('Authorization')?.replace('Bearer ', '');
const isValid = await c.env.MCP_API_KEYS.get(`key:${apiKey}`);
if (!isValid) return c.json({ error: 'Unauthorized' }, 403);
await next();
});
Cloudflare Zero Trust:
const jwt = c.req.header('Cf-Access-Jwt-Assertion');
const payload = await verifyJWT(jwt, c.env.CF_ACCESS_TEAM_DOMAIN);
Tasks (v1.24.0+)
Tasks enable long-running operations that return a handle for polling results later. Useful for expensive computations, batch processing, or operations that may need input.
Task States: working → input_required → completed / failed / cancelled
Server Capability Declaration:
const server = new McpServer({
name: 'my-server',
version: '1.0.0',
capabilities: {
tasks: {
list: {},
cancel: {},
requests: {
tools: { call: {} }
}
}
}
});
Tool with Task Support:
server.registerTool(
'long-running-analysis',
{
description: 'Analyze large dataset',
inputSchema: z.object({ datasetId: z.string() }),
execution: { taskSupport: 'optional' }
},
async ({ datasetId }, extra) => {
const result = await performAnalysis(datasetId);
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
}
);
Client Task Request:
{
"method": "tools/call",
"params": {
"name": "long-running-analysis",
"arguments": { "datasetId": "abc123" },
"task": { "ttl": 60000 }
}
}
Task Lifecycle:
- Client sends request with
task param → receives taskId
- Client polls via
tasks/get with taskId
- When status is
completed, client calls tasks/result to get output
- Optional: Client can
tasks/cancel to abort
📚 Spec: https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks
Sampling with Tools (v1.24.0+)
Servers can now include tool definitions in sampling requests, enabling server-side agent loops.
Use Case: Server needs to orchestrate multi-step reasoning using LLM + tools without custom frameworks.
const result = await server.requestSampling({
messages: [{ role: 'user', content: 'Analyze this data and fetch more if needed' }],
maxTokens: 4096,
tools: [
{
name: 'fetch_data',
description: 'Fetch additional data from API',
inputSchema: { type: 'object', properties: { query: { type: 'string' } } }
}
]
});
if (result.content[0].type === 'tool_use') {
const toolResult = await executeLocalTool(result.content[0]);
}
Key Points:
- Server-side agentic behavior as first-class MCP feature
- Standard MCP primitives (no custom frameworks)
- Tool definitions follow same schema as
tools/list
📚 Spec: SEP-1577
Cloudflare Service Tools
D1 Database:
server.registerTool('query-db', {
inputSchema: z.object({ query: z.string(), params: z.array(z.union([z.string(), z.number()])).optional() })
}, async ({ query, params }, env) => {
const result = await env.DB.prepare(query).bind(...(params || [])).all();
return { content: [{ type: 'text', text: JSON.stringify(result.results) }] };
});
KV, R2, Vectorize: See references/cloudflare-integration.md
Known Issues Prevention
This skill prevents 20 production issues documented in official MCP SDK and Cloudflare repos:
Issue #1: Export Syntax Issues (CRITICAL)
Error: "Cannot read properties of undefined (reading 'map')"
Source: honojs/hono#3955, honojs/vite-plugins#237
Why It Happens: Incorrect export format with Vite build causes cryptic errors
Prevention:
export default { fetch: app.fetch };
export default app;
Issue #2: Unclosed Transport Connections
Error: Memory leaks, hanging connections
Source: Best practice from SDK maintainers
Why It Happens: Not closing StreamableHTTPServerTransport on request end
Prevention:
app.post('/mcp', async (c) => {
const transport = new StreamableHTTPServerTransport();
c.res.raw.on('close', () => transport.close());
});
Issue #3: Tool Schema Validation Failure
Error: ListTools request handler fails to generate inputSchema
Source: GitHub modelcontextprotocol/typescript-sdk#1028
Why It Happens: Zod schemas not properly converted to JSON Schema
Prevention:
server.registerTool(
'tool-name',
{
inputSchema: z.object({ a: z.number() })
},
handler
);
Issue #4: Tool Arguments Not Passed to Handler
Error: Handler receives undefined arguments
Source: GitHub modelcontextprotocol/typescript-sdk#1026
Why It Happens: Schema type mismatch between registration and invocation
Prevention:
const schema = z.object({ a: z.number(), b: z.number() });
type Input = z.infer<typeof schema>;
server.registerTool(
'add',
{ inputSchema: schema },
async (args: Input) => {
return { content: [{ type: 'text', text: String(args.a + args.b) }] };
}
);
Issue #5: CORS Misconfiguration
Error: Browser clients can't connect to MCP server
Source: Common production issue
Why It Happens: Missing CORS headers for HTTP transport
Prevention:
import { cors } from 'hono/cors';
app.use('/mcp', cors({
origin: ['http://localhost:3000', 'https://your-app.com'],
allowMethods: ['POST', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization']
}));
Issue #6: Missing Rate Limiting
Error: API abuse, DDoS vulnerability
Source: Production security best practice
Why It Happens: No rate limiting on MCP endpoints
Prevention:
app.post('/mcp', async (c) => {
const ip = c.req.header('CF-Connecting-IP');
const rateLimitKey = `ratelimit:${ip}`;
const count = await c.env.CACHE.get(rateLimitKey);
if (count && parseInt(count) > 100) {
return c.json({ error: 'Rate limit exceeded' }, 429);
}
await c.env.CACHE.put(
rateLimitKey,
String((parseInt(count || '0') + 1)),
{ expirationTtl: 60 }
);
});
Issue #7: TypeScript Compilation Memory Issues
Error: Out of memory during tsc build
Source: GitHub modelcontextprotocol/typescript-sdk#985
Why It Happens: Large dependency tree in MCP SDK
Prevention:
"build": "NODE_OPTIONS='--max-old-space-size=4096' tsc && vite build"
Issue #8: UriTemplate ReDoS Vulnerability
Error: Server hangs on malicious URI patterns
Source: GitHub modelcontextprotocol/typescript-sdk#965 (Security)
Why It Happens: Regex denial-of-service in URI template parsing
Prevention: Update to SDK v1.20.2 or later (includes fix)
Issue #9: Authentication Bypass
Error: Unauthenticated access to MCP tools
Source: Production security best practice
Why It Happens: Missing or improperly implemented authentication
Prevention: Always implement authentication for production servers (see Authentication Patterns section)
Issue #10: Environment Variable Leakage
Error: Secrets exposed in error messages or logs
Source: Cloudflare Workers security best practice
Why It Happens: Environment variables logged or returned in responses
Prevention:
console.log('Env:', JSON.stringify(env));
try {
} catch (error) {
console.error('Operation failed:', error.message);
}
Issue #11: Server Instance Reuse Breaks Concurrent HTTP Sessions (CRITICAL)
Error: AbortError: This operation was aborted
Source: GitHub Issue #1405
Why It Happens: Calling Server.connect(transport) silently overwrites the previous transport without warning, breaking all earlier connections
Prevention:
app.post('/mcp', async (c) => {
const server = new McpServer({ name: 'my-server', version: '1.0.0' });
server.registerTool('echo', { inputSchema: z.object({ text: z.string() }) },
async ({ text }) => ({ content: [{ type: 'text', text }] })
);
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
enableJsonResponse: true
});
transport.onerror = (error) => console.error('Transport error:', error);
c.res.raw.on('close', () => transport.close());
await server.connect(transport);
await transport.handleRequest(c.req.raw, c.res.raw, await c..());
c.();
});
sharedServer = ({ : , : });
app.(, (c) => {
sharedServer.(transport);
});
Issue #12: sessionIdGenerator Type Error with TypeScript Strict Mode
Error: Type 'undefined' is not assignable to type '() => string'
Source: GitHub Issue #1397
Why It Happens: SDK 1.25.2 types break projects using exactOptionalPropertyTypes: true in tsconfig.json
Prevention:
const transport = new StreamableHTTPServerTransport({
enableJsonResponse: true
});
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
enableJsonResponse: true
});
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => crypto.randomUUID(),
enableJsonResponse: true
});
Issue #13: Global fetch Pollution from Hono (SDK 1.25.0-1.25.2)
Error: Native Node.js fetch behavior breaks after importing SDK
Source: GitHub Issue #1376
Why It Happens: Hono's server code globally overwrites global.fetch, breaking libraries expecting native behavior
Prevention:
npm install @modelcontextprotocol/sdk@1.25.3
const nativeFetch = global.fetch;
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
global.fetch = nativeFetch;
Issue #14: Task Error Wrapping Masks Validation Errors
Error: Confusing error message hides actual validation failure
Source: GitHub Issue #1385
Why It Happens: When task-augmented tool call fails validation before task creation, SDK wraps error incorrectly
Prevention:
server.experimental.tasks.registerToolTask(
'batch_process',
{
inputSchema: z.object({
itemCount: z.number().min(1).max(10),
processingTimeMs: z.number().min(500).max(5000).optional()
})
},
{
createTask: async (args, extra) => {
}
}
);
Issue #15: Tool Schema with All Optional Fields Causes InvalidParams
Error: "expected": "object", "received": "undefined"
Source: GitHub Issue #400
Why It Happens: Some LLM clients omit arguments field when all schema properties are optional
Prevention:
server.registerTool('fetch-records', {
inputSchema: z.object({
limit: z.number().optional()
})
}, handler);
server.registerTool('fetch-records', {
inputSchema: z.object({
action: z.literal('fetch').default('fetch'),
limit: z.number().optional()
})
}, handler);
server.registerTool('fetch-records', {
inputSchema: z.object({}).passthrough()
}, handler);
Issue #16: Bulk Tool Registration Triggers EventEmitter Memory Leak Warnings
Error: MaxListenersExceededWarning: Possible EventEmitter memory leak detected
Source: GitHub Issue #842
Why It Happens: Registering 80+ tools in a loop overwhelms stdout buffer with rapid sendToolListChanged() notifications
Prevention:
process.stdout.setMaxListeners(100);
const tools = [...];
for (const tool of tools) {
server.registerTool(tool.name, tool.schema, tool.handler);
}
Issue #17: Silent Transport Errors Without onerror Handler
Error: Transport errors vanish without logs or exceptions
Source: GitHub Issue #1395
Why It Happens: SDK silently swallows transport errors if onerror callback is not set
Prevention:
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
enableJsonResponse: true
});
transport.onerror = (error) => {
console.error('Transport error:', error);
};
await server.connect(transport);
Issue #18: DoS via Query String Array Limit Bypass
Error: Memory exhaustion from malicious query parameters
Source: GitHub Issue #1368
Why It Happens: The qs library's arrayLimit can be bypassed using bracket notation like ?foo[99999999]=bar
Prevention:
app.post('/mcp', async (c) => {
const queryParams = c.req.query();
if (Object.keys(queryParams).some(key => /\[\d{6,}\]/.test(key))) {
return c.json({ error: 'Invalid query parameters' }, 400);
}
});
Issue #19: Request Handlers Not Cancelled on Transport Close
Error: Long-running handlers continue executing after client disconnect, wasting resources
Source: GitHub Issue #611
Why It Happens: SDK doesn't automatically cancel request handlers when transport connection closes
Prevention:
server.registerTool(
'long-running-task',
{ inputSchema: z.object({ duration: z.number() }) },
async ({ duration }, extra) => {
const abortController = new AbortController();
const transport = extra.transport;
if (transport) {
const originalOnClose = transport.onclose;
transport.onclose = () => {
abortController.abort();
if (originalOnClose) originalOnClose();
};
}
try {
await longRunningTask(duration, abortController.signal);
return { content: [{ type: 'text', text: 'Done' }] };
} catch (error) {
if (error.name === 'AbortError') {
return { content: [{ type: 'text', text: 'Cancelled' }], isError: true };
}
throw error;
}
}
);
Issue #20: $defs Schema References Failed in SDK 1.22.0-1.22.x
Error: can't resolve reference #/$defs/...
Source: GitHub Issue #1175
Why It Happens: SDK 1.22.0 regression in cacheToolOutputSchemas broke listTools() with complex JSON Schema
Prevention: Update to SDK v1.23.0 or later (fixed). If on 1.22.x, upgrade immediately.
Deployment
wrangler dev
wrangler deploy
Testing: npx @modelcontextprotocol/inspector (connect to http://localhost:8787/mcp)
Templates & References
Templates: basic-mcp-server.ts, tool-server.ts, resource-server.ts, authenticated-server.ts, tasks-server.ts, wrangler.jsonc
References: tool-patterns.md, authentication-guide.md, testing-guide.md, cloudflare-integration.md, common-errors.md
Critical Rules
Always:
- ✅ Create fresh
McpServer instance per HTTP request (never reuse across sessions)
- ✅ Set
transport.onerror handler to catch silent errors
- ✅ Close transport on response end (
c.res.raw.on('close', () => transport.close()))
- ✅ Use direct export (
export default app, NOT { fetch: app.fetch })
- ✅ Implement authentication for production
- ✅ Update to SDK v1.25.3+ for security fixes, Tasks support, and fetch pollution fix
- ✅ Include at least one required field in tool schemas (avoid all-optional)
- ✅ Use
StreamableHTTPServerTransport for production (SSE is deprecated)
Never:
- ❌ Reuse
McpServer instance across concurrent HTTP sessions
- ❌ Export with object wrapper
- ❌ Forget to close StreamableHTTPServerTransport
- ❌ Omit
transport.onerror handler
- ❌ Log environment variables or secrets
- ❌ Use outdated SDK versions (<1.23.0 has schema bugs, <1.25.3 has fetch pollution)