本教程从零构建一个完整的 Model Context Protocol (MCP) 服务器,覆盖 TypeScript SDK 的核心原语(Tools / Resources / Prompts)、两种传输层(Stdio + Streamable HTTP)、2025-06-18 规范新特性(Elicitation、结构化输出、Tool Annotations),最终部署到 Cloudflare Workers。适合有 TypeScript 基础、希望构建生产级 MCP 工具链的开发者。
适用人群:有 Node.js / TypeScript 经验,想为 Claude Code 或其他 MCP 客户端构建自定义工具服务器的开发者。
学习目标:
- 理解 MCP 协议架构与三大原语的分工
- 用官方 @modelcontextprotocol/sdk 构建完整 MCP 服务器
- 实现双传输模式:本地 Stdio + 远程 Streamable HTTP
- 掌握 Elicitation、结构化输出、Tool Annotations 等新特性
- 将服务器部署到 Cloudflare Workers 并接入 OAuth 2.1 认证
| 项目 | 要求 |
|---|---|
| Node.js | v20+(建议 v22 LTS) |
| TypeScript | 5.x |
| 包管理器 | npm / pnpm / bun 均可 |
| Claude Code | 最新版(用于测试 MCP 连接) |
| 基础知识 | TypeScript 类型系统、async/await、HTTP 基础 |
Model Context Protocol(MCP)是 Anthropic 主导的开放协议,为 LLM 应用提供标准化的上下文获取方式。它的核心思想很简单:AI 模型需要工具,工具需要标准接口。
┌─────────────┐ MCP Protocol ┌─────────────┐
│ MCP Client │ ◄──────────────────► │ MCP Server │
│ (Claude Code│ JSON-RPC 2.0 │ (你的工具) │
│ VS Code │ │ │
│ 任意客户端) │ │ │
└─────────────┘ └─────────────┘
MCP 把"AI 能做什么"抽象为三大原语:
graph TB
MCP[MCP Server 能力] --> T[Tools 工具]
MCP --> R[Resources 资源]
MCP --> P[Prompts 提示模板]
T --> T1["执行动作:调用API、操作文件<br/>读写数据库、发HTTP请求"]
R --> R1["提供数据:文件内容、数据库记录<br/>API响应、配置信息"]
P --> P1["预设模板:代码审查模板<br/>分析框架、报告格式"]
style T fill:#e74c3c,color:#fff
style R fill:#3498db,color:#fff
style P fill:#2ecc71,color:#fff
很多开发者分不清何时用 Tool、何时用 Resource。判断规则如下:
| 场景 | 用什么 | 原因 |
|---|---|---|
| 调用外部 API 执行操作 | Tool | 有副作用 |
| 查询数据库并返回数据 | Tool 或 Resource | 只读可做 Resource,需参数过滤做 Tool |
| 提供静态配置文件 | Resource | 纯数据、无参数 |
| 返回项目目录结构 | Resource | 静态数据 |
| 生成代码审查报告 | Prompt | 需要结构化模板 |
| 搜索文件并高亮匹配 | Tool | 需要输入参数 |
| 获取当前时间 | Tool | 虽然无副作用,但每次结果不同 |
核心判断:有副作用或需要参数 → Tool;纯数据暴露 → Resource;模板化输出 → Prompt。
MCP 规范经历了三个关键版本:
timeline
title MCP 协议版本演进
2024-11 : 协议首次发布
: JSON-RPC batching
: SSE 传输
2025-03 : 2025-03-26 版本
: Streamable HTTP 传输
: OAuth 2.1 认证框架
2025-06 : 2025-06-18 版本
: Elicitation(人工确认)
: 结构化 Tool 输出
: Tool Annotations
: 移除 JSON-RPC batching
2025-06-18 版本的三大新特性:
mkdir mcp-devtool-server && cd mcp-devtool-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
初始化 TypeScript 配置:
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true
},
"include": ["src/**/*"]
}
创建一个只有 Tool 的最小 MCP 服务器:
// src/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "devtool-server",
version: "1.0.0",
});
// 注册一个 Tool
server.tool(
"get_timestamp",
"获取当前时间戳和格式化的日期时间",
{}, // 无参数
async () => ({
content: [
{
type: "text",
text: JSON.stringify({
unix: Math.floor(Date.now() / 1000),
iso: new Date().toISOString(),
local: new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }),
}, null, 2),
},
],
})
);
// 启动 Stdio 传输
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP DevTool Server running on stdio");
}
main().catch(console.error);
注意:
console.error而不是console.log。Stdio 传输下,stdout 用于 MCP 协议通信,只有 stderr 可以输出调试信息。如果你用console.log,协议数据会被污染。
在 package.json 中添加启动脚本:
{
"type": "module",
"scripts": {
"dev": "tsx src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
}
}
在项目目录创建 .mcp.json:
{
"mcpServers": {
"devtool": {
"command": "npx",
"args": ["tsx", "src/index.ts"],
"type": "stdio"
}
}
}
在项目目录下启动 Claude Code,然后输入:
请调用 get_timestamp 工具获取当前时间
如果一切正常,Claude Code 会调用你的 MCP Tool 并返回格式化的时间数据。
常见问题排查:
| 问题 | 原因 | 解决 |
|---|---|---|
| "Server not responding" | TypeScript 编译错误 | 运行 npx tsx src/index.ts 看报错 |
| Tool 不可见 | .mcp.json 路径不对 |
确保在项目根目录启动 Claude Code |
| 内容解析失败 | 返回格式不对 | content 必须是数组,每项有 type 和 text |
| "Method not found" | SDK 版本太旧 | 更新到 @modelcontextprotocol/sdk@latest |
Tool 是 MCP 中使用最广泛的原语。每个 Tool 由三部分组成:名称、描述、输入 Schema(Zod 定义)。
// src/tools/code-metrics.ts
import { z } from "zod";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
export function registerCodeMetricsTools(server: McpServer) {
// Tool 1:代码复杂度分析
server.tool(
"analyze_complexity",
"分析 TypeScript 代码的圈复杂度(Cyclomatic Complexity)",
{
code: z.string().describe("要分析的 TypeScript 代码"),
threshold: z.number().optional().default(10).describe("复杂度告警阈值"),
},
async ({ code, threshold }) => {
// 简化的圈复杂度计算
const patterns = [
/\bif\b/g, /\belse\b/g, /\bfor\b/g, /\bwhile\b/g,
/\bcase\b/g, /\bcatch\b/g, /\&\&/g, /\|\|/g, /\?\?/g,
];
let complexity = 1; // 基础复杂度
for (const pattern of patterns) {
const matches = code.match(pattern);
if (matches) complexity += matches.length;
}
const lines = code.split("\n").length;
const functions = (code.match(/\bfunction\b|\b=>\b|\w+\s*\(/g) || []).length;
return {
content: [
{
type: "text",
text: JSON.stringify({
complexity,
lines,
functions,
rating: complexity <= (threshold ?? 10) ? "✅ GOOD" : "⚠️ HIGH",
threshold,
suggestion: complexity > (threshold ?? 10)
? `复杂度 ${complexity} 超过阈值 ${threshold},建议拆分函数`
: "代码复杂度在合理范围内",
}, null, 2),
},
],
};
}
);
// Tool 2:JSON 格式化与验证
server.tool(
"format_json",
"格式化和验证 JSON 字符串,支持自定义缩进",
{
input: z.string().describe("要格式化的 JSON 字符串"),
indent: z.number().optional().default(2).describe("缩进空格数"),
sort_keys: z.boolean().optional().default(false).describe("是否排序键名"),
},
async ({ input, indent, sort_keys }) => {
try {
let parsed = JSON.parse(input);
if (sort_keys) {
parsed = sortObjectKeys(parsed);
}
return {
content: [
{
type: "text",
text: JSON.stringify(parsed, null, indent),
},
],
};
} catch (e) {
return {
content: [
{
type: "text",
text: `JSON 解析错误: ${(e as Error).message}`,
},
],
isError: true,
};
}
}
);
}
function sortObjectKeys(obj: unknown): unknown {
if (Array.isArray(obj)) return obj.map(sortObjectKeys);
if (obj && typeof obj === "object") {
return Object.keys(obj as Record<string, unknown>)
.sort()
.reduce((acc: Record<string, unknown>, key) => {
acc[key] = sortObjectKeys((obj as Record<string, unknown>)[key]);
return acc;
}, {});
}
return obj;
}
Resource 是只读的数据源。Claude Code 可以主动发现和读取 Resource,无需你显式调用。
// src/resources/project-context.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import * as fs from "fs";
import * as path from "path";
export function registerProjectResources(server: McpServer) {
// 静态 Resource:项目配置
server.resource(
"project-config",
"config://project",
async (uri) => ({
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify({
name: "mcp-devtool-server",
version: "1.0.0",
description: "Development toolchain MCP server",
supportedTools: ["analyze_complexity", "format_json", "generate_scaffold"],
supportedResources: ["project-config", "changelog"],
}, null, 2),
},
],
})
);
// 动态 Resource 模板:读取项目文件
server.resource(
"file-reader",
new URL("file://projects/{path}"),
async (uri, { path: filePath }) => {
// 安全检查:防止路径遍历
const safePath = filePath.replace(/\.\./g, "").replace(/^\//, "");
const fullPath = path.join(process.cwd(), safePath);
if (!fullPath.startsWith(process.cwd())) {
return {
contents: [{
uri: uri.href,
mimeType: "text/plain",
text: "Error: Path traversal detected",
}],
};
}
try {
const content = fs.readFileSync(fullPath, "utf-8");
const ext = path.extname(fullPath);
const mimeMap: Record<string, string> = {
".ts": "text/typescript",
".js": "text/javascript",
".json": "application/json",
".md": "text/markdown",
".yaml": "text/yaml",
".yml": "text/yaml",
};
return {
contents: [{
uri: uri.href,
mimeType: mimeMap[ext] || "text/plain",
text: content,
}],
};
} catch {
return {
contents: [{
uri: uri.href,
mimeType: "text/plain",
text: `Error: File not found - ${safePath}`,
}],
};
}
}
);
}
Prompt 是预定义的提示模板,可以接收参数并生成结构化的提示词。
// src/prompts/code-review.ts
import { z } from "zod";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
export function registerCodeReviewPrompts(server: McpServer) {
server.prompt(
"review-code",
"生成结构化的代码审查提示",
{
language: z.string().describe("编程语言"),
focus: z.enum(["security", "performance", "readability", "all"]).describe("审查重点"),
code: z.string().describe("待审查的代码"),
},
async ({ language, focus, code }) => {
const focusMap = {
security: "安全漏洞(OWASP Top 10、注入、XSS、认证问题)",
performance: "性能瓶颈(时间复杂度、内存泄漏、不必要的计算)",
readability: "可读性(命名规范、函数长度、注释质量)",
all: "以上所有方面",
};
return {
messages: [
{
role: "user",
content: {
type: "text",
text: `请审查以下 ${language} 代码,重点关注 ${focusMap[focus]}。
## 代码
\`\`\`${language}
${code}
\`\`\`
## 审查要求
1. 列出发现的问题,按严重程度排序(Critical > Warning > Info)
2. 每个问题给出具体的修改建议
3. 总结代码的整体质量评分(1-10)`,
},
},
],
};
}
);
}
// src/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { registerCodeMetricsTools } from "./tools/code-metrics.js";
import { registerProjectResources } from "./resources/project-context.js";
import { registerCodeReviewPrompts } from "./prompts/code-review.js";
const server = new McpServer({
name: "devtool-server",
version: "1.0.0",
});
// 注册所有能力
registerCodeMetricsTools(server);
registerProjectResources(server);
registerCodeReviewPrompts(server);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("DevTool MCP Server v1.0.0 running on stdio");
}
main().catch(console.error);
graph LR
subgraph "MCP Server (devtool-server)"
T[Tools] --> T1[analyze_complexity]
T --> T2[format_json]
T --> T3[generate_scaffold]
R[Resources] --> R1["config://project"]
R --> R2["file://projects/{path}"]
P[Prompts] --> P1[review-code]
end
CC[Claude Code] -->|JSON-RPC| Server
style Server fill:#1a1a2e,color:#fff
style CC fill:#6c5ce7,color:#fff
Tool Annotations 让客户端提前了解工具的特性,决定是否需要用户确认:
// src/tools/annotations.ts
import { z } from "zod";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
export function registerAnnotatedTools(server: McpServer) {
// 只读工具 —— 安全,无需确认
server.tool(
"search_files",
{
pattern: z.string().describe("搜索模式(glob)"),
directory: z.string().optional().default(".").describe("搜索目录"),
},
{
title: "搜索文件",
readOnlyHint: true, // 只读,无副作用
openWorldHint: true, // 访问外部资源
},
async ({ pattern, directory }) => {
// 实际实现用 fast-glob 等
return {
content: [{
type: "text",
text: `Searching for "${pattern}" in ${directory}...`,
}],
};
}
);
// 破坏性工具 —— 需要用户明确确认
server.tool(
"delete_files",
{
pattern: z.string().describe("要删除的文件模式"),
},
{
title: "删除文件",
destructiveHint: true, // 破坏性操作!
readOnlyHint: false,
},
async ({ pattern }) => {
return {
content: [{
type: "text",
text: `Deleted files matching: ${pattern}`,
}],
};
}
);
}
Annotation 字段说明:
| 字段 | 类型 | 含义 |
|---|---|---|
title |
string | 工具的人类可读标题 |
readOnlyHint |
boolean | 是否只读(无副作用) |
destructiveHint |
boolean | 是否有破坏性(删除、覆盖等) |
idempotentHint |
boolean | 多次调用是否等幂 |
openWorldHint |
boolean | 是否访问外部网络/资源 |
2025-06-18 版本允许 Tool 定义输出 Schema,客户端可以验证返回结构:
// src/tools/structured-output.ts
import { z } from "zod";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
// 定义输出 Schema
const SearchResultSchema = z.object({
query: z.string(),
totalMatches: z.number(),
files: z.array(z.object({
path: z.string(),
line: z.number(),
preview: z.string(),
relevance: z.number().min(0).max(1),
})),
executionTimeMs: z.number(),
});
export function registerStructuredTools(server: McpServer) {
server.tool(
"grep_code",
"在代码库中搜索文本模式,返回结构化结果",
{
query: z.string().describe("搜索关键词"),
max_results: z.number().optional().default(20),
},
{
// 输出 Schema —— 客户端可以据此验证
outputSchema: SearchResultSchema,
},
async ({ query, max_results }) => {
const startTime = Date.now();
// 模拟搜索逻辑
const results = [
{ path: "src/index.ts", line: 42, preview: `// found ${query} here`, relevance: 0.95 },
{ path: "src/tools/code-metrics.ts", line: 15, preview: `const ${query} = ...`, relevance: 0.82 },
].slice(0, max_results);
return {
structuredContent: {
query,
totalMatches: results.length,
files: results,
executionTimeMs: Date.now() - startTime,
},
// 仍然可以提供文本回退给不支持结构化输出的客户端
content: [{
type: "text",
text: `Found ${results.length} matches for "${query}"`,
}],
};
}
);
}
Elicitation 是 MCP 2025-06-18 最强大的新特性之一。它允许服务器在执行过程中暂停,向用户确认信息:
// src/tools/elicitation.ts
import { z } from "zod";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
export function registerElicitationTools(server: McpServer) {
server.tool(
"deploy_service",
"部署服务到指定环境(需要用户确认)",
{
service_name: z.string().describe("服务名称"),
environment: z.enum(["staging", "production"]).describe("目标环境"),
},
async ({ service_name, environment }, extra) => {
// 关键操作前,使用 Elicitation 向用户确认
const confirmation = await extra.elicitInput({
message: `即将部署 ${service_name} 到 ${environment} 环境。确认继续?`,
inputSchema: {
type: "object" as const,
properties: {
confirmed: {
type: "boolean" as const,
description: "确认部署",
},
rollout_percentage: {
type: "number" as const,
description: "灰度发布百分比(1-100)",
default: 100,
},
},
required: ["confirmed"],
},
});
if (!confirmation?.confirmed) {
return {
content: [{
type: "text",
text: "部署已取消",
}],
isError: true,
};
}
const rollout = confirmation.rollout_percentage ?? 100;
return {
content: [{
type: "text",
text: `✅ ${service_name} 已部署到 ${environment}(灰度 ${rollout}%)`,
}],
};
}
);
}
sequenceDiagram
participant User as 用户
participant Client as MCP Client<br/>(Claude Code)
participant Server as MCP Server
User->>Client: 调用 deploy_service
Client->>Server: tools/call {service_name, environment}
Server->>Client: elicitation/request {确认部署?}
Client->>User: 显示确认对话框
User->>Client: confirmed=true, rollout=30%
Client->>Server: elicitation/response
Server->>Client: 工具执行结果
Client->>User: 显示部署成功
graph TB
subgraph "本地传输 (Stdio)"
S1[Claude Code] -->|stdin/stdout| S2[MCP Server 进程]
end
subgraph "远程传输 (Streamable HTTP)"
R1[任意 MCP Client] -->|HTTP POST| R2[MCP Server<br/>Cloudflare Workers]
R2 -->|SSE Stream| R1
end
style S2 fill:#00b894,color:#fff
style R2 fill:#0984e3,color:#fff
| 特性 | Stdio | Streamable HTTP |
|---|---|---|
| 传输方式 | 标准输入/输出管道 | HTTP POST + SSE 流 |
| 适用场景 | 本地 CLI 工具 | 远程服务、团队共享 |
| 认证 | 无需 | OAuth 2.1 |
| 部署 | 用户本地安装 | Cloudflare / Vercel / 自托管 |
| 多客户端 | 一对一 | 多对一 |
| 延迟 | 极低(进程内) | 取决于网络 |
// src/transports/http.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamable-http.js";
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
import express from "express";
import { randomUUID } from "crypto";
export async function startHttpTransport(server: McpServer, port: number) {
const app = express();
app.use(express.json());
// 会话管理:每个客户端连接对应一个 transport 实例
const transports: Map<string, StreamableHTTPServerTransport> = new Map();
// MCP 消息端点
app.post("/mcp", async (req, res) => {
const sessionId = req.headers["mcp-session-id"] as string | undefined;
let transport: StreamableHTTPServerTransport;
if (sessionId && transports.has(sessionId)) {
// 已有会话:复用 transport
transport = transports.get(sessionId)!;
} else if (isInitializeRequest(req.body)) {
// 新会话:创建 transport
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: randomUUID,
});
transports.set(transport.sessionId!, transport);
// 清理断开的会话
transport.onclose = () => {
transports.delete(transport.sessionId!);
};
await server.connect(transport);
} else {
res.status(400).json({
jsonrpc: "2.0",
error: { code: -32000, message: "Bad Request: no valid session" },
id: null,
});
return;
}
await transport.handleRequest(req, res, req.body);
});
// SSE 端点(用于服务器推送通知)
app.get("/mcp", async (req, res) => {
const sessionId = req.headers["mcp-session-id"] as string;
const transport = transports.get(sessionId);
if (!transport) {
res.status(400).send("No active session");
return;
}
await transport.handleRequest(req, res);
});
// DELETE 端点(关闭会话)
app.delete("/mcp", async (req, res) => {
const sessionId = req.headers["mcp-session-id"] as string;
const transport = transports.get(sessionId);
if (transport) {
await transport.handleRequest(req, res);
transports.delete(sessionId);
}
});
app.listen(port, () => {
console.error(`Streamable HTTP server listening on port ${port}`);
});
}
// src/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { startHttpTransport } from "./transports/http.js";
import { registerCodeMetricsTools } from "./tools/code-metrics.js";
import { registerAnnotatedTools } from "./tools/annotations.js";
import { registerStructuredTools } from "./tools/structured-output.js";
import { registerElicitationTools } from "./tools/elicitation.js";
import { registerProjectResources } from "./resources/project-context.js";
import { registerCodeReviewPrompts } from "./prompts/code-review.js";
function createServer(): McpServer {
const server = new McpServer({
name: "devtool-server",
version: "1.0.0",
});
registerCodeMetricsTools(server);
registerAnnotatedTools(server);
registerStructuredTools(server);
registerElicitationTools(server);
registerProjectResources(server);
registerCodeReviewPrompts(server);
return server;
}
async function main() {
const mode = process.env.MCP_TRANSPORT || "stdio";
const server = createServer();
if (mode === "stdio") {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("DevTool MCP Server running on stdio");
} else if (mode === "http") {
const port = parseInt(process.env.PORT || "3000", 10);
await startHttpTransport(server, port);
} else {
console.error(`Unknown transport: ${mode}. Use "stdio" or "http"`);
process.exit(1);
}
}
main().catch(console.error);
通过环境变量切换传输模式:
# 本地 Stdio 模式(Claude Code 使用)
MCP_TRANSPORT=stdio npx tsx src/index.ts
# 远程 HTTP 模式(多客户端共享)
MCP_TRANSPORT=http PORT=3000 npx tsx src/index.ts
| 优势 | 说明 |
|---|---|
| 全球边缘 | 300+ 节点,客户端就近访问 |
| 零冷启动 | V8 isolate,毫秒级启动 |
| 免费额度 | 每天 10 万次请求免费 |
| OAuth 集成 | Cloudflare Access 提供内置认证 |
Workers 不支持 Node.js 原生模块,需要调整代码:
// src/workers/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamable-http.js";
import { z } from "zod";
const server = new McpServer({
name: "devtool-server-remote",
version: "1.0.0",
});
// 注册 Tools(不能使用 Node.js API)
server.tool(
"hash_text",
"计算文本的 SHA-256 哈希值",
{ text: z.string().describe("输入文本") },
async ({ text }) => {
const encoder = new TextEncoder();
const data = encoder.encode(text);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, "0")).join("");
return {
content: [{
type: "text",
text: `SHA-256: ${hashHex}`,
}],
};
}
);
server.tool(
"base64_encode",
"Base64 编码/解码",
{
input: z.string().describe("输入字符串"),
operation: z.enum(["encode", "decode"]).describe("操作类型"),
},
async ({ input, operation }) => {
const result = operation === "encode"
? btoa(unescape(encodeURIComponent(input)))
: decodeURIComponent(escape(atob(input)));
return {
content: [{
type: "text",
text: result,
}],
};
}
);
// Workers 入口
export default {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/mcp" && request.method === "POST") {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // 无状态模式
});
await server.connect(transport);
const body = await request.json();
return transport.handleRequest(request, body);
}
// 健康检查
if (url.pathname === "/health") {
return new Response(JSON.stringify({ status: "ok", version: "1.0.0" }), {
headers: { "Content-Type": "application/json" },
});
}
return new Response("Not Found", { status: 404 });
},
};
# wrangler.toml
name = "mcp-devtool-server"
main = "src/workers/index.ts"
compatibility_date = "2025-06-18"
compatibility_flags = ["nodejs_compat"]
[observability]
enabled = true
部署命令:
npx wrangler deploy
在 Claude Code 的全局配置 ~/.claude.json 中添加远程 MCP:
{
"mcpServers": {
"devtool-remote": {
"type": "streamable-http",
"url": "https://mcp-devtool-server.<your-subdomain>.workers.dev/mcp"
}
}
}
或通过命令行:
claude mcp add devtool-remote --transport streamable-http \
--url https://mcp-devtool-server.<your-subdomain>.workers.dev/mcp
远程 MCP 服务器必须考虑认证。2025-06-18 规范引入了基于 OAuth 2.1 的授权框架。
sequenceDiagram
participant Client as MCP Client
participant Server as MCP Server
participant Auth as Authorization Server<br/>(Cloudflare Access)
Client->>Server: POST /mcp (无 token)
Server-->>Client: HTTP 401 + WWW-Authenticate
Client->>Auth: OAuth 2.1 Authorization Request
Auth->>Client: Authorization Code
Client->>Auth: Token Exchange
Auth->>Client: Access Token
Client->>Server: POST /mcp (Bearer Token)
Server->>Server: 验证 Token
Server-->>Client: 200 OK + MCP Response
// src/middleware/auth.ts
import { verifyJwt } from "./jwt-utils.js";
interface AuthConfig {
audience: string;
issuer: string;
jwksUri: string;
}
export function createAuthMiddleware(config: AuthConfig) {
return async (request: Request): Promise<{ authorized: boolean; error?: Response }> => {
const authHeader = request.headers.get("Authorization");
if (!authHeader?.startsWith("Bearer ")) {
return {
authorized: false,
error: new Response("Unauthorized", {
status: 401,
headers: {
"WWW-Authenticate": `Bearer realm="mcp", audience="${config.audience}"`,
},
}),
};
}
const token = authHeader.slice(7);
try {
const payload = await verifyJwt(token, {
audience: config.audience,
issuer: config.issuer,
jwksUri: config.jwksUri,
});
// Token 有效
return { authorized: true };
} catch {
return {
authorized: false,
error: new Response("Invalid token", { status: 401 }),
};
}
};
}
// src/workers/index-with-auth.ts
import { createAuthMiddleware } from "../middleware/auth.js";
const auth = createAuthMiddleware({
audience: "https://mcp-devtool-server.your-domain.workers.dev",
issuer: "https://your-auth-domain.cloudflareaccess.com",
jwksUri: "https://your-auth-domain.cloudflareaccess.com/cdn-cgi/access/certs",
});
export default {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
// MCP 端点需要认证
if (url.pathname === "/mcp") {
const { authorized, error } = await auth(request);
if (!authorized) return error!;
// ... 处理 MCP 请求
}
// 健康检查不需要认证
if (url.pathname === "/health") {
return new Response(JSON.stringify({ status: "ok" }));
}
return new Response("Not Found", { status: 404 });
},
};
在 Claude Code 中配置认证:
{
"mcpServers": {
"devtool-remote-auth": {
"type": "streamable-http",
"url": "https://mcp-devtool-server.<subdomain>.workers.dev/mcp",
"headers": {
"Authorization": "Bearer ${MCP_ACCESS_TOKEN}"
}
}
}
}
MCP 提供了 Inspector 工具,可以在浏览器中可视化调试 MCP 服务器:
npx @modelcontextprotocol/inspector npx tsx src/index.ts
Inspector 会打开一个 Web UI,可以: - 查看服务器声明的 Tools / Resources / Prompts - 直接调用 Tool 并查看返回值 - 测试 Resource 读取 - 查看 JSON-RPC 消息日志
// tests/tools.test.ts
import { describe, it, expect } from "vitest";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { spawn } from "child_process";
describe("DevTool MCP Server", () => {
let client: Client;
beforeAll(async () => {
const transport = new StdioClientTransport({
command: "npx",
args: ["tsx", "src/index.ts"],
});
client = new Client({
name: "test-client",
version: "1.0.0",
});
await client.connect(transport);
});
it("应该列出所有注册的 Tools", async () => {
const tools = await client.listTools();
const toolNames = tools.tools.map(t => t.name);
expect(toolNames).toContain("get_timestamp");
expect(toolNames).toContain("analyze_complexity");
expect(toolNames).toContain("format_json");
});
it("应该正确分析代码复杂度", async () => {
const result = await client.callTool({
name: "analyze_complexity",
arguments: {
code: "if (a) { for (let i = 0; i < 10; i++) { if (b) { console.log(i); } } }",
},
});
const content = result.content as Array<{ type: string; text: string }>;
const parsed = JSON.parse(content[0].text);
expect(parsed.complexity).toBeGreaterThan(1);
});
it("应该格式化 JSON 并排序键", async () => {
const result = await client.callTool({
name: "format_json",
arguments: {
input: '{"z":1,"a":2}',
sort_keys: true,
},
});
const content = result.content as Array<{ type: string; text: string }>;
expect(content[0].text).toBe('{\n "a": 2,\n "z": 1\n}');
});
afterAll(async () => {
await client.close();
});
});
| 技巧 | 方法 |
|---|---|
| 查看协议消息 | Inspector 的 Messages 标签 |
| 调试 Tool 执行 | console.error(JSON.stringify({args, result})) |
| 检查连接状态 | claude mcp list 查看已注册服务器 |
| 日志持久化 | MCP_LOG=1 npx tsx src/index.ts 2>server.log |
| 热重载 | 用 tsx watch 替代 tsx,修改代码自动重启 |
| 连接超时 | 检查 server.connect() 是否在 30 秒内完成 |
mcp-devtool-server/
├── src/
│ ├── index.ts # 主入口(双传输)
│ ├── tools/
│ │ ├── code-metrics.ts # 代码复杂度分析
│ │ ├── annotations.ts # Tool Annotations 示例
│ │ ├── structured-output.ts # 结构化输出示例
│ │ └── elicitation.ts # Elicitation 示例
│ ├── resources/
│ │ └── project-context.ts # 项目上下文资源
│ ├── prompts/
│ │ └── code-review.ts # 代码审查提示模板
│ ├── transports/
│ │ └── http.ts # Streamable HTTP 传输
│ ├── middleware/
│ │ └── auth.ts # OAuth 2.1 认证
│ └── workers/
│ ├── index.ts # Workers 入口
│ └── index-with-auth.ts # 带 Auth 的 Workers 入口
├── tests/
│ └── tools.test.ts # 单元测试
├── .mcp.json # 本地 Claude Code 配置
├── wrangler.toml # Cloudflare Workers 配置
├── tsconfig.json
└── package.json
把前面所有知识串起来,构建一个真实可用的 DevOps 工具链 MCP 服务器,包含以下能力:
graph TB
subgraph "DevOps MCP Server"
direction TB
subgraph "Tools"
T1[analyze_complexity<br/>代码复杂度分析]
T2[format_json<br/>JSON格式化]
T3[hash_text<br/>哈希计算]
T4[deploy_service<br/>服务部署+确认]
T5[grep_code<br/>代码搜索]
end
subgraph "Resources"
R1[config://project<br/>项目配置]
R2[file://projects/path<br/>文件读取]
end
subgraph "Prompts"
P1[review-code<br/>代码审查模板]
end
end
subgraph "部署方式"
D1[Stdio 本地]
D2[HTTP 远程]
D3[Workers Cloudflare]
end
DevOps --> D1
DevOps --> D2
DevOps --> D3
style DevOps fill:#1a1a2e,color:#fff
完整运行命令:
# 克隆并安装
git clone <your-repo> && cd mcp-devtool-server
npm install
# 本地测试
npm run dev
# Inspector 调试
npx @modelcontextprotocol/inspector npm run dev
# 运行测试
npx vitest
# 部署到 Cloudflare
npx wrangler deploy
# Claude Code 接入远程服务器
claude mcp add devtools --transport streamable-http \
--url https://mcp-devtool-server.your-subdomain.workers.dev/mcp
| 决策点 | 推荐 | 替代方案 |
|---|---|---|
| 传输模式 | 本地用 Stdio,远程用 Streamable HTTP | SSE(已废弃) |
| Schema 库 | Zod(官方推荐) | JSON Schema 直接写 |
| 部署平台 | Cloudflare Workers(免费、全球) | Vercel / AWS Lambda |
| 认证方案 | OAuth 2.1(规范要求) | API Key(非标准) |
| 框架选择 | 官方 SDK @modelcontextprotocol/sdk |
FastMCP(第三方封装) |
| 测试工具 | MCP Inspector + Vitest | 手动 curl |
训练营适用性评估:本教程适合"AI 工具链开发训练营"中级模块,预计教学时长 4-5 小时(含动手实验)。建议分两节:第一节(2h)完成本地 Stdio 服务器 + 三大原语;第二节(2-3h)实现 Streamable HTTP + Cloudflare 部署 + OAuth 认证。学员需具备 TypeScript 基础和 HTTP 协议知识。
[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
