第五课

MCP 从零实现教程(五)

第五课:实现真正的

tools/call

目标:实现 MCP 最核心的能力——客户端通过标准协议调用 Tool。


📚 本课目标

学完本课后,你将能够理解:

  • tools/listtools/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 等标准传输层,而我们的目标是先理解最小实现,再逐步靠近官方。

posted @ 2026-07-31 17:48  zwx901323  阅读(3)  评论(0)    收藏  举报