适用版本:MCP Spec 2025-06-18 / SDK 1.x
目录
- 一、MCP 协议简介
- 二、核心架构
- 三、协议消息
- 四、Tools / Resources / Prompts
- 五、Python SDK 开发
- 六、TypeScript SDK 开发
- 七、传输层
- 八、Sampling 与 Roots
- 九、客户端集成
- 十、调试与测试
- 十一、安全考量
- 十二、生产部署
- 十三、常见模式
- 十四、常见问题
- 十五、完整示例:数据库 MCP Server
一、MCP 协议简介
MCP(Model Context Protocol) 是 Anthropic 提出并开源的开放协议,让 LLM 应用以标准化方式访问外部工具、数据源、上下文。可类比"AI 领域的 USB-C"。
1.1 解决的问题
- 工具碎片化:每个 LLM 应用各自实现工具调用,无法复用
- 数据源接入难:每接入一个新数据源都要重写适配
- 能力组合受限:无法把多个独立工具组合给一个 agent
1.2 关键收益
| 角色 | 收益 |
|---|---|
| LLM 应用开发者 | 一次接入 MCP,自动获得整个生态的工具 |
| 工具/数据源开发者 | 写一次 MCP server,被所有 MCP client 复用 |
| 用户 | 在 Claude Desktop/Cursor/IDE 间统一工具体验 |
二、核心架构
┌─────────────────┐ ┌─────────────────┐
│ MCP Host │ │ MCP Server │
│ (Claude / IDE) │ │ (你的工具) │
│ │ JSON-RPC 2.0 │ │
│ ┌─────────────┐ │ ←────────────→ │ Tools │
│ │ MCP Client │ │ stdio / HTTP │ Resources │
│ └─────────────┘ │ │ Prompts │
└─────────────────┘ └─────────────────┘
2.1 三类角色
| 角色 | 说明 |
|---|---|
| Host | 用户面向的 LLM 应用(Claude Desktop、Cursor、IDE 插件) |
| Client | Host 内的协议客户端,与单个 Server 1:1 |
| Server | 暴露能力的进程(数据库、文件系统、API 网关等) |
2.2 三类能力
| 能力 | 谁控制 | 适用场景 |
|---|---|---|
| Tools | LLM | 模型自行决定调用(写、查询、副作用) |
| Resources | 应用 | 应用决定加载哪些(文件、文档、数据) |
| Prompts | 用户 | 用户主动选择的模板(slash command) |
三、协议消息
3.1 JSON-RPC 2.0 基础
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "query_db",
"arguments": { "sql": "SELECT * FROM users LIMIT 10" }
}
}
3.2 关键方法集
| 方法 | 方向 | 作用 |
|---|---|---|
initialize | C→S | 协议握手 |
notifications/initialized | C→S | 握手完成通知 |
tools/list | C→S | 枚举工具 |
tools/call | C→S | 调用工具 |
resources/list | C→S | 枚举资源 |
resources/read | C→S | 读取资源 |
resources/subscribe | C→S | 订阅资源变化 |
prompts/list | C→S | 枚举提示词模板 |
prompts/get | C→S | 获取提示词内容 |
sampling/createMessage | S→C | server 反向请求 LLM 推理 |
notifications/tools/list_changed | S→C | 工具列表变更 |
3.3 握手流程
Client Server
│ │
│── initialize (protocolVersion) ────>│
│<──── result (capabilities) ─────────│
│── notifications/initialized ───────>│
│ │
│── tools/list ──────────────────────>│
│<──── tools[] ───────────────────────│
│ │
│── tools/call ──────────────────────>│
│<──── content[] ─────────────────────│
四、Tools / Resources / Prompts
4.1 Tool(工具)
{
"name": "query_database",
"title": "Query Database",
"description": "执行只读 SQL 查询",
"inputSchema": {
"type": "object",
"properties": {
"sql": { "type": "string", "description": "SQL 语句" },
"limit": { "type": "integer", "default": 100 }
},
"required": ["sql"]
},
"annotations": {
"readOnlyHint": true,
"destructiveHint": false,
"idempotentHint": true
}
}
annotations 对 LLM 与 host 的安全决策很关键:
| 字段 | 含义 |
|---|---|
readOnlyHint | 不修改环境 |
destructiveHint | 可能产生破坏性副作用 |
idempotentHint | 多次调用结果一致 |
openWorldHint | 与外部世界交互(如网络) |
4.2 Resource(资源)
{
"uri": "file:///project/README.md",
"name": "README",
"title": "Project Readme",
"mimeType": "text/markdown",
"description": "项目说明文档"
}
URI scheme 自定义:db://、git://、s3:// 都可以。
4.3 Prompt(提示词模板)
{
"name": "code_review",
"title": "Code Review",
"description": "对指定文件做 Code Review",
"arguments": [
{ "name": "file_path", "description": "文件路径", "required": true },
{ "name": "focus", "description": "重点关注(perf/security/style)" }
]
}
五、Python SDK 开发
pip install "mcp[cli]"
5.1 最简 server(FastMCP)
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("calculator")
@mcp.tool()
def add(a: float, b: float) -> float:
"""两数相加"""
return a + b
@mcp.tool()
def divide(a: float, b: float) -> float:
"""两数相除"""
if b == 0:
raise ValueError("除数不能为零")
return a / b
if __name__ == "__main__":
mcp.run() # 默认 stdio
5.2 资源 + 动态参数
@mcp.resource("file://{path}")
def read_file(path: str) -> str:
"""读取本地文件"""
with open(path, "r", encoding="utf-8") as f:
return f.read()
@mcp.resource("config://app")
def app_config() -> str:
return json.dumps({"version": "1.0", "env": "prod"})
5.3 Prompt 模板
from mcp.server.fastmcp.prompts import base
@mcp.prompt()
def code_review(file_path: str, focus: str = "general") -> list[base.Message]:
return [
base.UserMessage(
f"请对 {file_path} 做 Code Review,重点关注 {focus}。"
)
]
5.4 上下文与日志
from mcp.server.fastmcp import Context
@mcp.tool()
async def long_task(items: list[str], ctx: Context) -> str:
for i, item in enumerate(items):
await ctx.info(f"处理 {item}")
await ctx.report_progress(i, len(items))
await process(item)
return f"完成 {len(items)} 项"
5.5 Sampling(server 反向调 LLM)
from mcp.types import SamplingMessage, TextContent
@mcp.tool()
async def summarize_long(text: str, ctx: Context) -> str:
result = await ctx.session.create_message(
messages=[SamplingMessage(
role="user",
content=TextContent(type="text", text=f"摘要:{text}")
)],
max_tokens=500,
)
return result.content.text
Sampling 让 server 借用 host 端的 LLM,无需自己持有 API key。
5.6 低层 API(精细控制)
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
app = Server("low-level-demo")
@app.list_tools()
async def list_tools() -> list[Tool]:
return [Tool(
name="echo",
description="Echo input",
inputSchema={
"type": "object",
"properties": {"text": {"type": "string"}},
"required": ["text"],
},
)]
@app.call_tool()
async def call_tool(name, arguments):
if name == "echo":
return [TextContent(type="text", text=arguments["text"])]
raise ValueError(f"unknown tool: {name}")
async def main():
async with stdio_server() as (read, write):
await app.run(read, write, app.create_initialization_options())
六、TypeScript SDK 开发
npm i @modelcontextprotocol/sdk zod
6.1 最简 server
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: "calculator", version: "1.0.0" });
server.registerTool(
"add",
{
title: "Add",
description: "两数相加",
inputSchema: { a: z.number(), b: z.number() },
annotations: { readOnlyHint: true, idempotentHint: true },
},
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }],
}),
);
await server.connect(new StdioServerTransport());
6.2 资源(含动态模板)
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
server.registerResource(
"file",
new ResourceTemplate("file://{path}", { list: undefined }),
{ title: "Local File" },
async (uri, { path }) => {
const text = await fs.readFile(path as string, "utf-8");
return { contents: [{ uri: uri.href, text, mimeType: "text/plain" }] };
},
);
6.3 Prompt
server.registerPrompt(
"review_pr",
{
title: "Review PR",
description: "审查指定 PR",
argsSchema: { pr_url: z.string().url(), focus: z.string().optional() },
},
({ pr_url, focus }) => ({
messages: [{
role: "user",
content: { type: "text", text: `请审查 ${pr_url},关注 ${focus ?? "整体"}` },
}],
}),
);
6.4 Sampling
server.registerTool(
"rewrite",
{ description: "用 LLM 重写文本", inputSchema: { text: z.string() } },
async ({ text }, { sendRequest }) => {
const result = await sendRequest({
method: "sampling/createMessage",
params: {
messages: [{ role: "user", content: { type: "text", text: `重写:${text}` } }],
maxTokens: 1000,
},
});
return { content: [{ type: "text", text: result.content.text }] };
},
);
七、传输层
7.1 stdio(本地工具首选)
- 进程间管道,host 启动 server 子进程
- 零配置,启动快
- 适合:CLI 工具、本地数据访问
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/docs"]
}
}
}
7.2 Streamable HTTP(远程 server 推荐)
mcp.run(transport="streamable-http", host="0.0.0.0", port=8080)
特性:
- 单一
POST /mcpendpoint - 服务端可选择返回 JSON 或 SSE 流
- 支持断线重连(
Last-Event-ID头) - 替代了旧的 HTTP+SSE 双端点设计
7.3 SSE(已弃用但仍兼容)
新项目不推荐使用,仅为向后兼容旧 client 保留。
7.4 选择策略
| 场景 | 选择 |
|---|---|
| 本地 CLI / 文件系统 | stdio |
| 公司内部远程 server | Streamable HTTP |
| 跨网公开 SaaS | Streamable HTTP + OAuth |
| 浏览器内 client | Streamable HTTP(必须) |
八、Sampling 与 Roots
8.1 Sampling
让 server 反过来调用 host 的 LLM,避免:
- server 持有用户的 API key
- 模型选择不一致
- 成本不透明
Host 必须显示弹窗征得用户同意(人类在回路)。
8.2 Roots
Client 告诉 server"你能访问哪些目录/URI"。Server 在工具执行时不应越界。
const client = new Client({ name: "my-app", version: "1.0" }, {
capabilities: {
roots: { listChanged: true },
},
});
client.setRequestHandler(ListRootsRequestSchema, async () => ({
roots: [
{ uri: "file:///workspace/project-a", name: "Project A" },
],
}));
Server 端:
roots = await ctx.session.list_roots()
for r in roots.roots:
if not file_path.startswith(r.uri.path):
raise PermissionError("path outside roots")
九、客户端集成
9.1 Claude Desktop
~/Library/Application Support/Claude/claude_desktop_config.json(macOS):
{
"mcpServers": {
"my-tools": {
"command": "uv",
"args": ["run", "python", "/path/to/server.py"]
}
}
}
9.2 Claude Code
claude mcp add my-tools -- python /path/to/server.py
claude mcp add --transport http weather https://weather.example.com/mcp
claude mcp list
9.3 Cursor
.cursor/mcp.json:
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": { "GITHUB_TOKEN": "ghp_xxx" }
}
}
}
9.4 自建 client(Python)
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
params = StdioServerParameters(
command="python", args=["server.py"]
)
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
result = await session.call_tool("add", {"a": 1, "b": 2})
print(result.content[0].text)
十、调试与测试
10.1 MCP Inspector(官方调试 UI)
npx @modelcontextprotocol/inspector python server.py
# 浏览器打开 http://localhost:5173
可视化测试 tools/resources/prompts,实时看协议消息。
10.2 单元测试
import pytest
from mcp.shared.memory import create_connected_server_and_client_session
from server import mcp
@pytest.mark.asyncio
async def test_add():
async with create_connected_server_and_client_session(mcp._mcp_server) as (_, client):
result = await client.call_tool("add", {"a": 2, "b": 3})
assert result.content[0].text == "5"
10.3 协议日志
import logging
logging.basicConfig(level=logging.DEBUG)
# 所有 JSON-RPC 消息会打到 stderr
⚠️ stdio 传输下,stdout 必须只输出协议消息,日志一律走 stderr。
10.4 Schema 校验
工具的 inputSchema 必须是合法 JSON Schema,否则 host 拒绝注册。可用 jsonschema 库做本地校验:
from jsonschema import Draft7Validator
Draft7Validator.check_schema(tool.inputSchema)
十一、安全考量
11.1 信任边界
[ Host (用户授权) ] ─ 用户视角的工具调用
│
▼
[ Client ] ─ 协议层校验
│
▼
[ Server (你的代码) ] ─ 真正执行操作
│
▼
[ 系统资源 ] ─ DB/FS/API
每一层都要做纵深防御。
11.2 工具风险等级
| 等级 | 示例 | 建议 |
|---|---|---|
| 安全 | 计算、查询 | annotations.readOnlyHint=true |
| 中风险 | 写文件、发邮件 | 描述里明确说明副作用 |
| 高风险 | 删除、付款、运维 | destructiveHint=true,host 必须二次确认 |
11.3 Prompt Injection 防护
工具返回内容会进入 LLM 上下文,任何外部数据都可能藏指令:
@mcp.tool()
async def fetch_url(url: str) -> str:
text = await http_get(url)
# 不要直接返回原始 HTML:考虑过滤、清洗、加边界标记
return f"<external_content>\n{sanitize(text)}\n</external_content>"
11.4 路径与资源限制
ALLOWED_ROOTS = ["/workspace", "/tmp/sandbox"]
def validate_path(p: str):
p = os.path.realpath(p)
if not any(p.startswith(r) for r in ALLOWED_ROOTS):
raise PermissionError(f"path outside sandbox: {p}")
11.5 速率限制
from aiolimiter import AsyncLimiter
limiter = AsyncLimiter(10, 1) # 10 req/s
@mcp.tool()
async def expensive_call(...):
async with limiter:
return await call_api(...)
11.6 OAuth(远程 server)
MCP 2025-06-18 规范要求远程 server 用 OAuth 2.1 + PKCE:
from mcp.server.auth import OAuthServerProvider
provider = OAuthServerProvider(
issuer_url="https://auth.example.com",
audience="my-mcp-server",
)
mcp = FastMCP("secure-server", auth=provider)
十二、生产部署
12.1 Dockerfile(Streamable HTTP)
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8080
HEALTHCHECK CMD python -c "import httpx; httpx.get('http://localhost:8080/health').raise_for_status()"
CMD ["python", "-m", "server"]
# server.py
mcp = FastMCP("prod-server")
@mcp.custom_route("/health", methods=["GET"])
async def health(_):
from starlette.responses import JSONResponse
return JSONResponse({"status": "ok"})
mcp.run(transport="streamable-http", host="0.0.0.0", port=8080)
12.2 K8s 部署
apiVersion: apps/v1
kind: Deployment
metadata: { name: mcp-server }
spec:
replicas: 3
selector: { matchLabels: { app: mcp } }
template:
metadata: { labels: { app: mcp } }
spec:
containers:
- name: mcp
image: registry/mcp-server:1.0
ports: [{ containerPort: 8080 }]
envFrom: [{ secretRef: { name: mcp-secrets } }]
readinessProbe:
httpGet: { path: /health, port: 8080 }
resources:
limits: { cpu: "1", memory: "1Gi" }
12.3 横向扩展注意
- stdio server 无法横向扩展:1 client = 1 进程
- HTTP server 默认无状态:可任意扩缩容
- 若使用 sampling/订阅等长连接特性,需 sticky session
12.4 配置管理
import os
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
db_url: str
api_key: str
log_level: str = "INFO"
class Config:
env_file = ".env"
settings = Settings()
十三、常见模式
13.1 工具组合
把多个工具拆细 + 组合,比单个"巨型工具"更适合 LLM 调用:
# 好:拆成原子操作
@mcp.tool() def list_tables() -> list[str]: ...
@mcp.tool() def describe_table(name: str) -> dict: ...
@mcp.tool() def query(sql: str) -> list[dict]: ...
# 差:一个大杂烩
@mcp.tool() def database_op(op: str, **kwargs): ...
13.2 资源 vs 工具
- 数据本身 → Resource(让 host 决定什么时候加载)
- 执行操作 → Tool(让 LLM 决定何时调用)
举例:
| 需求 | 用法 |
|---|---|
| 读 README | Resource file://README.md |
| 搜索代码 | Tool search_code(query) |
| 列出 issues | 都行,但 Resource 更利于上下文常驻 |
13.3 进度回报
长任务必须用 ctx.report_progress,否则 host 端无反馈:
@mcp.tool()
async def index_repo(path: str, ctx: Context) -> str:
files = list_files(path)
for i, f in enumerate(files):
await ctx.report_progress(i, len(files))
await index_one(f)
return f"已索引 {len(files)} 文件"
13.4 错误返回
工具内部错误不应抛异常导致协议中断,而应作为内容返回 + isError:
@mcp.tool()
async def query(sql: str) -> list[TextContent]:
try:
return [TextContent(type="text", text=run(sql))]
except SQLError as e:
return [TextContent(type="text", text=f"SQL error: {e}", isError=True)]
十四、常见问题
14.1 Claude Desktop 看不到 server
- 确认配置文件路径与平台一致
- 重启 Claude Desktop(配置变更不自动 reload)
- 看日志:macOS
~/Library/Logs/Claude/mcp-server-*.log
14.2 stdio server 启动后立即退出
- stdout 被业务日志污染 → 改走 stderr 或 logging 文件
print(...)默认走 stdout,不要直接 print
14.3 Tool 不被 LLM 调用
- description 太模糊 → 写明用途、输入输出示例
- 工具名不直观 → 用
verb_noun风格(search_docs、create_issue) - inputSchema 太复杂 → 拆成多个简单工具
14.4 大对象返回慢
- 返回 Resource URI 让 host 按需读取,而不是 Tool 内嵌大文本
- 流式返回(多次
ctx.info+ 最后一次 result)
14.5 类型 vs Schema 不匹配
Python SDK 自动从类型注解生成 schema,但复杂类型(嵌套 TypedDict)可能失败,可手动写:
@mcp.tool()
def search(filter: dict) -> list[dict]:
"""..."""
...
# 手写 schema:
search.input_schema = {
"type": "object",
"properties": {
"filter": {
"type": "object",
"properties": {
"tag": {"type": "string"},
"date": {"type": "string", "format": "date"},
},
},
},
"required": ["filter"],
}
十五、完整示例:数据库 MCP Server
一个生产级只读 SQL 查询 server:
import asyncio
import asyncpg
import os
from contextlib import asynccontextmanager
from mcp.server.fastmcp import FastMCP, Context
DB_URL = os.environ["DATABASE_URL"]
ALLOWED_SCHEMAS = {"public", "analytics"}
MAX_ROWS = 1000
pool: asyncpg.Pool | None = None
@asynccontextmanager
async def lifespan(server):
global pool
pool = await asyncpg.create_pool(DB_URL, min_size=1, max_size=5)
yield
await pool.close()
mcp = FastMCP("postgres-readonly", lifespan=lifespan)
def is_safe_sql(sql: str) -> bool:
s = sql.strip().lower()
forbidden = ("insert", "update", "delete", "drop", "alter", "truncate", "grant")
return s.startswith("select") and not any(w in s for w in forbidden)
@mcp.tool(
annotations={"readOnlyHint": True, "idempotentHint": True}
)
async def query(sql: str, ctx: Context) -> str:
"""执行只读 SELECT 查询。返回 JSON 数组。"""
if not is_safe_sql(sql):
return "ERROR: 仅允许 SELECT 查询"
sql = f"SELECT * FROM ({sql}) AS subq LIMIT {MAX_ROWS}"
await ctx.info(f"执行:{sql}")
async with pool.acquire() as conn:
rows = await conn.fetch(sql)
return str([dict(r) for r in rows])
@mcp.tool(annotations={"readOnlyHint": True})
async def list_tables() -> list[str]:
"""列出所有可访问的表"""
async with pool.acquire() as conn:
rows = await conn.fetch("""
SELECT table_schema || '.' || table_name AS qname
FROM information_schema.tables
WHERE table_schema = ANY($1::text[])
ORDER BY 1
""", list(ALLOWED_SCHEMAS))
return [r["qname"] for r in rows]
@mcp.tool(annotations={"readOnlyHint": True})
async def describe_table(name: str) -> dict:
"""查看表结构"""
schema, table = name.split(".", 1) if "." in name else ("public", name)
if schema not in ALLOWED_SCHEMAS:
return {"error": f"schema {schema} 不允许"}
async with pool.acquire() as conn:
rows = await conn.fetch("""
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema=$1 AND table_name=$2
""", schema, table)
return {"columns": [dict(r) for r in rows]}
@mcp.resource("schema://{table}")
async def get_schema(table: str) -> str:
info = await describe_table(table)
return str(info)
@mcp.prompt()
def explore_schema() -> str:
return ("先调用 list_tables 看看有哪些表,"
"再用 describe_table 选几个相关的查看结构,"
"最后写 query 提取数据。")
if __name__ == "__main__":
mcp.run(transport="streamable-http", host="0.0.0.0", port=8080)
15.1 客户端配置(Claude Code)
claude mcp add --transport http db https://db-mcp.internal:8080/mcp \
--header "Authorization: Bearer $TOKEN"
15.2 一次完整的对话
用户:我们最活跃的用户是谁?
LLM ─> list_tables()
LLM ─> describe_table("public.users")
LLM ─> describe_table("public.events")
LLM ─> query("SELECT u.name, COUNT(e.id) FROM users u
JOIN events e ON e.user_id = u.id
GROUP BY u.name ORDER BY 2 DESC LIMIT 5")
LLM <─ [{"name":"Alice","count":1234},...]
LLM:最活跃用户是 Alice,共 1234 次事件...
附录:速查
A.1 Annotations 快查
| 字段 | 含义 |
|---|---|
readOnlyHint | 只读 |
destructiveHint | 破坏性 |
idempotentHint | 幂等 |
openWorldHint | 涉外(网络) |
A.2 SDK 速查
| 功能 | Python | TypeScript |
|---|---|---|
| 注册工具 | @mcp.tool() | server.registerTool |
| 注册资源 | @mcp.resource(uri) | server.registerResource |
| 注册 Prompt | @mcp.prompt() | server.registerPrompt |
| 进度上报 | ctx.report_progress | extra.sendNotification |
| 反向 sampling | ctx.session.create_message | sendRequest |
| 启动 stdio | mcp.run() | server.connect(StdioServerTransport) |
| 启动 HTTP | mcp.run("streamable-http") | StreamableHTTPServerTransport |
A.3 调试入口
| 工具 | 用途 |
|---|---|
@modelcontextprotocol/inspector | 协议调试 UI |
mcp dev server.py | 内置 dev server |
| Claude Desktop logs | ~/Library/Logs/Claude/ |
| stderr | server 端日志输出位置 |
A.4 资源链接
- 规范:modelcontextprotocol.io
- Python SDK:github.com/modelcontex…
- TS SDK:github.com/modelcontex…
- 官方 server 集合:github.com/modelcontex…