本教程面向有一定Claude Code使用经验、想要深入理解MCP(Model Context Protocol)协议原理并使用TypeScript SDK构建生产级MCP Server的开发者。你将学到MCP协议的架构设计、JSON-RPC消息流、stdio与Streamable HTTP双传输模式、Tools/Resources/Prompts三大原语的完整实现,以及OAuth 2.1认证集成。教程最终交付一个完整的「代码质量分析」MCP Server实战项目。
适用人群:有TypeScript/Node.js基础,已熟悉Claude Code基本操作,想要从协议层面理解MCP并构建可发布的自定义工具的开发者。
| 依赖 | 最低版本 | 验证命令 |
|---|---|---|
| Node.js | 20.x+ | node -v |
| npm/pnpm | 10.x+ / 9.x+ | npm -v |
| TypeScript | 5.x+ | npx tsc -v |
| Claude Code CLI | 最新版 | claude -v |
| Git | 2.x+ | git -v |
确保你已拥有Anthropic API Key或Claude Code订阅。本教程所有代码在macOS和Linux上测试通过,Windows用户建议使用WSL2。
Model Context Protocol是Anthropic于2024年11月发布的开放标准协议,旨在解决LLM应用与外部工具/数据源之间的标准化连接问题。MCP的核心理念可以类比USB-C接口——一个通用协议连接所有AI工具。
MCP协议定义了三个核心角色:
graph TB
subgraph Host["Host(宿主应用)"]
Client["MCP Client<br/>协议客户端"]
App["Claude Code / Claude Desktop<br/>IDE / 自定义应用"]
end
subgraph Server1["MCP Server A"]
T1["Tools<br/>可调用函数"]
R1["Resources<br/>数据资源"]
P1["Prompts<br/>提示模板"]
end
subgraph Server2["MCP Server B"]
T2["Tools"]
R2["Resources"]
P2["Prompts"]
end
subgraph External["外部世界"]
API["REST APIs"]
DB["数据库"]
FS["文件系统"]
Git["Git仓库"]
end
App --> Client
Client -->|"JSON-RPC 2.0<br/>双向通信"| Server1
Client -->|"JSON-RPC 2.0<br/>双向通信"| Server2
Server1 --> API
Server1 --> DB
Server2 --> FS
Server2 --> Git
关键设计原则:一个Host可以运行多个Client实例,每个Client连接一个Server。这种架构保证了安全隔离和模块化。
MCP基于JSON-RPC 2.0协议,所有消息分为三类:
请求(Request):
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "analyze_code",
"arguments": {
"file_path": "/src/index.ts",
"metrics": ["complexity", "maintainability"]
}
}
}
响应(Response):
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "text",
"text": "代码复杂度分析报告:..."
}
]
}
}
通知(Notification):无ID,不期望响应
{
"jsonrpc": "2.0",
"method": "notifications/progress",
"params": {
"progressToken": "abc123",
"progress": 50,
"total": 100
}
}
sequenceDiagram
participant Client as MCP Client
participant Server as MCP Server
Note over Client,Server: 第1步:初始化握手
Client->>Server: initialize {capabilities, clientInfo}
Server-->>Client: {capabilities, serverInfo}
Client->>Server: notifications/initialized
Note over Client,Server: 第2步:能力发现
Client->>Server: tools/list
Server-->>Client: [tool1, tool2, ...]
Client->>Server: resources/list
Server-->>Client: [resource1, ...]
Note over Client,Server: 第3步:正常操作
Client->>Server: tools/call {name, arguments}
Server-->>Server: 执行工具逻辑
Server-->>Client: {content: [...]}
Note over Client,Server: 第4步:关闭
Client->>Server: 关闭连接/通知
初始化握手是MCP协议最关键的环节。Client在initialize请求中声明自己的能力(如支持sampling),Server在响应中声明自己提供的能力(tools/resources/prompts)。握手成功后进入正常操作阶段。
| 特性 | stdio | Streamable HTTP |
|---|---|---|
| 连接方式 | 标准输入/输出管道 | HTTP POST + SSE |
| 部署场景 | 本地CLI工具 | 远程服务、云端 |
| 认证 | 无需(本地进程) | OAuth 2.1 / API Key |
| 多客户端 | 不支持 | 支持 |
| 延迟 | 极低(进程间通信) | 网络延迟 |
| 配置复杂度 | 低 | 中高 |
| Claude Code支持 | ✅ 原生 | ✅ 需URL配置 |
# 创建项目目录
mkdir code-analyzer-mcp && cd code-analyzer-mcp
# 初始化npm项目
npm init -y
# 安装TypeScript SDK和开发依赖
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
# 初始化TypeScript配置
npx tsc --init
编辑tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
code-analyzer-mcp/
├── src/
│ ├── index.ts # 入口:stdio传输模式
│ ├── server-http.ts # 入口:Streamable HTTP模式
│ ├── server.ts # Server核心逻辑(共享)
│ ├── tools/
│ │ ├── complexity.ts # 复杂度分析工具
│ │ ├── dependency.ts # 依赖分析工具
│ │ └── security.ts # 安全检查工具
│ ├── resources/
│ │ └── config.ts # 配置资源
│ ├── prompts/
│ │ └── review.ts # 代码审查提示模板
│ └── utils/
│ ├── parser.ts # 代码解析工具
│ └── formatter.ts # 报告格式化
├── package.json
├── tsconfig.json
└── README.md
{
"name": "code-analyzer-mcp",
"version": "1.0.0",
"description": "代码质量分析 MCP Server - TypeScript实现",
"type": "module",
"bin": {
"code-analyzer-mcp": "./dist/index.js"
},
"scripts": {
"build": "tsc",
"dev": "tsx src/index.ts",
"dev:http": "tsx src/server-http.ts",
"start": "node dist/index.js",
"start:http": "node dist/server-http.js",
"inspect": "npx @modelcontextprotocol/inspector tsx src/index.ts"
},
"files": ["dist"]
}
常见问题:
"type": "module"是必需的,MCP TypeScript SDK使用ESM模块。如果忘记设置,会遇到require() of ES Module错误。
Tool是MCP最核心的原语——它让LLM能够调用你定义的函数。每个Tool由三部分组成:
// src/server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
export function createServer(): McpServer {
const server = new McpServer({
name: "code-analyzer",
version: "1.0.0",
});
// 注册所有工具
registerComplexityTool(server);
registerDependencyTool(server);
registerSecurityTool(server);
// 注册资源
registerConfigResource(server);
// 注册提示模板
registerReviewPrompt(server);
return server;
}
// src/tools/complexity.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { analyzeComplexity } from "../utils/parser.js";
export function registerComplexityTool(server: McpServer): void {
server.tool(
// 工具名称
"analyze_complexity",
// 工具描述 - 这段文字直接影响LLM是否选择调用
"分析指定TypeScript/JavaScript文件的代码复杂度指标。返回圈复杂度、认知复杂度、" +
"函数行数统计、嵌套深度等指标。支持单个文件和目录批量分析。",
// 参数Schema - 使用Zod定义,自动转换为JSON Schema
{
path: z.string().describe("要分析的文件或目录路径(绝对路径)"),
metrics: z
.array(z.enum([
"cyclomatic", // 圈复杂度
"cognitive", // 认知复杂度
"lines", // 行数统计
"nesting", // 嵌套深度
"all" // 全部指标
]))
.default(["all"])
.describe("要计算的复杂度指标列表"),
threshold: z
.number()
.default(10)
.describe("复杂度告警阈值(超过此值标记为高风险)"),
include_pattern: z
.string()
.default("**/*.{ts,tsx,js,jsx}")
.describe("文件匹配模式(glob格式)"),
},
// 工具实现函数
async ({ path, metrics, threshold, include_pattern }) => {
try {
const results = await analyzeComplexity({
path,
metrics: metrics.includes("all")
? ["cyclomatic", "cognitive", "lines", "nesting"]
: metrics,
threshold,
includePattern: include_pattern,
});
// 高风险函数统计
const highRisk = results.filter(r => r.cyclomatic > threshold);
const report = formatReport(results, highRisk, threshold);
return {
content: [
{
type: "text",
text: report,
},
// 如果有高风险函数,同时返回结构化数据
...(highRisk.length > 0 ? [{
type: "text",
text: `\n⚠️ 发现 ${highRisk.length} 个高风险函数(复杂度 > ${threshold})`,
}] : []),
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `❌ 分析失败:${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
}
);
}
关键要点:
server.tool()的第二个参数(description)必须写清楚工具的能力和适用场景,因为LLM完全依赖这段描述来决定是否调用{ content: [...] }格式,支持多个content块isError: true标记,让Client区分正常输出和错误信息// src/tools/dependency.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
interface DependencyNode {
name: string;
version: string;
dependencies: string[];
dev: boolean;
}
export function registerDependencyTool(server: McpServer): void {
server.tool(
"analyze_dependencies",
"分析项目的npm依赖关系。检测过时版本、安全漏洞、未使用的依赖、以及依赖树深度。支持package.json和lockfile解析。",
{
project_path: z.string().describe("项目根目录路径"),
check_updates: z.boolean().default(false)
.describe("是否检查可更新的版本"),
check_vulnerabilities: z.boolean().default(true)
.describe("是否检查已知安全漏洞"),
depth: z.number().default(3)
.describe("依赖树分析深度"),
},
async ({ project_path, check_updates, check_vulnerabilities, depth }) => {
// 读取package.json
const pkgJson = await readPackageJson(project_path);
// 构建依赖树
const depTree = await buildDependencyTree(project_path, depth);
// 可选:检查安全漏洞
const vulnerabilities = check_vulnerabilities
? await checkSecurity(project_path)
: [];
// 可选:检查版本更新
const updates = check_updates
? await checkForUpdates(pkgJson)
: [];
// 检测未使用依赖
const unused = await findUnusedDependencies(project_path, pkgJson);
const report = [
"## 依赖分析报告\n",
`📦 直接依赖: ${Object.keys(pkgJson.dependencies || {}).length} 个`,
`🔧 开发依赖: ${Object.keys(pkgJson.devDependencies || {}).length} 个`,
`🌳 最大依赖深度: ${depTree.maxDepth} 层`,
`🌳 总依赖数量: ${depTree.totalCount} 个\n`,
];
if (vulnerabilities.length > 0) {
report.push(`### ⚠️ 安全漏洞 (${vulnerabilities.length})`);
vulnerabilities.forEach(v => {
report.push(`- **${v.package}** ${v.severity}: ${v.title}`);
report.push(` 修复版本: ${v.fixVersion}`);
});
report.push("");
}
if (unused.length > 0) {
report.push(`### 🗑️ 未使用的依赖 (${unused.length})`);
unused.forEach(d => report.push(`- ${d}`));
report.push("");
}
if (updates.length > 0) {
report.push(`### 📈 可更新的依赖 (${updates.length})`);
updates.forEach(u => {
report.push(`- **${u.name}**: ${u.current} → ${u.latest}`);
});
}
return {
content: [{ type: "text", text: report.join("\n") }],
};
}
);
}
// src/tools/security.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
export function registerSecurityTool(server: McpServer): void {
server.tool(
"security_scan",
"扫描项目代码中的潜在安全问题。检测硬编码密钥、SQL注入风险、XSS漏洞模式、" +
"不安全的依赖版本、以及敏感文件泄露风险。基于静态分析规则引擎。",
{
target_path: z.string().describe("要扫描的项目路径"),
rules: z.array(z.enum([
"hardcoded-secrets", // 硬编码密钥
"sql-injection", // SQL注入
"xss", // XSS漏洞
"unsafe-deserialize", // 不安全反序列化
"sensitive-files", // 敏感文件检测
"all"
])).default(["all"]).describe("启用的检查规则"),
severity: z.enum(["low", "medium", "high", "critical"])
.default("medium")
.describe("最低报告严重级别"),
},
async ({ target_path, rules, severity }) => {
const activeRules = rules.includes("all")
? ["hardcoded-secrets", "sql-injection", "xss",
"unsafe-deserialize", "sensitive-files"]
: rules;
const findings = await runSecurityScan(target_path, activeRules, severity);
const grouped = {
critical: findings.filter(f => f.severity === "critical"),
high: findings.filter(f => f.severity === "high"),
medium: findings.filter(f => f.severity === "medium"),
low: findings.filter(f => f.severity === "low"),
};
const lines = [
"## 🔒 安全扫描报告\n",
`扫描目标: ${target_path}`,
`启用规则: ${activeRules.join(", ")}`,
`发现总数: ${findings.length}\n`,
];
for (const [level, items] of Object.entries(grouped)) {
if (items.length === 0) continue;
const icon = level === "critical" ? "🔴" :
level === "high" ? "🟠" :
level === "medium" ? "🟡" : "🟢";
lines.push(`### ${icon} ${level.toUpperCase()} (${items.length})`);
items.forEach(f => {
lines.push(`- **${f.rule}**: ${f.message}`);
lines.push(` 📄 ${f.file}:${f.line}`);
if (f.suggestion) lines.push(` 💡 建议: ${f.suggestion}`);
});
lines.push("");
}
return {
content: [{ type: "text", text: lines.join("\n") }],
};
}
);
}
许多开发者会混淆Resources和Tools。核心区别:
| 维度 | Tools | Resources |
|---|---|---|
| 语义 | 动作/操作(动词) | 数据/信息(名词) |
| 调用方式 | LLM主动调用 | LLM读取嵌入上下文 |
| 副作用 | 可能有(写数据库等) | 只读 |
| 缓存 | 不缓存 | 可缓存(ETag机制) |
| 典型场景 | 执行分析、调用API | 提供配置、文档、数据 |
// src/resources/config.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
export function registerConfigResource(server: McpServer): void {
// 静态资源:固定的配置信息
server.resource(
"analysis-config",
"config://analysis-rules",
async (uri) => ({
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify({
version: "1.0.0",
rules: {
cyclomatic_threshold: 10,
max_function_lines: 50,
max_nesting_depth: 4,
excluded_patterns: [
"node_modules/**",
"dist/**",
"*.min.js",
"*.d.ts"
],
severity_mapping: {
hardcoded_secrets: "critical",
sql_injection: "high",
xss: "high",
unsafe_deserialize: "medium",
}
}
}, null, 2),
},
],
})
);
// 动态资源:从文件系统读取的项目元信息
server.resource(
"project-info",
"project://info",
{ mimeType: "application/json" },
async (uri) => {
const projectMeta = await gatherProjectMeta();
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(projectMeta, null, 2),
},
],
};
}
);
// 模板资源:支持参数化的资源URI
server.resource(
"file-content",
new ResourceTemplate("file://{+path}", { list: undefined }),
async (uri, { path }) => {
const content = await readFileContent(path);
return {
contents: [
{
uri: uri.href,
mimeType: guessMimeType(path),
text: content,
},
],
};
}
);
}
注意:Resources在Claude Code中的可见性有限——目前Claude Code主要使用Tools交互,Resources更多用于Claude Desktop和自定义客户端。但定义Resources可以让你的Server在更多场景下复用。
Prompts原语让你定义可复用的提示模板,用户可以通过Claude Desktop的Prompt选择器快速使用。每个Prompt可以接受参数,返回结构化的消息序列。
// src/prompts/review.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
export function registerReviewPrompt(server: McpServer): void {
server.prompt(
"code-review",
"生成结构化代码审查清单。基于传入的文件路径和关注点," +
"生成一份全面的代码审查指南,引导LLM进行深度审查。",
{
file_path: z.string().describe("待审查的文件路径"),
focus_areas: z.string()
.default("correctness,performance,security")
.describe("审查关注领域(逗号分隔)"),
language: z.string().default("chinese")
.describe("审查报告语言"),
},
async ({ file_path, focus_areas, language }) => {
const areas = focus_areas.split(",").map(s => s.trim());
const lang = language === "chinese" ? "中文" : "English";
return {
messages: [
{
role: "user",
content: {
type: "text",
text: `请对以下文件进行全面的代码审查:${file_path}\n\n` +
`请使用${lang}输出审查报告,重点关注以下领域:\n` +
areas.map((a, i) => `${i + 1}. **${a}**`).join("\n") +
`\n\n审查报告请包含:\n` +
`- 总体评分(1-10)\n` +
`- 每个关注领域的具体发现\n` +
`- 严重程度分级(critical/high/medium/low)\n` +
`- 具体修复建议和代码示例\n` +
`- 总结和优先修复建议`,
},
},
],
};
}
);
}
stdio是最简单的传输方式——Claude Code通过子进程启动你的Server,通过stdin/stdout通信:
// src/index.ts
#!/usr/bin/env node
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { createServer } from "./server.js";
async function main() {
const server = createServer();
const transport = new StdioServerTransport();
await server.connect(transport);
// stdio模式下,日志不能写到stdout(会干扰协议通信)
// 使用stderr或文件日志
console.error("✅ Code Analyzer MCP Server 已启动 (stdio模式)");
}
main().catch((error) => {
console.error("❌ 启动失败:", error);
process.exit(1);
});
调试要点:stdio模式下,console.log()会写入stdout,与MCP消息流冲突导致协议错误。所有日志必须使用console.error()或写入文件。
Streamable HTTP是MCP 2025-03-26规范引入的新传输方式,替代了旧的SSE传输。它支持:
// src/server-http.ts
import express from "express";
import { StreamableHTTPServerTransport } from
"@modelcontextprotocol/sdk/server/streamableHttp.js";
import { createServer } from "./server.js";
const app = express();
app.use(express.json());
// Streamable HTTP端点
app.post("/mcp", async (req, res) => {
const server = createServer();
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // 无状态模式
});
try {
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
} catch (error) {
console.error("MCP请求处理失败:", error);
res.status(500).json({
jsonrpc: "2.0",
error: { code: -32603, message: "Internal error" },
id: null,
});
}
});
// SSE端点(用于服务器推送)
app.get("/mcp", async (req, res) => {
const server = createServer();
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
await server.connect(transport);
await transport.handleRequest(req, res);
});
// DELETE端点(关闭会话)
app.delete("/mcp", async (req, res) => {
const server = createServer();
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
await server.connect(transport);
await transport.handleRequest(req, res);
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.error(`✅ Code Analyzer MCP Server 已启动 (HTTP模式)`);
console.error(` 端点: http://localhost:${PORT}/mcp`);
});
graph LR
subgraph stdio模式
CC1["Claude Code CLI"] -->|"启动子进程"| Proc["Node.js 进程<br/>(Server)"]
Proc -->|"stdin/stdout<br/>JSON-RPC"| CC1
end
subgraph Streamable_HTTP模式["Streamable HTTP模式"]
CC2["Claude Code CLI"] -->|"HTTP POST /mcp<br/>JSON-RPC in body"| GW["Express Server<br/>:3000"]
GW -->|"JSON-RPC response<br/>或 SSE stream"| CC2
end
style stdio模式 fill:#e8f5e9
style Streamable_HTTP模式 fill:#e3f2fd
在项目根目录创建.claude/settings.json:
{
"mcpServers": {
"code-analyzer": {
"command": "npx",
"args": ["-y", "tsx", "/absolute/path/to/code-analyzer-mcp/src/index.ts"],
"env": {
"NODE_ENV": "development"
}
}
}
}
编辑~/.claude/settings.json,让所有项目都能使用:
{
"mcpServers": {
"code-analyzer": {
"command": "node",
"args": ["/usr/local/lib/code-analyzer-mcp/dist/index.js"],
"env": {}
}
}
}
对于Streamable HTTP模式的Server:
{
"mcpServers": {
"code-analyzer-remote": {
"type": "http",
"url": "https://your-domain.com/mcp",
"headers": {
"Authorization": "Bearer your-api-key"
}
}
}
}
启动Claude Code后,使用/mcp命令查看已连接的MCP服务器状态:
claude
> /mcp
预期输出:
MCP Servers:
✅ code-analyzer (connected)
Tools: analyze_complexity, analyze_dependencies, security_scan
Resources: config://analysis-rules, project://info
Prompts: code-review
常见问题排查: - 如果Server未显示,检查JSON配置语法是否正确 - 如果显示
❌ connection failed,手动运行npx tsx src/index.ts看是否有错误输出 - 如果Server启动了但Tool不可见,检查server.tool()的description是否为空
// src/utils/parser.ts
import { readFile, stat, readdir } from "fs/promises";
import { join, extname } from "path";
interface ComplexityResult {
file: string;
function_name: string;
start_line: number;
end_line: number;
cyclomatic: number;
cognitive: number;
lines: number;
nesting_depth: number;
}
export async function analyzeComplexity(options: {
path: string;
metrics: string[];
threshold: number;
includePattern: string;
}): Promise<ComplexityResult[]> {
const { path, threshold } = options;
const isDir = (await stat(path)).isDirectory();
const files = isDir ? await collectFiles(path) : [path];
const results: ComplexityResult[] = [];
for (const file of files) {
if (!isSourceFile(file)) continue;
const content = await readFile(file, "utf-8");
const functions = extractFunctions(content, file);
for (const fn of functions) {
const cyclomatic = calculateCyclomaticComplexity(fn.body);
const cognitive = calculateCognitiveComplexity(fn.body);
if (cyclomatic > threshold || cognitive > threshold) {
results.push({
file,
function_name: fn.name,
start_line: fn.startLine,
end_line: fn.endLine,
cyclomatic,
cognitive,
lines: fn.endLine - fn.startLine + 1,
nesting_depth: calculateMaxNesting(fn.body),
});
}
}
}
return results.sort((a, b) => b.cyclomatic - a.cyclomatic);
}
// 圈复杂度:基于决策点计数
function calculateCyclomaticComplexity(code: string): number {
const patterns = [
/\bif\b/g,
/\belse\s+if\b/g,
/\bfor\b/g,
/\bwhile\b/g,
/\bcase\b/g,
/\bcatch\b/g,
/\?\s*[^:]+:/g, // 三元运算符
/&&/g, // 逻辑与
/\|\|/g, // 逻辑或
/\?\?/g, // 空值合并
];
let complexity = 1; // 基础复杂度
for (const pattern of patterns) {
const matches = code.match(pattern);
if (matches) complexity += matches.length;
}
return complexity;
}
// 认知复杂度:考虑嵌套加成
function calculateCognitiveComplexity(code: string): number {
let complexity = 0;
let nestingLevel = 0;
const lines = code.split("\n");
for (const line of lines) {
const trimmed = line.trim();
const indent = line.length - trimmed.length;
// 进入嵌套
if (/[\{]$/.test(trimmed) || /\b(if|for|while|switch|try)\b/.test(trimmed)) {
complexity += 1 + nestingLevel;
if (/[\{]$/.test(trimmed)) nestingLevel++;
}
// 退出嵌套
if (/^\}/.test(trimmed)) {
nestingLevel = Math.max(0, nestingLevel - 1);
}
}
return complexity;
}
async function collectFiles(dir: string): Promise<string[]> {
const entries = await readdir(dir, { withFileTypes: true });
const files: string[] = [];
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory() && !entry.name.startsWith(".")
&& entry.name !== "node_modules" && entry.name !== "dist") {
files.push(...await collectFiles(fullPath));
} else if (entry.isFile() && isSourceFile(entry.name)) {
files.push(fullPath);
}
}
return files;
}
function isSourceFile(name: string): boolean {
return [".ts", ".tsx", ".js", ".jsx"].includes(extname(name));
}
// src/utils/formatter.ts
import { ComplexityResult } from "./parser.js";
export function formatReport(
results: ComplexityResult[],
highRisk: ComplexityResult[],
threshold: number
): string {
const lines: string[] = [
"## 📊 代码复杂度分析报告\n",
`分析文件数: ${new Set(results.map(r => r.file)).size}`,
`分析函数数: ${results.length}`,
`告警阈值: ${threshold}`,
`高风险函数: ${highRisk.length}\n`,
];
// 按文件分组
const grouped = groupByFile(results);
for (const [file, fns] of Object.entries(grouped)) {
lines.push(`### 📄 ${file}`);
lines.push("| 函数 | 圈复杂度 | 认知复杂度 | 行数 | 嵌套深度 | 风险 |");
lines.push("|------|---------|-----------|------|---------|------|");
for (const fn of fns) {
const risk = fn.cyclomatic > threshold ? "🔴 高" :
fn.cyclomatic > threshold * 0.7 ? "🟡 中" : "🟢 低";
lines.push(
`| \`${fn.function_name}\` (${fn.start_line}-${fn.end_line}) ` +
`| ${fn.cyclomatic} | ${fn.cognitive} | ${fn.lines} | ` +
`${fn.nesting_depth} | ${risk} |`
);
}
lines.push("");
}
if (highRisk.length > 0) {
lines.push("### 🚨 优先重构建议\n");
for (const fn of highRisk.slice(0, 5)) {
lines.push(
`1. **${fn.function_name}** (${fn.file}:${fn.start_line})` +
` - 圈复杂度 ${fn.cyclomatic},建议拆分为多个小函数`
);
}
}
return lines.join("\n");
}
function groupByFile(results: ComplexityResult[]): Record<string, ComplexityResult[]> {
return results.reduce((acc, r) => {
(acc[r.file] ||= []).push(r);
return acc;
}, {} as Record<string, ComplexityResult[]>);
}
# 编译TypeScript
npm run build
# 使用MCP Inspector测试(官方调试工具)
npm run inspect
MCP Inspector是一个浏览器端的交互式调试界面,可以: - 查看Server暴露的所有Tools/Resources/Prompts - 手动调用Tool并查看响应 - 检查JSON-RPC消息流 - 验证输入参数的JSON Schema
启动后在浏览器打开http://localhost:5173,你将看到:
Tools:
├── analyze_complexity
│ Parameters: path, metrics, threshold, include_pattern
├── analyze_dependencies
│ Parameters: project_path, check_updates, check_vulnerabilities, depth
└── security_scan
Parameters: target_path, rules, severity
Resources:
├── config://analysis-rules
└── project://info
Prompts:
└── code-review
Parameters: file_path, focus_areas, language
当你的MCP Server通过Streamable HTTP对外提供服务时,必须实现认证以防止未授权访问。MCP规范要求使用OAuth 2.1 + Dynamic Client Registration(DCR)。
sequenceDiagram
participant Client as MCP Client<br/>(Claude Code)
participant Server as MCP Server<br/>(你的服务)
participant Auth as Authorization Server<br/>(如Auth0/Clerk)
Client->>Server: POST /mcp (无token)
Server-->>Client: 401 + WWW-Authenticate header
Client->>Auth: GET /authorize (PKCE flow)
Auth-->>Client: 登录页面
Client->>Auth: 用户授权
Auth-->>Client: Authorization Code
Client->>Auth: POST /token (code + PKCE verifier)
Auth-->>Client: Access Token
Client->>Server: POST /mcp (Bearer token)
Server->>Auth: 验证token (introspect/JWKS)
Auth-->>Server: Token有效
Server-->>Client: 正常MCP响应
// src/middleware/auth.ts
import { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";
const JWKS_URI = process.env.JWKS_URI || "";
const AUDIENCE = process.env.AUTH_AUDIENCE || "code-analyzer-mcp";
const ISSUER = process.env.AUTH_ISSUER || "";
// 简化版JWKS验证中间件
export async function requireAuth(
req: Request,
res: Response,
next: NextFunction
): Promise<void> {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith("Bearer ")) {
res.setHeader("WWW-Authenticate",
`Bearer realm="MCP", resource_metadata="${ISSUER}/.well-known/oauth-authorization-server"`);
res.status(401).json({
jsonrpc: "2.0",
error: {
code: -32001,
message: "Authentication required"
},
id: null,
});
return;
}
const token = authHeader.slice(7);
try {
const decoded = jwt.verify(token, await getSigningKey(), {
audience: AUDIENCE,
issuer: ISSUER,
algorithms: ["RS256"],
});
// 将用户信息注入请求上下文
(req as any).mcp_context = { user: decoded };
next();
} catch {
res.status(401).json({
jsonrpc: "2.0",
error: { code: -32001, message: "Invalid or expired token" },
id: null,
});
}
}
async function getSigningKey(): Promise<string> {
// 生产环境中应从JWKS端点获取公钥并缓存
// 这里简化处理
return process.env.JWT_PUBLIC_KEY || "";
}
更新server-http.ts:
import { requireAuth } from "./middleware/auth.js";
// MCP端点添加认证中间件
app.post("/mcp", requireAuth, async (req, res) => {
// ...原有逻辑不变
});
// WELL-KNOWN端点:OAuth服务器发现
app.get("/.well-known/oauth-authorization-server", (req, res) => {
res.json({
issuer: ISSUER,
authorization_endpoint: `${ISSUER}/authorize`,
token_endpoint: `${ISSUER}/oauth/token`,
registration_endpoint: `${ISSUER}/oauth/register`,
scopes_supported: ["mcp:tools", "mcp:resources"],
code_challenge_methods_supported: ["S256"],
});
});
# 确保构建产物是最新的
npm run build
# 登录npm
npm login
# 发布
npm publish --access public
发布后,用户可以这样配置你的Server:
{
"mcpServers": {
"code-analyzer": {
"command": "npx",
"args": ["-y", "code-analyzer-mcp"]
}
}
}
# Dockerfile
FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY dist/ ./dist/
# Streamable HTTP模式
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
CMD ["node", "dist/server-http.js"]
# 构建并运行
docker build -t code-analyzer-mcp .
docker run -p 3000:3000 \
-e AUTH_ISSUER=https://your-auth-server.com \
-e JWT_PUBLIC_KEY="$(cat public.pem)" \
code-analyzer-mcp
flowchart TB
Start([开始开发]) --> Init[初始化项目<br/>npm init + 安装SDK]
Init --> Design[设计工具接口<br/>定义Tool/Resource/Prompt]
Design --> Code[编写工具实现<br/>注册到McpServer]
Code --> Stdio[实现stdio传输<br/>本地开发调试]
Stdio --> Inspect[使用MCP Inspector测试]
Inspect --> Config[配置Claude Code<br/>.claude/settings.json]
Config --> Verify[/mcp命令验证]
Verify --> HTTP{需要远程部署?}
HTTP -->|是| StreamHTTP[实现Streamable HTTP]
StreamHTTP --> OAuth[添加OAuth认证]
OAuth --> Docker[Docker容器化]
Docker --> Publish[发布到npm/镜像仓库]
HTTP -->|否| LocalPub[npm publish本地包]
LocalPub --> Done([完成])
Publish --> Done
style Start fill:#4caf50,color:white
style Done fill:#2196f3,color:white
style HTTP fill:#ff9800,color:white
snake_case,如analyze_complexity而非analyzeComplexity.describe()描述,让LLM理解每个参数的含义.default()值,减少必填参数数量isError: true标记,提供可操作的修复建议notifications/progress)反馈进度.transform()在验证时完成数据预处理| 维度 | 评估 |
|---|---|
| 适合训练营类型 | 全栈开发训练营、AI工具开发训练营、DevOps工程师进阶 |
| 建议教学时长 | 6-8小时(含动手实践),分2个session |
| 学员基础要求 | TypeScript中级、Node.js基础、REST API经验 |
| 难度评级 | ⭐⭐⭐⭐(中高) |
| 实践环节占比 | 70%编码 + 20%调试测试 + 10%部署发布 |
| 推荐分组 | 2-3人一组,每组完成一个不同主题的MCP Server |
[!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 等场景全覆盖。
