typescript-mcp

|

jezweb/claude-skills339 installsMITSynced Aug 26

Works with

Claude CodeCursorCodex CLIGitHub CopilotGemini CLI
---
name: typescript-mcp
description: |
license: MIT
---

# 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)

More General & Other skills

← All General & Other skills

Check your AI visibility

One URL in, a 0–100 score and the exact fixes out.

RUN THE CHECK

Browse all the tools

15 tools across six categories
13 of them never send your data anywhere

Free · No signup · No trial clock

SEE THE DIRECTORY