MCP 协议
每接一个工具都要写一遍适配代码,MCP 就是来解决这个问题的。工具写成 MCP Server,所有支持它的 Agent 都能直接用。
为什么要有 MCP
假设有 5 个 Agent 应用和 10 个外部系统。没有统一标准时,每个应用都要给每个系统写一遍适配,一共 50 份代码。
MCP(Model Context Protocol)是一个开放协议,规定 Agent 应用和外部工具怎么对话。系统只要实现一次 MCP Server,应用只要实现一次 MCP Client,两边就都能互通。
写过 IDE 插件的话,这个思路来自 LSP:编辑器和语言服务器之间有统一协议,编辑器不用为每种语言单独适配。
架构
MCP 里有三个角色:
- Host:Agent 应用本身,比如 Claude Code 或你写的程序
- Client:Host 里的连接器,每个连接对应一个 Server
- Server:提供能力的服务,可以是本地进程,也可以是远程服务
通信使用 JSON-RPC 2.0。Server 能提供三种能力,区别在于由谁决定使用:
Tools 最常用,本章也以它为主。
写一个 Server
MCP 的 Python SDK 从 2.0 版起把 FastMCP 改名为 MCPServer,网上的旧教程要注意这个变化。用法没怎么变:函数加上装饰器就是工具,类型注解和文档字符串自动变成工具定义。
pip install mcp定义 MCP Server
from mcp.server.mcpserver import MCPServer
server = MCPServer("weather")
@server.tool()
def get_weather(city: str) -> str:
"""查询城市当前的天气"""
return f"{city}:晴,25 度"
if __name__ == "__main__":
server.run() # 默认用 stdio 通信void run() {
var mapper = McpJsonDefaults.getMapper();
var transport = new StdioServerTransportProvider(mapper);
var tool = McpSchema.Tool.builder()
.name("get_weather")
.description("查询城市当前的天气")
.inputSchema(mapper, """
{"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]}
""")
.build();
McpServer.sync(transport)
.serverInfo("weather", "1.0.0")
.toolCall(tool, (exchange, request) -> {
var city = request.arguments().get("city");
return McpSchema.CallToolResult.builder()
.addTextContent(city + ":晴,25 度")
.build();
})
.build();
}fun run() {
val mapper = McpJsonDefaults.getMapper()
val transport = StdioServerTransportProvider(mapper)
val tool = McpSchema.Tool.builder()
.name("get_weather")
.description("查询城市当前的天气")
.inputSchema(
mapper,
"""{"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]}""",
)
.build()
McpServer.sync(transport)
.serverInfo("weather", "1.0.0")
.toolCall(tool) { _, request ->
val city = request.arguments()["city"]
McpSchema.CallToolResult.builder()
.addTextContent("$city:晴,25 度")
.build()
}
.build()
}| 对比项 | Python | Java |
|---|---|---|
| 声明工具 | 装饰器 | Builder 加回调 |
| 输入 Schema | 从类型注解生成 | 手写或由框架生成 |
| 取参数 | 函数参数 | request.arguments() |
| 对比项 | Python | Kotlin |
|---|---|---|
| 声明工具 | 装饰器 | Builder 加回调 |
| 输入 Schema | 从类型注解生成 | 手写或由框架生成 |
| 取参数 | 函数参数 | request.arguments() |
Java 项目一般不直接用这个 Builder。Spring AI 提供了注解式的写法,体验和 Python 的装饰器接近。
调用 Server
Client 负责连接和通信。直接把 server 对象传进去,就在同一个进程里通信,不用启动子进程,写测试时很方便。
import asyncio
from mcp import Client
from mcp.server.mcpserver import MCPServer
server = MCPServer("weather")
@server.tool()
def get_weather(city: str) -> str:
"""查询城市当前的天气"""
return f"{city}:晴"
async def main():
async with Client(server) as client:
result = await client.list_tools()
tool = result.tools[0]
print(tool.name) # => get_weather
print(tool.description) # => 查询城市当前的天气
print(tool.input_schema["required"]) # => ['city']
answer = await client.call_tool("get_weather", {"city": "北京"})
print(answer.content[0].text) # => 北京:晴
print(answer.is_error) # => False
asyncio.run(main())资源和提示
资源用网址形式的标识符,提示是可以带参数的模板。它们的写法和工具一样。
import asyncio
from mcp import Client
from mcp.server.mcpserver import MCPServer
server = MCPServer("docs")
@server.resource("docs://readme")
def readme() -> str:
"""项目说明"""
return "这是项目说明"
@server.prompt()
def review(code: str) -> str:
"""审查一段代码"""
return f"请审查这段代码:\n{code}"
async def main():
async with Client(server) as client:
resource = await client.read_resource("docs://readme")
print(resource.contents[0].text) # => 这是项目说明
prompt = await client.get_prompt("review", {"code": "x = 1"})
print(prompt.messages[0].role) # => user
asyncio.run(main())传输方式
Server 和 Client 之间有两种主要的传输方式:
stdio 模式下 Host 启动子进程并通过管道通信。
import asyncio
import sys
from mcp import Client, StdioServerParameters
async def main():
params = StdioServerParameters(
command=sys.executable,
args=["weather_server.py"],
)
async with Client(params) as client:
result = await client.call_tool("get_weather", {"city": "上海"})
print(result.content[0].text) # => 上海:晴
asyncio.run(main())连接远程服务时,把网址传给 Client 就行。
接入 Agent 循环
把 MCP 接进「工具调用循环」一章的 Agent 只要两步:把 list_tools 的结果转成模型要的工具定义,模型要调用时转给 call_tool。
import asyncio
import json
from mcp import Client
from openai import AsyncOpenAI
llm = AsyncOpenAI()
def to_openai(tool) -> dict:
return {
"type": "function",
"function": {
"name": tool.name,
"description": tool.description,
"parameters": tool.input_schema,
},
}
async def run_agent(mcp: Client, task: str) -> str:
tools = [to_openai(t) for t in (await mcp.list_tools()).tools]
messages = [{"role": "user", "content": task}]
for _ in range(10):
response = await llm.chat.completions.create(
model=MODEL, messages=messages, tools=tools,
)
message = response.choices[0].message
messages.append(message.model_dump(exclude_none=True))
if not message.tool_calls:
return message.content
for call in message.tool_calls:
result = await mcp.call_tool(
call.function.name,
json.loads(call.function.arguments),
)
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": result.content[0].text,
})
raise RuntimeError("超过最大步数")
async def main():
async with Client(server) as mcp:
print(await run_agent(mcp, "北京天气?")) # => 北京今天晴
asyncio.run(main())现成的 Agent 应用大多通过配置文件接入 Server,不用写代码。下面是常见的写法,具体文件位置看各应用的文档。
{
"mcpServers": {
"weather": {
"command": "python",
"args": ["weather_server.py"]
}
}
}安全
MCP Server 能执行任意代码,接入之前要像审查依赖一样审查它:
- 只用可信来源的 Server。 它运行时拥有你的本地权限。
- 工具描述也不可信。 描述会进入模型的上下文,恶意 Server 可以在里面藏指令,规范明确要求把这些描述当作不可信内容。
- 工具调用要经过确认。 规范要求 Host 在调用工具前取得用户同意。
- 限制权限。 给 Server 的 Token 只开必要的权限,只读就够的时候不要给写权限。
- 注意工具结果。 Server 返回的内容可能包含恶意指令,详见「Prompt Injection」。
| Python | Java | Kotlin | 说明 |
|---|---|---|---|
MCPServer("name") | McpServer.sync(transport) | McpServer.sync(transport) | 创建服务 |
@server.tool() | .toolCall(tool, handler) | .toolCall(tool) { } | 注册工具 |
server.run() | StdioServerTransportProvider | StdioServerTransportProvider | 默认用 stdio |
Client(server) | McpClient.sync(transport) | McpClient.sync(transport) | 连接服务 |
client.call_tool() | client.callTool() | client.callTool() | 调用工具 |
本章要点
- MCP 统一了 Agent 应用和外部工具的连接方式,工具写一次到处能用
- Host 是应用,Client 是连接器,Server 提供 Tools、Resources 和 Prompts
- Python SDK 2.x 用
MCPServer,函数加@server.tool()就是工具 - 本地用 stdio,远程用 Streamable HTTP,stdio 模式下不能向标准输出打印
- 接入 Agent 时,把
list_tools转成工具定义,把调用转给call_tool - Server 拥有真实权限,工具描述和返回内容都不可信
参考资料:MCP 规范MCP 简介MCP Python SDKPython SDK v2 迁移指南MCP Java SDK