Files
gh-jezweb-claude-skills-ski…/references/common-errors.md
2025-11-30 08:25:43 +08:00

11 KiB

Common Errors in TypeScript MCP Servers

This document details 10+ production issues that occur when building MCP servers with TypeScript on Cloudflare Workers, along with their solutions.


Error 1: Export Syntax Issues (CRITICAL)

Error Message:

Cannot read properties of undefined (reading 'map')

Source: honojs/hono#3955, honojs/vite-plugins#237

Why It Happens: Vite + Cloudflare Workers require direct export of the Hono app, not an object wrapper. The object wrapper { fetch: app.fetch } causes cryptic build errors because Vite's module resolution fails to properly handle the wrapped export.

Prevention:

// ❌ WRONG - Causes build errors
export default { fetch: app.fetch };

// ✅ CORRECT - Direct export
export default app;

// Also works for Module Worker format:
export default {
  fetch: app.fetch,
  scheduled: async (event, env, ctx) => { /* cron handler */ }
};
// But ONLY if you need scheduled/queue/DO handlers

How to Fix:

  1. Search your codebase for export default { fetch:
  2. Replace with direct export: export default app;
  3. Rebuild: npm run build

Error 2: Unclosed Transport Connections

Error Symptoms:

  • Memory leaks in production
  • Hanging connections
  • Gradual performance degradation

Source: Best practice from MCP SDK maintainers

Why It Happens: StreamableHTTPServerTransport maintains an open connection. If not explicitly closed when the HTTP response ends, the connection remains open, consuming memory.

Prevention:

app.post('/mcp', async (c) => {
  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: undefined,
    enableJsonResponse: true
  });

  // CRITICAL: Always close on response end
  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);
});

How to Fix:

  1. Add c.res.raw.on('close', () => transport.close()); after creating transport
  2. Redeploy

Error 3: Tool Schema Validation Failure

Error Message:

ListTools request handler fails to generate inputSchema

Source: GitHub modelcontextprotocol/typescript-sdk#1028

Why It Happens: Zod schemas need to be converted to JSON Schema for MCP protocol compliance. The SDK handles this automatically, but errors occur if schemas are malformed or if manual conversion is attempted incorrectly.

Prevention:

// ✅ CORRECT - SDK handles Zod → JSON Schema conversion
server.registerTool(
  'tool-name',
  {
    description: 'Tool description',
    inputSchema: z.object({
      a: z.number().describe('First number'),
      b: z.number().describe('Second number')
    })
  },
  async ({ a, b }) => {
    return { content: [{ type: 'text', text: String(a + b) }] };
  }
);

// ❌ WRONG - Manual conversion not needed
import { zodToJsonSchema } from 'zod-to-json-schema';
server.registerTool('tool-name', {
  inputSchema: zodToJsonSchema(schema)  // Don't do this
}, handler);

How to Fix:

  1. Ensure using SDK v1.20.2+
  2. Pass Zod schema directly to inputSchema
  3. Do NOT manually convert with zodToJsonSchema() unless absolutely necessary

Error 4: Tool Arguments Not Passed to Handler

Error Symptoms:

  • Handler receives undefined for all arguments
  • Tool execution fails with "cannot read property"

Source: GitHub modelcontextprotocol/typescript-sdk#1026

Why It Happens: Type mismatch between schema definition and handler signature. The SDK expects exact type alignment.

Prevention:

// Define schema
const schema = z.object({
  a: z.number(),
  b: z.number()
});

// Infer type from schema
type Input = z.infer<typeof schema>;

// Use typed handler
server.registerTool(
  'add',
  { description: 'Adds numbers', inputSchema: schema },
  async (args: Input) => {  // Type must match schema
    // args.a and args.b are properly typed and passed
    return {
      content: [{ type: 'text', text: String(args.a + args.b) }]
    };
  }
);

How to Fix:

  1. Use z.infer<typeof schema> to get the type
  2. Type the handler parameter explicitly
  3. Ensure parameter names match schema keys exactly

Error 5: CORS Misconfiguration

Error Symptoms:

  • Browser clients can't connect
  • "No 'Access-Control-Allow-Origin' header" error
  • Preflight (OPTIONS) requests fail

Source: Common production issue

Why It Happens: MCP servers accessed from browsers need CORS headers. Cloudflare Workers don't add these by default.

Prevention:

import { cors } from 'hono/cors';

app.use('/mcp', cors({
  origin: [
    'http://localhost:3000',       // Dev
    'https://your-app.com'         // Prod
  ],
  allowMethods: ['POST', 'OPTIONS'],
  allowHeaders: ['Content-Type', 'Authorization'],
  credentials: true  // If using cookies/auth
}));

app.post('/mcp', async (c) => {
  // MCP handler
});

How to Fix:

  1. Install: npm install hono
  2. Add CORS middleware before MCP endpoint
  3. Update origin array with your domains
  4. Redeploy

Error 6: Missing Rate Limiting

Error Symptoms:

  • API abuse
  • DDoS vulnerability
  • Unexpected high costs

Source: Production security best practice

Why It Happens: Public MCP endpoints without rate limiting are vulnerable to abuse.

Prevention:

app.post('/mcp', async (c) => {
  // Rate limit by IP
  const ip = c.req.header('CF-Connecting-IP') || 'unknown';
  const rateLimitKey = `ratelimit:${ip}:${Math.floor(Date.now() / 60000)}`;

  const count = await c.env.CACHE.get(rateLimitKey);
  if (count && parseInt(count) >= 100) {  // 100 req/min
    return c.json({ error: 'Rate limit exceeded' }, 429);
  }

  await c.env.CACHE.put(
    rateLimitKey,
    String((parseInt(count || '0') + 1)),
    { expirationTtl: 60 }
  );

  // Continue with MCP handler
});

