1. 环境准备
MCP 官方提供了 TypeScript
SDK(@modelcontextprotocol/sdk),这是实现 MCP Server 的首选方式。SDK 封装了协议细节,让开发者可以专注于业务逻辑。
1.1 项目初始化
mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
npx tsc --init
1.2 基础项目结构
my-mcp-server/
├── src/
│ ├── index.ts # 入口与服务器初始化
│ ├── tools.ts # 工具定义与处理函数
│ ├── resources.ts # 资源定义
│ └── prompts.ts # 提示词模板
├── package.json
└── tsconfig.json
2. 创建基础服务器
2.1 入口文件
// src/index.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
// 定义服务器能力
const server = new Server(
{
name: "weather-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
resources: {},
},
}
);
// 注册工具列表处理器
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "get_weather",
description: "获取指定城市的当前天气",
inputSchema: {
type: "object",
properties: {
city: {
type: "string",
description: "城市名称,如 Beijing",
},
units: {
type: "string",
enum: ["celsius", "fahrenheit"],
description: "温度单位",
},
},
required: ["city"],
},
},
],
};
});
// 注册工具调用处理器
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === "get_weather") {
const schema = z.object({
city: z.string(),
units: z.enum(["celsius", "fahrenheit"]).optional().default("celsius"),
});
const params = schema.parse(args);
const weather = await fetchWeather(params.city, params.units);
return {
content: [
{
type: "text",
text: JSON.stringify(weather, null, 2),
},
],
};
}
throw new Error(`未知工具: ${name}`);
});
// 启动 stdio 传输
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Weather MCP Server running on stdio");
2.2 运行与测试
# 构建并运行
npx tsx src/index.ts
# 在 Claude Desktop 中配置
# ~/Library/Application Support/Claude/claude_desktop_config.json
{
"mcpServers": {
"weather": {
"command": "npx",
"args": ["-y", "tsx", "/path/to/my-mcp-server/src/index.ts"]
}
}
}
3. 工具调用的完整实现
3.1 输入验证
使用 Zod 进行强类型验证,确保参数符合预期:
// src/tools.ts
import { z } from "zod";
export const ToolSchemas = {
get_weather: z.object({
city: z.string().min(1).max(100),
units: z.enum(["celsius", "fahrenheit"]).default("celsius"),
}),
search_location: z.object({
query: z.string().min(1),
limit: z.number().int().min(1).max(20).default(5),
}),
};
export type ToolArgs = {
[K in keyof typeof ToolSchemas]: z.infer<(typeof ToolSchemas)[K]>;
};
3.2 业务逻辑分离
// src/weather.ts
interface WeatherData {
city: string;
temperature: number;
condition: string;
humidity: number;
windSpeed: number;
units: string;
}
export async function fetchWeather(
city: string,
units: string
): Promise<WeatherData> {
// 实际项目中调用天气 API
// 这里使用模拟数据
return {
city,
temperature: units === "celsius" ? 22 : 72,
condition: "晴朗",
humidity: 45,
windSpeed: 10,
units,
};
}
export async function searchLocations(
query: string,
limit: number
): Promise<string[]> {
const mockLocations = [
"Beijing",
"Shanghai",
"Guangzhou",
"Shenzhen",
"Chengdu",
"Hangzhou",
"Wuhan",
"Xi'an",
];
return mockLocations
.filter((loc) => loc.toLowerCase().includes(query.toLowerCase()))
.slice(0, limit);
}
4. 错误处理
MCP 使用 JSON-RPC 的错误机制。服务器应返回标准化的错误响应:
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const { name, arguments: args } = request.params;
if (name === "get_weather") {
const params = ToolSchemas.get_weather.parse(args);
const weather = await fetchWeather(params.city, params.units);
return {
content: [{ type: "text", text: JSON.stringify(weather) }],
};
}
return {
content: [
{
type: "text",
text: `错误: 未知工具 '${name}'`,
},
],
isError: true,
};
} catch (error) {
if (error instanceof z.ZodError) {
return {
content: [
{
type: "text",
text: `参数验证失败: ${error.errors.map((e) => e.message).join(", ")}`,
},
],
isError: true,
};
}
return {
content: [
{
type: "text",
text: `执行错误: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
});
5. 资源暴露
Resources 提供只读数据访问,与 Tools 不同,它们不执行操作而是返回数据:
// 注册资源列表
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: "weather://beijing/current",
name: "北京当前天气",
mimeType: "application/json",
},
{
uri: "weather://shanghai/current",
name: "上海当前天气",
mimeType: "application/json",
},
],
};
});
// 处理资源读取请求
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri;
if (uri === "weather://beijing/current") {
const weather = await fetchWeather("Beijing", "celsius");
return {
contents: [
{
uri,
mimeType: "application/json",
text: JSON.stringify(weather),
},
],
};
}
if (uri === "weather://shanghai/current") {
const weather = await fetchWeather("Shanghai", "celsius");
return {
contents: [
{
uri,
mimeType: "application/json",
text: JSON.stringify(weather),
},
],
};
}
throw new Error(`未知资源: ${uri}`);
});
6. Prompts 提示词模板
Prompts 让服务器可以贡献预定义的对话模板:
import {
ListPromptsRequestSchema,
GetPromptRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
// 列出可用提示词模板
server.setRequestHandler(ListPromptsRequestSchema, async () => {
return {
prompts: [
{
name: "weather_report",
description: "生成指定城市的详细天气报告",
arguments: [
{
name: "city",
description: "目标城市",
required: true,
},
],
},
{
name: "travel_advice",
description: "根据天气提供旅行建议",
arguments: [
{
name: "destination",
description: "目的地",
required: true,
},
{
name: "activity",
description: "活动类型",
required: false,
},
],
},
],
};
});
// 获取完整提示词内容
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === "weather_report") {
const city = args?.city ?? "未知城市";
return {
description: `${city} 天气报告`,
messages: [
{
role: "user",
content: {
type: "text",
text: `请获取 ${city} 的当前天气信息,并提供以下分析:\n` +
`1. 当前温度和体感温度\n` +
`2. 天气状况和建议穿着\n` +
`3. 未来几小时的天气趋势`
,
},
},
],
};
}
throw new Error(`未知提示词: ${name}`);
});
7. 中间件与横切关注点
在实际项目中,MCP 服务器需要处理大量横切关注点:日志记录、指标采集、访问控制、请求限速等。使用中间件模式可以优雅地解决这些问题。
7.1 日志中间件
function loggingMiddleware(handler: RequestHandler): RequestHandler {
return async (request) => {
const start = Date.now();
const method = request.method;
console.log(`[${new Date().toISOString()}] ${method} 请求开始`);
try {
const result = await handler(request);
const duration = Date.now() - start;
console.log(`[${new Date().toISOString()}] ${method} 请求成功 (${duration}ms)`);
return result;
} catch (error) {
const duration = Date.now() - start;
console.error(`[${new Date().toISOString()}] ${method} 请求失败 (${duration}ms):`, error);
throw error;
}
};
}
// 使用中间件包装处理器
server.setRequestHandler(
CallToolRequestSchema,
loggingMiddleware(async (request) => {
// 实际的处理逻辑
return handleToolCall(request);
})
);
7.2 请求限流中间件
class RateLimitMiddleware {
private calls: Map<string, number[]> = new Map();
private maxCalls: number;
private windowMs: number;
constructor(maxCalls: number = 60, windowMs: number = 60000) {
this.maxCalls = maxCalls;
this.windowMs = windowMs;
}
wrap(handler: RequestHandler): RequestHandler {
return async (request) => {
const toolName = (request.params as any).name ?? "unknown";
const now = Date.now();
if (!this.calls.has(toolName)) {
this.calls.set(toolName, []);
}
const timestamps = this.calls.get(toolName)!;
// 清理过期记录
const validTimestamps = timestamps.filter((t) => now - t < this.windowMs);
this.calls.set(toolName, validTimestamps);
if (validTimestamps.length >= this.maxCalls) {
throw new Error(`工具 ${toolName} 调用频率超限,请稍后重试`);
}
validTimestamps.push(now);
return handler(request);
};
}
}
8. HTTP SSE 传输方式
除了 stdio,MCP Server 还可以通过 HTTP + SSE 方式对外提供服务:
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";
const app = express();
const transports: Map<string, SSEServerTransport> = new Map();
app.get("/sse", async (req, res) => {
const transport = new SSEServerTransport("/messages", res);
transports.set(transport.sessionId, transport);
res.on("close", () => {
transports.delete(transport.sessionId);
});
await server.connect(transport);
});
app.post("/messages", async (req, res) => {
const sessionId = req.query.sessionId as string;
const transport = transports.get(sessionId);
if (!transport) {
res.status(400).send("无效的 sessionId");
return;
}
await transport.handlePostMessage(req, res);
});
app.listen(3000, () => {
console.log("MCP Server listening on http://localhost:3000");
});
8. 测试策略
8.1 单元测试
// tests/weather.test.ts
import { describe, it, expect } from "vitest";
import { fetchWeather, searchLocations } from "../src/weather";
describe("Weather API", () => {
it("should fetch weather for Beijing", async () => {
const weather = await fetchWeather("Beijing", "celsius");
expect(weather.city).toBe("Beijing");
expect(weather.units).toBe("celsius");
});
it("should search locations", async () => {
const results = await searchLocations("Shang", 5);
expect(results).toContain("Shanghai");
expect(results.length).toBeLessThanOrEqual(5);
});
});
8.2 集成测试
使用 MCP Client SDK 对服务器进行端到端测试:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
async function createTestClient() {
const client = new Client({ name: "test-client", version: "1.0" });
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([
client.connect(clientTransport),
server.connect(serverTransport),
]);
return client;
}
it("should list tools", async () => {
const client = await createTestClient();
const tools = await client.listTools();
expect(tools.tools.length).toBeGreaterThan(0);
});
9. 打包与发布
9.1 编译为可执行文件
使用 pkg 或 esbuild 将 TypeScript 编译为可直接运行的 Node.js 程序:
# 使用 esbuild 打包
npx esbuild src/index.ts --bundle --platform=node --outfile=dist/server.js
# 或者直接使用 tsx 运行(推荐开发时)
9.2 npm 发布
10. 打包与发布
10.1 编译为独立二进制
使用 pkg 可以将 Node.js 应用打包为无需 Node.js 环境的独立可执行文件,便于分发和部署:
# 安装打包工具
npm install -D pkg
# 打包为各平台可执行文件
npx pkg dist/server.js --targets node18-linux-x64,node18-macos-x64,node18-win-x64 --output bin/mcp-server
10.2 npm 全局安装
将 MCP 服务器发布到 npm,用户可以直接通过 npx 运行:
{
"name": "@my-org/mcp-weather-server",
"version": "1.0.0",
"bin": {
"mcp-weather-server": "./dist/server.js"
},
"scripts": {
"build": "esbuild src/index.ts --bundle --platform=node --outfile=dist/server.js",
"start": "node dist/server.js"
}
}
# 用户安装方式
npm install -g @my-org/mcp-weather-server
mcp-weather-server
# 或直接使用 npx
npx @my-org/mcp-weather-server
10.3 Docker 容器化
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY dist/ ./dist/
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]
docker build -t mcp-weather-server .
docker run -p 3000:3000 mcp-weather-server
11. 总结
实现一个健壮的 MCP Server 需要注意以下要点:
- 输入验证:始终使用 Zod 或其他 schema 库验证工具参数,防止注入攻击
- 错误传播:使用标准的
isError标志而非抛异常,让 AI 助手知道调用失败 - 工具描述:工具描述要足够详细,因为这是 AI 决定何时调用该工具的唯一依据
- 资源粒度:资源应该以合理的粒度组织,太大的资源会超出模型的上下文限制
- 测试覆盖:使用 InMemoryTransport 进行快速集成测试,无需启动真实进程
在掌握基础实现后,接下来的文章将深入讲解 MCP Client 的集成方式、安全实践、高级工具设计模式以及多服务器协同工作。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。