Loading...
Loading...
Compare original and translation side by side
@modelcontextprotocol/sdk@modelcontextprotocol/sdkconfig://app/settingsuser://{userId}config://app/settingsuser://{userId}basic-mcp-server.ts// 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;npm install @modelcontextprotocol/sdk hono zod
npm install -D @cloudflare/workers-types wrangler typescriptwrangler deploybasic-mcp-server.ts// 参见 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'
});
// 注册一个简单工具
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端点配置
const app = new Hono();
app.post('/mcp', async (c) => {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
enableJsonResponse: true
});
// 关键:在响应结束时关闭传输连接以防止内存泄漏
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;npm install @modelcontextprotocol/sdk hono zod
npm install -D @cloudflare/workers-types wrangler typescriptwrangler deploytool-server.ts// 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'}`
}]
};
}
);tool-server.ts// 示例:天气API工具
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'}`
}]
};
}
);resource-server.tsimport { 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)
}]
};
}
);resource-server.tsimport { ResourceTemplate } from '@modelcontextprotocol/sdk/types.js';
// 静态资源
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'] })
}]
})
);
// 带参数的动态资源
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)
}]
};
}
);authenticated-server.tsimport { 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
});authenticated-server.tsimport { Hono } from 'hono';
const app = new Hono();
// API密钥认证中间件
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服务器逻辑(用户已通过认证)
// ... 传输配置与处理逻辑
});wrangler kv namespace create MCP_API_KEYSwrangler.jsonc{
"kv_namespaces": [
{ "binding": "MCP_API_KEYS", "id": "YOUR_NAMESPACE_ID" }
]
}async function verifyApiKey(key: string, env: Env): Promise<boolean> {
const storedKey = await env.MCP_API_KEYS.get(`key:${key}`);
return storedKey !== null;
}undefinedwrangler kv namespace create MCP_API_KEYSwrangler.jsonc{
"kv_namespaces": [
{ "binding": "MCP_API_KEYS", "id": "YOUR_NAMESPACE_ID" }
]
}async function verifyApiKey(key: string, env: Env): Promise<boolean> {
const storedKey = await env.MCP_API_KEYS.get(`key:${key}`);
return storedKey !== null;
}undefinedundefinedundefinedimport { 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 Accessimport { 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);
// 用户已通过Cloudflare Access认证references/authentication-guide.mdreferences/authentication-guide.mdserver.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)
}]
};
}
);{
"d1_databases": [
{ "binding": "DB", "database_name": "my-db", "database_id": "..." }
]
}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)
}]
};
}
);{
"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(
'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(
'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)
}]
};
}
);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');
});
});npm install -D vitest @cloudflare/vitest-pool-workersnpx vitestimport { 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) }]
})
);
// 测试工具执行
const result = await server.callTool('add', { a: 5, b: 3 });
expect(result.content[0].text).toBe('8');
});
});npm install -D vitest @cloudflare/vitest-pool-workersnpx vitestundefinedundefinedundefinedundefinedreferences/testing-guide.mdreferences/testing-guide.md"Cannot read properties of undefined (reading 'map')"// ❌ WRONG - Causes cryptic build errors
export default { fetch: app.fetch };
// ✅ CORRECT - Direct export
export default app;"Cannot read properties of undefined (reading 'map')"// ❌ 错误写法 - 会导致构建错误
export default { fetch: app.fetch };
// ✅ 正确写法 - 直接导出
export default app;app.post('/mcp', async (c) => {
const transport = new StreamableHTTPServerTransport(/*...*/);
// CRITICAL: Always close on response end
c.res.raw.on('close', () => transport.close());
// ... handle request
});app.post('/mcp', async (c) => {
const transport = new StreamableHTTPServerTransport(/*...*/);
// 关键:务必在响应结束时关闭连接
c.res.raw.on('close', () => transport.close());
// ... 请求处理逻辑
});ListTools request handler fails to generate inputSchema// ✅ 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 validationListTools request handler fails to generate inputSchema// ✅ 正确写法 - SDK会自动处理Zod Schema转换
server.registerTool(
'tool-name',
{
inputSchema: z.object({ a: z.number() })
},
handler
);
// 除非需要自定义验证,否则无需手动调用zodToJsonSchema()undefinedconst 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) }] };
}
);undefinedconst 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与args.b会被正确类型化并传递
return { content: [{ type: 'text', text: String(args.a + args.b) }] };
}
);import { cors } from 'hono/cors';
app.use('/mcp', cors({
origin: ['http://localhost:3000', 'https://your-app.com'],
allowMethods: ['POST', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization']
}));import { cors } from 'hono/cors';
app.use('/mcp', cors({
origin: ['http://localhost:3000', 'https://your-app.com'],
allowMethods: ['POST', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization']
}));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...
});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 }
);
// 继续处理请求...
});Out of memorytscundefinedOut of memorytscundefinedundefinedundefined// ❌ 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);
}// ❌ 错误写法 - 暴露敏感信息
console.log('Env:', JSON.stringify(env));
// ✅ 正确写法 - 永远不要记录env对象
try {
// ... 使用env.SECRET_KEY
} catch (error) {
// 错误上下文不要包含env
console.error('Operation failed:', error.message);
}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedname: 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 }}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"
}
}{
"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"
}
}references/cloudflare-agents-vs-standalone.mdreferences/cloudflare-agents-vs-standalone.mdtemplates/basic-mcp-server.tstemplates/tool-server.tstemplates/resource-server.tstemplates/full-server.tstemplates/authenticated-server.tstemplates/wrangler.jsonctemplates/basic-mcp-server.tstemplates/tool-server.tstemplates/resource-server.tstemplates/full-server.tstemplates/authenticated-server.tstemplates/wrangler.jsoncreferences/tool-patterns.mdreferences/authentication-guide.mdreferences/testing-guide.mdreferences/deployment-guide.mdreferences/cloudflare-integration.mdreferences/common-errors.mdreferences/cloudflare-agents-vs-standalone.mdreferences/tool-patterns.mdreferences/authentication-guide.mdreferences/testing-guide.mdreferences/deployment-guide.mdreferences/cloudflare-integration.mdreferences/common-errors.mdreferences/cloudflare-agents-vs-standalone.mdscripts/init-mcp-server.shscripts/test-mcp-connection.shscripts/init-mcp-server.shscripts/test-mcp-connection.sh/websites/modelcontextprotocol/websites/modelcontextprotocolexport default appexport default appexport default { fetch: app.fetch }export default { fetch: app.fetch }wrangler devwrangler devreferences/common-errors.mdnpx @modelcontextprotocol/inspectorreferences/common-errors.mdnpx @modelcontextprotocol/inspector