How to Fix:

  1. Add KV namespace for rate limiting
  2. Implement IP-based rate limiting
  3. Adjust limits based on your needs
  4. Consider using Cloudflare Rate Limiting (paid feature)

Error 7: TypeScript Compilation Memory Issues

Error Message:

FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory

Source: GitHub modelcontextprotocol/typescript-sdk#985

Why It Happens: The MCP SDK has a large dependency tree. TypeScript compilation can exceed default Node.js memory limits (512MB).

Prevention:

// package.json
{
  "scripts": {
    "build": "NODE_OPTIONS='--max-old-space-size=4096' tsc && vite build",
    "typecheck": "NODE_OPTIONS='--max-old-space-size=4096' tsc --noEmit"
  }
}

How to Fix:

  1. Update build scripts with NODE_OPTIONS
  2. Increase to 4096MB (4GB)
  3. If still failing, try 8192MB
  4. Consider using tsup instead of raw tsc for faster builds

Error 8: UriTemplate ReDoS Vulnerability

Error Symptoms:

  • Server hangs on certain URI patterns
  • CPU maxed out
  • Timeouts

Source: GitHub modelcontextprotocol/typescript-sdk#965 (Security)

Why It Happens: Regex denial-of-service (ReDoS) in URI template parsing. Malicious URIs with nested patterns cause exponential regex evaluation.

Prevention: Update to SDK v1.20.2 or later (includes fix):

npm install @modelcontextprotocol/sdk@latest

How to Fix:

  1. Check current version: npm list @modelcontextprotocol/sdk
  2. If < 1.20.2, update: npm install @modelcontextprotocol/sdk@latest
  3. Rebuild and redeploy

Error 9: Authentication Bypass

Error Symptoms:

  • Unauthorized access to tools
  • API abuse
  • Data leaks

Source: Production security best practice

Why It Happens: MCP servers deployed without authentication are publicly accessible.

Prevention:

// Use API key authentication
app.use('/mcp', async (c, next) => {
  const authHeader = c.req.header('Authorization');
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return c.json({ error: 'Unauthorized' }, 401);
  }

  const apiKey = authHeader.replace('Bearer ', '');
  const isValid = await c.env.MCP_API_KEYS.get(`key:${apiKey}`);

  if (!isValid) {
    return c.json({ error: 'Invalid API key' }, 403);
  }

  await next();
});

How to Fix:

  1. Add authentication middleware (see authenticated-server.ts template)
  2. Create KV namespace for API keys
  3. Generate secure API keys
  4. Distribute keys securely to authorized clients

Error 10: Environment Variable Leakage

Error Symptoms:

  • Secrets exposed in logs
  • API keys visible in error messages
  • Security breach

Source: Cloudflare Workers security best practice

Why It Happens: Logging or returning env objects exposes all secrets.

Prevention:

// ❌ WRONG - Exposes ALL secrets
console.log('Env:', JSON.stringify(env));
return c.json({ env }, 500);

// ✅ CORRECT - Never log env directly
try {
  const apiKey = env.MY_API_KEY;  // Use specific keys only
  // ... use apiKey
} catch (error) {
  console.error('Operation failed:', error.message);  // Safe
  return c.json({ error: 'Internal error' }, 500);
}

How to Fix:

  1. Search codebase for console.log(env or JSON.stringify(env)
  2. Remove all env logging
  3. Use specific environment variables only
  4. Never return env in responses

Additional Common Issues

Issue 11: Missing Zod Descriptions

Why It Matters: LLMs use parameter descriptions to understand tools. Missing descriptions = poor tool usage.

Fix:

// ❌ BAD - No descriptions
z.object({ name: z.string(), age: z.number() })

// ✅ GOOD - Clear descriptions
z.object({
  name: z.string().describe('User full name'),
  age: z.number().describe('User age in years')
})

Issue 12: Large Response Payloads

Problem: Returning huge JSON objects or binary data causes timeouts.

Fix:

// Limit response size
const MAX_RESPONSE_SIZE = 100000;  // 100KB

server.registerTool('fetch-data', { ... }, async ({ url }) => {
  const response = await fetch(url);
  const text = await response.text();

  if (text.length > MAX_RESPONSE_SIZE) {
    return {
      content: [{
        type: 'text',
        text: `Response too large (${text.length} bytes). First 100KB:\n${text.slice(0, MAX_RESPONSE_SIZE)}`
      }]
    };
  }

  return { content: [{ type: 'text', text }] };
});

Issue 13: Not Handling Async Errors

Problem: Unhandled promise rejections crash Workers.

Fix:

server.registerTool('risky-operation', { ... }, async (args) => {
  try {
    const result = await riskyAsyncOperation(args);
    return { content: [{ type: 'text', text: result }] };
  } catch (error) {
    return {
      content: [{
        type: 'text',
        text: `Operation failed: ${(error as Error).message}`
      }],
      isError: true
    };
  }
});

Debugging Checklist

When encountering MCP server issues:

  1. Check SDK version (npm list @modelcontextprotocol/sdk)
  2. Verify export syntax (direct export, not object wrapper)
  3. Confirm transport is closed on response end
  4. Test with MCP Inspector (npx @modelcontextprotocol/inspector)
  5. Check Cloudflare Workers logs (wrangler tail)
  6. Verify authentication middleware (if production)
  7. Test CORS headers (if browser clients)
  8. Review Zod schemas for proper types
  9. Check rate limiting implementation
  10. Ensure no env variables are logged

Last Updated: 2025-10-28 Verified Against: @modelcontextprotocol/sdk@1.20.2