TypeScript MCP Server on Cloudflare Workers
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.
When to Use This Skill
Use this skill when:
- Building MCP servers to expose APIs, tools, or data to LLMs
- Deploying serverless MCP endpoints on Cloudflare Workers
- Integrating external APIs as MCP tools (REST, GraphQL, databases)
- Creating stateless MCP servers for edge deployment
- Exposing Cloudflare services (D1, KV, R2, Vectorize) via MCP protocol
- Implementing authenticated MCP servers with API keys, OAuth, or Zero Trust
- Building multi-tool MCP servers with resources and prompts
- Needing production-ready templates that prevent common MCP errors
Do NOT use this skill when:
- Building Python MCP servers (use FastMCP skill instead)
- Needing stateful agents with WebSockets (use Cloudflare Agents SDK)
- Wanting long-running persistent agents with SQLite storage (use Durable Objects)
- Building local CLI tools (use stdio transport, not HTTP)
Core Concepts
MCP Protocol Components
1. Tools - Functions LLMs can invoke
- Input/output schemas defined with Zod
- Async handlers return structured content
- Can call external APIs, databases, or computations
2. Resources - Static or dynamic data exposure
- URI-based addressing (e.g.,
config://app/settings) - Templates support parameters (e.g.,
user://{userId}) - Return text, JSON, or binary data
3. Prompts - Pre-configured prompt templates
- Provide reusable conversation starters
- Can include placeholders and dynamic content
- Help standardize LLM interactions
4. Completions (Optional) - Argument auto-complete
- Suggest valid values for tool arguments
- Improve developer experience
Quick Start
1. Basic MCP Server Template
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
2. Tool-Server Template
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'}`
}]
};
}
);
3. Resource-Server Template
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)
}]
};
}
);
4. Authenticated Server Template
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
});
Authentication Patterns
Pattern 1: API Key (Recommended for Most Cases)
Setup:
- Create KV namespace:
wrangler kv namespace create MCP_API_KEYS - Add to
wrangler.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"
Pattern 2: Cloudflare Zero Trust Access
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
Pattern 3: OAuth 2.0
See references/authentication-guide.md for complete OAuth implementation.
Cloudflare Service Integration
D1 Database Tool Example
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": "..." }
]
}
KV Storage Tool Example
server.registerTool(
'get-cache',
{
description: 'Retrieves cached value by key',
inputSchema: z.object({ key: z.string() })
},
as