home / skills / jezweb / claude-skills / typescript-mcp
/skills/typescript-mcp
This skill helps you build and troubleshoot TypeScript MCP servers on Cloudflare Workers, preventing common errors and securing robust API transports.
npx playbooks add skill jezweb/claude-skills --skill typescript-mcpReview the files below or copy the command above to add this skill to your agents.
---
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/[email protected], [email protected], [email protected]
**Spec Version**: 2025-11-25
---
## Quick Start
```bash
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**:
```typescript
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
});
// CRITICAL: Set error handler to catch transport errors
transport.onerror = (error) => {
console.error('MCP transport error:', error);
};
// CRITICAL: Close transport to prevent memory leaks
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; // CRITICAL: Direct export, not { fetch: app.fetch }
```
**Deploy**: `wrangler deploy`
---
## Authentication
**API Key** (KV-based):
```typescript
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**:
```typescript
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**:
```typescript
const server = new McpServer({
name: 'my-server',
version: '1.0.0',
capabilities: {
tasks: {
list: {},
cancel: {},
requests: {
tools: { call: {} }
}
}
}
});
```
**Tool with Task Support**:
```typescript
server.registerTool(
'long-running-analysis',
{
description: 'Analyze large dataset',
inputSchema: z.object({ datasetId: z.string() }),
execution: { taskSupport: 'optional' } // 'forbidden' | 'optional' | 'required'
},
async ({ datasetId }, extra) => {
// If invoked as task, extra.task contains taskId
const result = await performAnalysis(datasetId);
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
}
);
```
**Client Task Request**:
```json
{
"method": "tools/call",
"params": {
"name": "long-running-analysis",
"arguments": { "datasetId": "abc123" },
"task": { "ttl": 60000 }
}
}
```
**Task Lifecycle**:
1. Client sends request with `task` param ā receives `taskId`
2. Client polls via `tasks/get` with `taskId`
3. When status is `completed`, client calls `tasks/result` to get output
4. 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.
```typescript
// Server initiates sampling with tools available
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' } } }
}
]
});
// Handle tool calls in response
if (result.content[0].type === 'tool_use') {
const toolResult = await executeLocalTool(result.content[0]);
// Continue conversation with tool result...
}
```
**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**:
```typescript
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**:
```typescript
// ā WRONG - Causes cryptic build errors
export default { fetch: app.fetch };
// ā
CORRECT - Direct export
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**:
```typescript
app.post('/mcp', async (c) => {
const transport = new StreamableHTTPServerTransport(/*...*/);
// CRITICAL: Always close on response end
c.res.raw.on('close', () => transport.close());
// ... handle request
});
```
### 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**:
```typescript
// ā
CORRECT - SDK handles Zod schema conversion automatically
server.registerTool(
'tool-name',
{
inputSchema: z.object({ a: z.number() })
},
handler
);
// No need for manual zodToJsonSchema() unless custom validation
```
### 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**:
```typescript
const schema = z.object({ a: z.number(), b: z.number() });
type Input = z.infer<typeof schema>;
server.registerTool(
'add',
{ inputSchema: schema },
async (args: Input) => {
// args.a and args.b properly typed and passed
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**:
```typescript
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**:
```typescript
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 }
);
// Continue...
});
```
### 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**:
```bash
# Add to package.json scripts
"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**:
```typescript
// ā WRONG - Exposes secrets
console.log('Env:', JSON.stringify(env));
// ā
CORRECT - Never log env objects
try {
// ... use env.SECRET_KEY
} catch (error) {
// Don't include env in error context
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](https://github.com/modelcontextprotocol/typescript-sdk/issues/1405)
**Why It Happens**: Calling `Server.connect(transport)` silently overwrites the previous transport without warning, breaking all earlier connections
**Prevention**:
```typescript
// ā
CORRECT - Create fresh McpServer per HTTP session
app.post('/mcp', async (c) => {
const server = new McpServer({ name: 'my-server', version: '1.0.0' });
// Register tools per request
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.req.json());
return c.body(null);
});
// ā WRONG - Reusing server instance across sessions
const sharedServer = new McpServer({ name: 'my-server', version: '1.0.0' });
app.post('/mcp', async (c) => {
await sharedServer.connect(transport); // Breaks previous sessions!
});
```
### Issue #12: sessionIdGenerator Type Error with TypeScript Strict Mode
**Error**: `Type 'undefined' is not assignable to type '() => string'`
**Source**: [GitHub Issue #1397](https://github.com/modelcontextprotocol/typescript-sdk/issues/1397)
**Why It Happens**: SDK 1.25.2 types break projects using `exactOptionalPropertyTypes: true` in tsconfig.json
**Prevention**:
```typescript
// With exactOptionalPropertyTypes: true
// ā
CORRECT - Omit the property instead of setting to undefined
const transport = new StreamableHTTPServerTransport({
enableJsonResponse: true
// sessionIdGenerator omitted entirely
});
// ā WRONG - Setting to undefined causes type error in SDK 1.25.2
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // Type error!
enableJsonResponse: true
});
// Alternative: Provide a generator function
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](https://github.com/modelcontextprotocol/typescript-sdk/issues/1376)
**Why It Happens**: Hono's server code globally overwrites `global.fetch`, breaking libraries expecting native behavior
**Prevention**:
```typescript
// FIXED in SDK v1.25.3 - Update to latest version
npm install @modelcontextprotocol/[email protected]
// Workaround for older versions (1.25.0-1.25.2):
const nativeFetch = global.fetch;
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
global.fetch = nativeFetch; // Restore if needed
```
### Issue #14: Task Error Wrapping Masks Validation Errors
**Error**: Confusing error message hides actual validation failure
**Source**: [GitHub Issue #1385](https://github.com/modelcontextprotocol/typescript-sdk/issues/1385)
**Why It Happens**: When task-augmented tool call fails validation before task creation, SDK wraps error incorrectly
**Prevention**:
```typescript
// Expected error for invalid input:
// "Invalid arguments: Too small: expected number to be >=500"
// Actual error (confusing):
// "Invalid task creation result: expected object, received undefined"
// WORKAROUND: Add explicit validation before task logic
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) => {
// SDK should fix this - currently no workaround
// Validation errors are masked by task wrapping
}
}
);
```
### Issue #15: Tool Schema with All Optional Fields Causes InvalidParams
**Error**: `"expected": "object", "received": "undefined"`
**Source**: [GitHub Issue #400](https://github.com/modelcontextprotocol/typescript-sdk/issues/400)
**Why It Happens**: Some LLM clients omit `arguments` field when all schema properties are optional
**Prevention**:
```typescript
// ā WRONG - All optional fields may cause issues
server.registerTool('fetch-records', {
inputSchema: z.object({
limit: z.number().optional()
})
}, handler);
// ā
CORRECT - Always include at least one required field
server.registerTool('fetch-records', {
inputSchema: z.object({
action: z.literal('fetch').default('fetch'), // Required
limit: z.number().optional()
})
}, handler);
// Alternative: Use empty object schema
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](https://github.com/modelcontextprotocol/typescript-sdk/issues/842)
**Why It Happens**: Registering 80+ tools in a loop overwhelms stdout buffer with rapid `sendToolListChanged()` notifications
**Prevention**:
```typescript
// Workaround: Increase maxListeners before bulk registration
process.stdout.setMaxListeners(100);
const tools = [...]; // Array of 80+ tool definitions
for (const tool of tools) {
server.registerTool(tool.name, tool.schema, tool.handler);
}
// Future SDK may provide batch registration API
```
### Issue #17: Silent Transport Errors Without onerror Handler
**Error**: Transport errors vanish without logs or exceptions
**Source**: [GitHub Issue #1395](https://github.com/modelcontextprotocol/typescript-sdk/issues/1395)
**Why It Happens**: SDK silently swallows transport errors if `onerror` callback is not set
**Prevention**:
```typescript
// ā
CORRECT - Always set onerror handler
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
enableJsonResponse: true
});
transport.onerror = (error) => {
console.error('Transport error:', error);
// Handle error appropriately
};
await server.connect(transport);
```
### Issue #18: DoS via Query String Array Limit Bypass
**Error**: Memory exhaustion from malicious query parameters
**Source**: [GitHub Issue #1368](https://github.com/modelcontextprotocol/typescript-sdk/issues/1368)
**Why It Happens**: The `qs` library's `arrayLimit` can be bypassed using bracket notation like `?foo[99999999]=bar`
**Prevention**:
```typescript
// Validate query parameters to prevent DoS
app.post('/mcp', async (c) => {
const queryParams = c.req.query();
// Reject malicious patterns
if (Object.keys(queryParams).some(key => /\[\d{6,}\]/.test(key))) {
return c.json({ error: 'Invalid query parameters' }, 400);
}
// ... handle request
});
```
### Issue #19: Request Handlers Not Cancelled on Transport Close
**Error**: Long-running handlers continue executing after client disconnect, wasting resources
**Source**: [GitHub Issue #611](https://github.com/modelcontextprotocol/typescript-sdk/issues/611)
**Why It Happens**: SDK doesn't automatically cancel request handlers when transport connection closes
**Prevention**:
```typescript
// Workaround: Use AbortController pattern manually
server.registerTool(
'long-running-task',
{ inputSchema: z.object({ duration: z.number() }) },
async ({ duration }, extra) => {
const abortController = new AbortController();
// Listen for transport close
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](https://github.com/modelcontextprotocol/typescript-sdk/issues/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
```bash
# Local
wrangler dev # http://localhost:8787/mcp
# Production
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)
This skill helps you build and deploy MCP (Model Context Protocol) servers using TypeScript on Cloudflare Workers. It bundles recommended transports, authentication patterns, Cloudflare integrations, task support, and fixes for 20 documented production issues. The goal is reliable, secure MCP endpoints that avoid common runtime and build pitfalls.
The skill provides concrete patterns and code snippets for creating per-request McpServer instances, using StreamableHTTPServerTransport, and wiring Hono routes for Workers. It documents authentication (API keys, Cloudflare Zero Trust), task lifecycle handling, sampling with server-side tools, and integrations with D1, KV, R2 and vector stores. It also lists specific preventions for build, runtime, security, and memory problems discovered in the MCP TypeScript ecosystem.
Do I need a persistent McpServer instance?
No. Create a new McpServer per HTTP request/session. Reusing one instance breaks concurrent sessions and causes AbortError issues.
Which transport should I use in production?
Use StreamableHTTPServerTransport. It offers better error recovery and bidirectional communication; SSE is deprecated and not recommended for new deployments.
How do I prevent task validation errors being masked?
Perform explicit validation before task creation or use the experimental task registration helpers to ensure validation errors surface with clear messages.