第五课
MCP 从零实现教程(五)
第五课:实现真正的
tools/call
目标:实现 MCP 最核心的能力——客户端通过标准协议调用 Tool。
📚 本课目标
学完本课后,你将能够理解:
tools/list和tools/call的关系- 什么是 JSON-RPC Request
- 什么是 JSON-RPC Response
- 如何实现一个真正符合 MCP 思想的 Tool 调用接口
到这一课,我们已经不再是“模拟 Tool 调用”,而是开始按照 MCP 的通信方式组织代码。
MCP 官方规范规定,客户端调用工具时发送 tools/call 请求,服务器根据请求中的工具名称和参数执行对应工具,并返回标准 JSON-RPC 响应。
📖 第一部分:回顾上一课
上一课我们实现了:
Client
│
▼
tools/list
│
▼
Server
│
▼
返回所有 Tool
但是客户端还有一个问题:
“我知道有哪些 Tool 了,那我要怎么调用它?”
答案就是:
tools/call
📖 第二部分:真实 MCP 请求长什么样?
客户端发送:
{
"jsonrpc": "2.0",
"id": 100,
"method": "tools/call",
"params": {
"name": "add",
"arguments": {
"a": 10,
"b": 20
}
}
}
观察一下。
整个请求可以分成四部分:
jsonrpc
↓
请求版本
----------------
id
↓
请求编号
----------------
method
↓
调用哪个接口
----------------
params
↓
接口参数
是不是和 REST API 很像?
📖 第三部分:先解析 Request
假设收到:
request = {
"jsonrpc": "2.0",
"id": 100,
"method": "tools/call",
"params": {
"name": "add",
"arguments": {
"a": 10,
"b": 20
}
}
}
解析:
params = request["params"]
tool_name = params["name"]
args = params["arguments"]
print(tool_name)
print(args)
输出:
add
{
"a": 10,
"b": 20
}
是不是非常简单?
💻 第四部分:执行 Tool
上一课:
我们已经有:
TOOLS = {
"add": add,
"weather": weather
}
所以:
func = TOOLS[tool_name]
result = func(**args)
print(result)
输出:
30
是不是和第三课一样?
唯一变化:
参数来自:
JSON-RPC
而不是:
request = {
...
}
💻 第五部分:封装 call_tool()
开始封装。
def call_tool(request):
params = request["params"]
tool_name = params["name"]
args = params["arguments"]
if tool_name not in TOOLS:
return {
"jsonrpc": "2.0",
"id": request["id"],
"error": {
"message": "Tool 不存在"
}
}
result = TOOLS[tool_name](**args)
return {
"jsonrpc": "2.0",
"id": request["id"],
"result": result
}
是不是已经开始有服务器的感觉了?
💻 第六部分:统一 Dispatcher
上一课 Dispatcher:
if method == "tools/list":
今天增加:
def dispatch(request):
method = request["method"]
if method == "tools/list":
return list_tools()
elif method == "tools/call":
return call_tool(request)
return {
"jsonrpc": "2.0",
"id": request.get("id"),
"error": {
"message": "未知 Method"
}
}
Dispatcher 已经可以处理多个接口。
💻 第七部分:完整示例
TOOLS = {}
def tool(func):
TOOLS[func.__name__] = func
return func
@tool
def add(a: int, b: int):
return a + b
def call_tool(request):
params = request["params"]
tool_name = params["name"]
args = params["arguments"]
result = TOOLS[tool_name](**args)
return {
"jsonrpc": "2.0",
"id": request["id"],
"result": result
}
request = {
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "add",
"arguments": {
"a": 10,
"b": 20
}
}
}
print(call_tool(request))
输出:
{
"jsonrpc": "2.0",
"id": 1,
"result": 30
}
📖 第八部分:为什么要有 id?
很多新手都会问:
为什么有:
"id": 1
因为:
客户端可能同时发送:
请求1
↓
天气
------------
请求2
↓
搜索
------------
请求3
↓
数据库
服务器返回顺序可能变成:
请求2
请求1
请求3
所以:
客户端需要:
id
知道:
这是谁的结果。
所以:
请求:
{
"id": 99
}
返回:
{
"id": 99
}
必须一致。
📖 第九部分:目前整个系统流程
现在已经拥有:
Client
│
├──────────────┐
│ │
▼ ▼
tools/list tools/call
│ │
└──────┬───────┘
▼
Dispatcher
▼
Tool Registry
▼
Python Function
▼
Result
▼
JSON-RPC Response
看到这里,
其实 MCP 已经没有秘密了。
💻 第十部分:优化 Registry
上一课:
TOOLS["add"] = add
建议升级:
TOOLS["add"] = {
"func": add,
"description": "加法",
"parameters": {
"a": int,
"b": int
}
}
以后:
调用:
result = TOOLS[tool_name]["func"](**args)
这样:
Registry 就真正成为:
一个 Tool 数据库。
🧪 本课练习
新增:
@tool
def multiply(a: int, b: int):
return a * b
@tool
def hello(name: str):
return f"你好,{name}"
分别构造:
{
"method": "tools/call",
"params": {
"name": "multiply",
"arguments": {
"a": 6,
"b": 7
}
}
}
以及:
{
"method": "tools/call",
"params": {
"name": "hello",
"arguments": {
"name": "John"
}
}
}
确保:
dispatch(request)
返回正确 JSON-RPC。
🎯 本课总结
今天,我们已经实现了 MCP 最核心的接口。
目前已经完成:
✅ Tool 注册(Registry)
✅ Tool 元数据(Metadata)
✅ Tool Discovery(tools/list)
✅ Tool 调用(tools/call)
✅ JSON-RPC 请求解析
✅ JSON-RPC 响应返回
如果把网络通信去掉,你已经拥有了一个 Mini MCP Server 内核。
🚀 下一课预告(真正开始接近官方 MCP)
到目前为止,我们一直是:
Python 字典
│
▼
dispatch()
下一课,我们终于要把它变成真正的服务器。
我们将实现:
Client
│
HTTP / STDIO
│
▼
Mini MCP Server
│
▼
Dispatcher
│
▼
Tool Registry
你会第一次看到:
另一个 Python 程序真的可以连接到你写的 MCP Server,并调用你的 Tool。
这也是官方 MCP Server 的运行方式,只不过官方实现支持 STDIO、Streamable HTTP 等标准传输层,而我们的目标是先理解最小实现,再逐步靠近官方。

浙公网安备 33010602011771号