// |
| name | typescript-mcp |
| description | Build stateless MCP servers with TypeScript on Cloudflare Workers using @modelcontextprotocol/sdk. Provides patterns for tools, resources, prompts, and authentication (API keys, OAuth, Zero Trust). Use when exposing APIs to LLMs, integrating Cloudflare services (D1, KV, R2, Vectorize), or troubleshooting export syntax errors, unclosed transport leaks, or CORS misconfigurations. |
| license | MIT |
| metadata | {"version":"1.0.0","last_updated":"2025-10-28T00:00:00.000Z","sdk_version":"@modelcontextprotocol/sdk@1.20.2","platform":"cloudflare-workers","production_tested":true,"token_efficiency":"70%","errors_prevented":"10+"} |
| allowed-tools | ["Read","Write","Edit","Bash","Grep","Glob"] |
Build production-ready Model Context Protocol (MCP) servers using TypeScript and deploy them on Cloudflare Workers. This skill covers the official @modelcontextprotocol/sdk, HTTP transport setup, authentication patterns, Cloudflare service integrations, and comprehensive error prevention.
Use this skill when:
Do NOT use this skill when:
1. Tools - Functions LLMs can invoke
2. Resources - Static or dynamic data exposure
config://app/settings)user://{userId})3. Prompts - Pre-configured prompt templates
4. Completions (Optional) - Argument auto-complete
Use the basic-mcp-server.ts template for a minimal working server:
// See templates/basic-mcp-server.ts
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'
});
// Register a simple tool
server.registerTool(
'echo',
{
description: 'Echoes back the input text',
inputSchema: z.object({
text: z.string().describe('Text to echo back')
})
},
async ({ text }) => ({
content: [{ type: 'text', text }]
})
);
// HTTP endpoint setup
const app = new Hono();
app.post('/mcp', async (c) => {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
enableJsonResponse: true
});
// CRITICAL: Close transport on response end 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;
Install dependencies:
npm install @modelcontextprotocol/sdk hono zod
npm install -D @cloudflare/workers-types wrangler typescript
Deploy:
wrangler deploy
Use tool-server.ts for exposing multiple tools (API integrations, calculations):
// Example: Weather API tool
server.registerTool(
'get-weather',
{
description: 'Fetches current weather for a city',
inputSchema: z.object({
city: z.string().describe('City name'),
units: z.enum(['metric', 'imperial']).default('metric')
})
},
async ({ city, units }, env) => {
const response = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${city}&units=${units}&appid=${env.WEATHER_API_KEY}`
);
const data = await response.json();
return {
content: [{
type: 'text',
text: `Temperature in ${city}: ${data.main.temp}ยฐ${units === 'metric' ? 'C' : 'F'}`
}]
};
}
);
Use resource-server.ts for exposing data:
import { ResourceTemplate } from '@modelcontextprotocol/sdk/types.js';
// Static resource
server.registerResource(
'config',
new ResourceTemplate('config://app', { list: undefined }),
{ description: 'Application configuration' },
async (uri) => ({
contents: [{
uri: uri.href,
mimeType: 'application/json',
text: JSON.stringify({ version: '1.0.0', features: ['tool1', 'tool2'] })
}]
})
);
// Dynamic resource with parameter
server.registerResource(
'user-profile',
new ResourceTemplate('user://{userId}', { list: undefined }),
{ description: 'User profile data' },
async (uri, { userId }, env) => {
const user = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first();
return {
contents: [{
uri: uri.href,
mimeType: 'application/json',
text: JSON.stringify(user)
}]
};
}
);
Use authenticated-server.ts for production security:
import { Hono } from 'hono';
const app = new Hono();
// API Key authentication middleware
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();
});
app.post('/mcp', async (c) => {
// MCP server logic (user is authenticated)
// ... transport setup and handling
});
Setup:
wrangler kv namespace create MCP_API_KEYSwrangler.jsonc:{
"kv_namespaces": [
{ "binding": "MCP_API_KEYS", "id": "YOUR_NAMESPACE_ID" }
]
}
Implementation:
async function verifyApiKey(key: string, env: Env): Promise<boolean> {
const storedKey = await env.MCP_API_KEYS.get(`key:${key}`);
return storedKey !== null;
}
Manage keys:
# Add key
wrangler kv key put --binding=MCP_API_KEYS "key:abc123" "true"
# Revoke key
wrangler kv key delete --binding=MCP_API_KEYS "key:abc123"
import { verifyJWT } from '@cloudflare/workers-jwt';
const jwt = c.req.header('Cf-Access-Jwt-Assertion');
if (!jwt) {
return c.json({ error: 'Access denied' }, 403);
}
const payload = await verifyJWT(jwt, c.env.CF_ACCESS_TEAM_DOMAIN);
// User authenticated via Cloudflare Access
See references/authentication-guide.md for complete OAuth implementation.
server.registerTool(
'query-database',
{
description: 'Executes SQL query on D1 database',
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, null, 2)
}]
};
}
);
Wrangler config:
{
"d1_databases": [
{ "binding": "DB", "database_name": "my-db", "database_id": "..." }
]
}
server.registerTool(
'get-cache',
{
description: 'Retrieves cached value by key',
inputSchema: z.object({ key: z.string() })
},
async ({ key }, env) => {
const value = await env.CACHE.get(key);
return {
content: [{ type: 'text', text: value || 'Key not found' }]
};
}
);
server.registerTool(
'upload-file',
{
description: 'Uploads file to R2 bucket',
inputSchema: z.object({
key: z.string(),
content: z.string(),
contentType: z.string().optional()
})
},
async ({ key, content, contentType }, env) => {
await env.BUCKET.put(key, content, {
httpMetadata: { contentType: contentType || 'text/plain' }
});
return {
content: [{ type: 'text', text: `File uploaded: ${key}` }]
};
}
);
server.registerTool(
'semantic-search',
{
description: 'Searches vector database',
inputSchema: z.object({
query: z.string(),
topK: z.number().default(5)
})
},
async ({ query, topK }, env) => {
const embedding = await env.AI.run('@cf/baai/bge-base-en-v1.5', {
text: query
});
const results = await env.VECTORIZE.query(embedding.data[0], {
topK,
returnMetadata: true
});
return {
content: [{
type: 'text',
text: JSON.stringify(results.matches, null, 2)
}]
};
}
);
import { describe, it, expect } from 'vitest';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
describe('Calculator Tool', () => {
it('should add two numbers', async () => {
const server = new McpServer({ name: 'test', version: '1.0.0' });
server.registerTool(
'add',
{
description: 'Adds two numbers',
inputSchema: z.object({
a: z.number(),
b: z.number()
})
},
async ({ a, b }) => ({
content: [{ type: 'text', text: String(a + b) }]
})
);
// Test tool execution
const result = await server.callTool('add', { a: 5, b: 3 });
expect(result.content[0].text).toBe('8');
});
});
Install:
npm install -D vitest @cloudflare/vitest-pool-workers
Run:
npx vitest
# Run server locally
npm run dev
# In another terminal
npx @modelcontextprotocol/inspector
# Connect to: http://localhost:8787/mcp
See references/testing-guide.md for comprehensive testing patterns.
This skill prevents 10+ production issues documented in official MCP SDK and Cloudflare repos:
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;
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
});
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
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<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) }] };
}
);
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']
}));
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...
});
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"
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)
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)
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);
}
# Install dependencies
npm install
# Run locally with Wrangler
npm run dev
# or
wrangler dev
# Server available at: http://localhost:8787/mcp
# Build
npm run build
# Deploy to Cloudflare Workers
wrangler deploy
# Deploy to specific environment
wrangler deploy --env production
name: Deploy MCP Server
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm test
- name: Deploy to Cloudflare Workers
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
{
"dependencies": {
"@modelcontextprotocol/sdk": "^1.20.2",
"@cloudflare/workers-types": "^4.20251011.0",
"hono": "^4.10.1",
"zod": "^3.23.8",
"zod-to-json-schema": "^3.24.1"
},
"devDependencies": {
"@cloudflare/vitest-pool-workers": "^0.5.29",
"vitest": "^3.0.0",
"wrangler": "^4.43.0",
"typescript": "^5.7.0"
}
}
Use Cloudflare Agents MCP when you need:
Use this skill (standalone TypeScript MCP) when you need:
See references/cloudflare-agents-vs-standalone.md for detailed comparison.
All templates are production-ready and tested on Cloudflare Workers:
templates/basic-mcp-server.ts - Minimal working server (echo tool example)templates/tool-server.ts - Multiple tools implementation (API integrations, calculations)templates/resource-server.ts - Resource-only server (static and dynamic resources)templates/full-server.ts - Complete server (tools + resources + prompts)templates/authenticated-server.ts - Production server with API key authenticationtemplates/wrangler.jsonc - Cloudflare Workers configuration with all bindingsWhen Claude should use these: When creating a new MCP server, copy the appropriate template based on the use case (tools-only, resources-only, authenticated, or full-featured).
Comprehensive documentation for advanced topics:
references/tool-patterns.md - Common tool implementation patterns (API wrappers, database queries, calculations, file operations)references/authentication-guide.md - All authentication methods detailed (API keys, OAuth 2.0, Zero Trust, JWT)references/testing-guide.md - Unit testing, integration testing with MCP Inspector, E2E testing with Claude Agent SDKreferences/deployment-guide.md - Wrangler workflows, environment management, CI/CD with GitHub Actionsreferences/cloudflare-integration.md - Using D1, KV, R2, Vectorize, Workers AI, Queues, Durable Objectsreferences/common-errors.md - All 10+ errors with detailed solutions, root causes, and prevention strategiesreferences/cloudflare-agents-vs-standalone.md - Decision matrix for choosing between standalone MCP and Cloudflare Agents SDKWhen Claude should load these: When developer needs advanced implementation details, debugging help, or architectural guidance.
Automation scripts for initializing and testing MCP servers:
scripts/init-mcp-server.sh - Initializes new MCP server project with dependencies, wrangler config, and template selectionscripts/test-mcp-connection.sh - Tests MCP server connectivity and validates tool/resource endpointsWhen Claude should use these: When setting up a new project or debugging connectivity issues.
/websites/modelcontextprotocol (if available)Example Servers:
โ
Close transport on response end to prevent memory leaks
โ
Use direct export syntax (export default app) not object wrapper
โ
Implement authentication for production servers
โ
Add rate limiting to prevent API abuse
โ
Use Zod schemas for type-safe tool definitions
โ
Test with MCP Inspector before deploying to production
โ
Update to SDK v1.20.2+ for security fixes
โ
Document all tools with clear descriptions
โ
Handle errors gracefully and return meaningful messages
โ
Use environment variables for secrets (never hardcode)
โ Export with object wrapper (export default { fetch: app.fetch })
โ Forget to close StreamableHTTPServerTransport
โ Deploy without authentication in production
โ Log environment variables or secrets
โ Use CommonJS format (must use ES modules)
โ Skip CORS configuration for browser clients
โ Hardcode API keys or credentials
โ Return raw error objects (may leak sensitive data)
โ Deploy without testing tools/resources locally
โ Use outdated SDK versions with known vulnerabilities
Use this checklist to verify your MCP server setup:
wrangler dev succeedsThis skill is based on patterns from:
Questions? Issues?
references/common-errors.md for troubleshootingnpx @modelcontextprotocol/inspectorLast Updated: 2025-10-28 SDK Version: @modelcontextprotocol/sdk@1.20.2 Maintainer: Claude Skills Repository