typescript-mcp

安装量: 339
排名: #2740

安装

npx skills add https://github.com/jezweb/claude-skills --skill typescript-mcp

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 });

// 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):

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' } // '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:

{ "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.

// 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:

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:

// ❌ 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:

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:

// ✅ 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:

const schema = z.object({ a: z.number(), b: z.number() }); type Input = z.infer;

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:

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 } );

// 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:

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:

// ❌ 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 Why It Happens: Calling Server.connect(transport) silently overwrites the previous transport without warning, breaking all earlier connections Prevention:

// ✅ 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 Why It Happens: SDK 1.25.2 types break projects using exactOptionalPropertyTypes: true in tsconfig.json Prevention:

// 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 Why It Happens: Hono's server code globally overwrites global.fetch, breaking libraries expecting native behavior Prevention:

// 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 Why It Happens: When task-augmented tool call fails validation before task creation, SDK wraps error incorrectly Prevention:

// 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 Why It Happens: Some LLM clients omit arguments field when all schema properties are optional Prevention:

// ❌ 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 Why It Happens: Registering 80+ tools in a loop overwhelms stdout buffer with rapid sendToolListChanged() notifications Prevention:

// 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 Why It Happens: SDK silently swallows transport errors if onerror callback is not set Prevention:

// ✅ 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 Why It Happens: The qs library's arrayLimit can be bypassed using bracket notation like ?foo[99999999]=bar Prevention:

// 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 Why It Happens: SDK doesn't automatically cancel request handlers when transport connection closes Prevention:

// 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 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

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)

返回排行榜