MCP(Model Context Protocol)是一种专为大语言模型设计的标准化交互协议,它定义了模型与外部服务之间的通信规范。简单来说,MCP就像是一个翻译官,帮助AI模型理解和使用各种外部API和服务。在实际开发中,MCP服务器充当了中间层,将复杂的API调用封装成模型可以理解的标准化工具接口。
在传统的大模型应用中,开发者通常需要为每个外部服务编写特定的集成代码。这种方式存在几个明显问题:
MCP协议通过标准化解决了这些问题。它定义了统一的工具描述格式、输入输出规范以及错误处理机制。举个例子,无论是调用GitHub API还是Slack API,模型只需要理解一种工具调用方式,MCP服务器会负责将其转换为具体的API请求。
一个典型的MCP服务器包含以下核心组件:
code复制┌───────────────────────────────────────┐
│ MCP Server │
├─────────────┬─────────────┬───────────┤
│ Tool Layer │ Core Layer │ Transport │
└─────────────┴─────────────┴───────────┘
Tool Layer:这是与业务逻辑直接相关的部分,每个工具对应一个外部API功能。例如,GitHub集成可能包含"create_issue"、"search_repo"等工具。工具层负责:
Core Layer:提供共享的基础设施,包括:
Transport Layer:处理与模型的通信协议,支持两种主要方式:
提示:在新项目中,建议优先选择可流式HTTP传输,它比SSE(Server-Sent Events)更灵活且易于扩展。
根据官方推荐和实际项目经验,技术栈选择应考虑以下因素:
| 考量因素 | Python (FastMCP) | TypeScript (MCP SDK) |
|---|---|---|
| 开发效率 | 适合快速原型开发 | 类型安全,适合大型项目 |
| 生态系统 | AI生态丰富 | 前端/全栈开发更友好 |
| 性能 | 中等 | 较好(V8引擎优化) |
| 学习曲线 | 较低 | 中等(需要TypeScript知识) |
对于大多数生产环境项目,我推荐使用TypeScript方案,原因包括:
bash复制# 创建项目目录
mkdir my-mcp-server && cd my-mcp-server
# 初始化npm项目
npm init -y
# 安装核心依赖
npm install @modelcontextprotocol/sdk zod dotenv
# 安装开发依赖
npm install -D typescript @types/node ts-node nodemon eslint prettier
# 初始化TypeScript配置
npx tsc --init
关键配置文件示例(tsconfig.json):
json复制{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
bash复制# 创建虚拟环境
python -m venv venv
source venv/bin/activate # Linux/Mac
venv\Scripts\activate # Windows
# 安装依赖
pip install fastmcp pydantic python-dotenv
项目结构建议:
code复制my_mcp_server/
├── src/
│ ├── tools/
│ │ ├── __init__.py
│ │ ├── github_tools.py
│ │ └── slack_tools.py
│ ├── schemas/
│ ├── core/
│ │ ├── auth.py
│ │ └── error_handling.py
│ └── main.py
├── tests/
├── .env
└── requirements.txt
一个良好的MCP工具应该遵循以下设计规范:
以GitHub Issue搜索工具为例,我们来看具体实现:
typescript复制import { z } from "zod";
import { Octokit } from "@octokit/rest";
// 输入参数定义
const SearchIssuesInput = z.object({
query: z.string().min(2).max(200).describe("搜索关键词"),
repo: z.string().describe("仓库名称,格式:owner/repo"),
labels: z.array(z.string()).optional().describe("筛选标签"),
limit: z.number().int().min(1).max(100).default(20)
}).strict();
// 工具注册
server.registerTool(
"github_search_issues",
{
title: "搜索GitHub Issues",
description: "在指定仓库中搜索符合条件的问题",
inputSchema: SearchIssuesInput,
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true
}
},
async (params) => {
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
try {
const { data } = await octokit.rest.search.issuesAndPullRequests({
q: `repo:${params.repo} ${params.query}`,
per_page: params.limit
});
return {
content: [{
type: "text",
text: `找到${data.total_count}个匹配的issue`
}],
structuredContent: {
items: data.items.map(item => ({
number: item.number,
title: item.title,
url: item.html_url,
state: item.state
})),
has_more: data.total_count > params.limit
}
};
} catch (error) {
throw new Error(`搜索失败: ${error.message}`);
}
}
);
python复制from fastmcp import FastMCP
from pydantic import BaseModel, Field
from github import Github
mcp = FastMCP("github_mcp")
class SearchIssuesInput(BaseModel):
query: str = Field(..., min_length=2, max_length=200, description="搜索关键词")
repo: str = Field(..., description="仓库名称,格式:owner/repo")
labels: list[str] | None = Field(None, description="筛选标签")
limit: int = Field(20, ge=1, le=100)
@mcp.tool(
name="github_search_issues",
annotations={
"readOnlyHint": True,
"destructiveHint": False,
"idempotentHint": True
}
)
async def search_issues(params: SearchIssuesInput):
g = Github(process.env.GITHUB_TOKEN)
repo = g.get_repo(params.repo)
query = f"{params.query} "
if params.labels:
query += " ".join(f"label:{label}" for label in params.labels)
issues = repo.get_issues(state="open").get_page(0)[:params.limit]
return {
"content": f"找到{len(issues)}个匹配的issue",
"structured": [
{
"number": issue.number,
"title": issue.title,
"url": issue.html_url
} for issue in issues
]
}
分页是API工具中的常见需求,MCP推荐以下实现方式:
基于偏移量的分页:
typescript复制const result = await api.getItems({
offset: params.offset || 0,
limit: params.limit
});
return {
content: [...],
structuredContent: {
items: result.items,
next_offset: result.items.length === params.limit
? (params.offset || 0) + params.limit
: null
}
};
基于游标的分页:
python复制result = await api.list_items(
cursor=params.cursor,
page_size=params.limit
)
return {
"content": "...",
"structured": {
"items": result.items,
"next_cursor": result.next_cursor
}
}
注意事项:无论采用哪种分页方式,都应确保:
- 默认限制在20-50条之间
- 返回明确的has_more或next_cursor指示
- 不要一次性加载所有数据到内存
MCP服务器通常需要处理两种认证场景:
服务器级认证:用于MCP服务器访问外部API
python复制# .env文件
GITHUB_TOKEN=your_personal_access_token
SLACK_TOKEN=xoxb-your-bot-token
# 认证处理
from dotenv import load_dotenv
import os
load_dotenv()
github = Github(os.getenv("GITHUB_TOKEN"))
用户级认证:用于多租户场景
typescript复制// 使用OAuth 2.1的示例
server.use(async (context, next) => {
const token = context.request.headers['authorization']?.split(' ')[1];
if (!token) throw new Error('Missing authentication');
try {
const user = await verifyOAuthToken(token);
context.user = user;
await next();
} catch (error) {
throw new Error('Invalid token');
}
});
良好的错误处理应包含以下要素:
标准化的错误代码:
json复制{
"error": {
"code": "INVALID_INPUT",
"message": "query参数长度必须在2-200字符之间",
"details": {
"min": 2,
"max": 200,
"actual": 1
}
}
}
分级错误处理:
python复制@mcp.error_handler
async def handle_errors(exc: Exception):
if isinstance(exc, ValidationError):
return {
"code": "INVALID_INPUT",
"message": "输入验证失败",
"details": exc.errors()
}
elif isinstance(exc, ApiError):
return {
"code": "API_ERROR",
"message": str(exc)
}
else:
return {
"code": "INTERNAL_ERROR",
"message": "服务器内部错误"
}
可操作的错误消息:
有效的评估问题应具备以下特征:
复杂性:需要多个步骤或工具调用才能解决
xml复制<question>找出我们团队在过去两周创建的、标记为bug但尚未分配的所有GitHub issue,
然后检查Slack中是否有关于这些issue的讨论,最后总结最紧急的三个问题</question>
现实性:模拟真实用户需求
xml复制<question>我们的网站性能监控显示API响应时间在每天上午10点有明显上升,
请分析可能的原因并提出改进建议</question>
可验证性:有明确的正确答案
xml复制<question>我们仓库中哪个Python文件的TODO注释最多?具体有多少个?</question>
<answer>utils.py, 12个</answer>
完整的评估流程包括:
准备阶段:
执行阶段:
bash复制# 使用官方评估工具
mcp-eval --server ./my-server --questions eval/questions.xml
分析阶段:
评估报告示例:
code复制评估结果 (2024-03-15)
----------------------
总问题数: 10
正确回答: 8
部分正确: 1
失败: 1
平均工具调用次数: 3.2
平均响应时间: 4.5s
主要问题:
- 复杂分页查询处理不完整
- 多工具协同时上下文传递有误
合理的缓存可以显著提升MCP服务器性能:
typescript复制import NodeCache from 'node-cache';
const cache = new NodeCache({
stdTTL: 300, // 默认缓存5分钟
checkperiod: 60
});
server.registerTool("github_get_repo", {
// ...
}, async (params) => {
const cacheKey = `repo:${params.owner}/${params.repo}`;
const cached = cache.get(cacheKey);
if (cached) return cached;
const data = await fetchRepoData(params);
cache.set(cacheKey, data);
return data;
});
缓存注意事项:
当模型需要获取多个相关数据时,批量处理可以减少API调用:
python复制@mcp.tool(name="github_batch_get_issues")
async def batch_get_issues(params: BatchGetIssuesInput):
results = []
async with aiohttp.ClientSession() as session:
tasks = [fetch_issue(session, repo, number)
for repo, number in params.items]
results = await asyncio.gather(*tasks, return_exceptions=True)
return {
"items": [r for r in results if not isinstance(r, Exception)],
"errors": [{"item": params.items[i], "error": str(r)}
for i, r in enumerate(results) if isinstance(r, Exception)]
}
对于长时间运行的操作,流式响应能提升用户体验:
typescript复制server.registerTool("long_running_task", {
// ...
}, async (params, context) => {
const stream = new PassThrough();
// 立即返回流式响应
context.response.setHeader('Content-Type', 'text/event-stream');
context.response.setHeader('Cache-Control', 'no-cache');
context.response.setHeader('Connection', 'keep-alive');
stream.pipe(context.response);
// 异步处理任务
processTaskAsync(params, (progress) => {
stream.write(`data: ${JSON.stringify({progress})}\n\n`);
}).then(result => {
stream.write(`data: ${JSON.stringify({result})}\n\n`);
stream.end();
});
return; // 不返回常规响应
});
在实际项目中,我发现合理的工具拆分和组合能显著提升模型使用效率。例如,将复杂的多步操作拆分为原子工具,同时提供组合工具作为快捷方式。这种架构既保持了灵活性,又优化了常见场景的使用体验。
另一个重要经验是详尽的工具描述和示例。模型依赖这些元数据来正确使用工具,我习惯为每个工具提供:
最后,完善的监控和日志必不可少。我建议至少记录: