Development
typescript-mcp - Claude MCP Skill
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.
SEO Guide: Enhance your AI agent with the typescript-mcp tool. This Model Context Protocol (MCP) server allows Claude Desktop and other LLMs to build mcp servers with typescript on cloudflare workers. covers tools, resources, prompts, tasks, au... Download and configure this skill to unlock new capabilities for your AI workflow.
Documentation
SKILL.md# 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
```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/sdk@1.25.3
// 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)Signals
Information
- Repository
- jezweb/claude-skills
- Author
- jezweb
- Last Sync
- 2/18/2026
- Repo Updated
- 2/17/2026
- Created
- 1/16/2026
Reviews (0)
No reviews yet. Be the first to review this skill!
Related Skills
upgrade-nodejs
Upgrading Bun's Self-Reported Node.js Version
cursorrules
CrewAI Development Rules
cn-check
Install and run the Continue CLI (`cn`) to execute AI agent checks on local code changes. Use when asked to "run checks", "lint with AI", "review my changes with cn", or set up Continue CI locally.
CLAUDE
CLAUDE.md
Related Guides
Bear Notes Claude Skill: Your AI-Powered Note-Taking Assistant
Learn how to use the bear-notes Claude skill. Complete guide with installation instructions and examples.
Mastering tmux with Claude: A Complete Guide to the tmux Claude Skill
Learn how to use the tmux Claude skill. Complete guide with installation instructions and examples.
OpenAI Whisper API Claude Skill: Complete Guide to AI-Powered Audio Transcription
Learn how to use the openai-whisper-api Claude skill. Complete guide with installation instructions and examples.