Skip to content

MCP与工具接入

最麻烦的不是写函数本身,而是每个 AI 客户端都要重新接一遍。今天在脚本里写 list_todos,明天在 IDE 助手里又写一套查询接口,后天换到另一个 Agent 里还要再描述一次参数和返回值。工具函数没变,变的是外面那层接法。

MCP 解决的就是这一层接法。它把工具、资源和提示模板放到一个独立服务里,客户端按统一协议去发现和调用。这样一来,工具不再绑死在某一个脚本里,而是由 MCP server 暴露出来,能被支持 MCP 的客户端接上。

这里的 Host 可以理解成用户正在用的 AI 应用。Client 是 Host 里负责连某一个 MCP server 的连接对象。Server 才是真正暴露工具和资料的地方。一个 Host 可以连多个 server,但每个 client 通常只维护到一个 server 的连接。

一、没有 MCP 的接法

08 里的待办 Agent 是手写工具接入:

python
if tool_call.name == "list_todos":
    return list_todos()

if tool_call.name == "get_todo":
    return get_todo(todo_id)

这种写法很清楚,也适合学习。问题出在复用上。另一个 Agent 也想查待办时,要把工具定义、参数 schema、路由、错误处理再搬一遍。工具越多,这些重复代码越明显。

MCP 的思路是把这层拆出去:待办系统自己提供一个 todo-mcp-server,里面暴露 list_todosget_todo。客户端只负责连接 server,向它要工具列表,再把模型选择的工具调用转发过去。

二、Tools

Tools 是 MCP 里最像函数调用的东西。一个 tool 有名字、描述、参数 schema,模型看到这些信息后,可以决定要不要调用。

比如待办服务暴露一个工具:

json
{
  "name": "get_todo",
  "description": "按 todo_id 查询一条待办详情",
  "inputSchema": {
    "type": "object",
    "properties": {
      "todo_id": {
        "type": "string",
        "description": "待办 id,例如 todo-002"
      }
    },
    "required": ["todo_id"]
  }
}

客户端发现工具时,会向 server 发类似 tools/list 的请求。真正调用时,会发 tools/call,带上工具名和参数:

json
{
  "name": "get_todo",
  "arguments": {
    "todo_id": "todo-002"
  }
}

server 执行本地逻辑,再把结果返回给客户端。模型看到的还是工具结果;只不过工具从“脚本里的 Python 函数”变成了“MCP server 暴露出来的能力”。

Tools 适合有动作的事情:查接口、查数据库、跑计算、读取某个对象详情。写操作也可以做成 tool,但风险会立刻变高。比如 delete_todosend_emailrestart_service 这种工具,客户端界面和 server 侧都要做确认、鉴权和日志。提示词管不住误操作,真正的限制还得写在工具实现和权限配置里。

三、Resources

Resources 更像资料入口。它不强调“执行一个动作”,而是把一份上下文暴露给客户端,比如文件、数据库 schema、项目配置、某个静态文档。

待办例子里,可以有一个资源:

text
todo://all

它代表当前所有待办的只读视图。客户端读取这个资源时,server 返回内容:

json
{
  "uri": "todo://all",
  "mimeType": "application/json",
  "text": "[{\"id\":\"todo-001\",\"title\":\"买菜\"}]"
}

Resources 和 Tools 容易混。一个简单判断是:只是拿资料,更像 resource;需要执行某个函数、带参数、产生计算或副作用,更像 tool。

比如:

  • todo://all:所有待办列表,适合 resource。
  • get_todo(todo_id):按 id 查详情,适合 tool。
  • search_docs(query):根据关键词检索资料,适合 tool。
  • file:///repo/README.md:一份具体文件,适合 resource。

实际系统里两者经常一起用。工具执行完也可能返回 resource 链接,让客户端继续读取更大的内容。

四、Prompts

Prompts 是 server 暴露出来的提示模板。它不像 tool 那样由模型自动调用,更像用户或客户端选择的一段固定模板。

比如一个代码检查 server 可以暴露:

text
review_code

客户端拿到这个 prompt 后,根据参数生成一段提示词,再交给模型。它的价值在于把某个系统的推荐问法、固定步骤、字段要求放在 server 里,而不是让每个客户端都手写一遍。

Prompts 在运维里也能见到类似形状。比如同一个日志平台希望排查请求超时时都带上时间范围、trace id、服务名、错误片段,那就可以把这些输入要求整理成 prompt 模板。模板本身不查日志,真正查日志还是 tool 或 resource。

五、传输方式

MCP 常见两种传输方式:stdio 和 Streamable HTTP。

stdio 最像本地小工具。客户端启动一个本地进程,MCP server 从标准输入读 JSON-RPC 消息,再把响应写到标准输出。这个方式适合本机文件、命令行工具、本地脚本。日志只能写到 stderrstdout 里只能放协议消息;这点很关键,不然客户端会把普通日志当成协议内容解析。

Streamable HTTP 更像远程服务。server 提供一个 HTTP 入口,客户端用 POST/GET 交换消息。远程接入时要认真处理认证、Origin 校验、监听地址和会话。比如本地调试时只监听 127.0.0.1,比直接绑 0.0.0.0 安全得多。

学习阶段先看 stdio 更容易。它像把一个命令行工具包装成 MCP server;等要跨机器、多人共用、统一鉴权时,再看 HTTP 方式。

六、只读边界

MCP server 暴露的工具越接近真实系统,越要先画清楚只读边界。

比如待办 server 里,下面几个工具风险完全不同:

text
list_todos      只读
get_todo        只读
create_todo     写入
delete_todo     删除

学习和排查类工具先从只读开始最稳。只读工具即使模型判断错了,也只是查错资料或回答不准;写入和删除工具一旦放出去,就要考虑用户确认、权限、审计、幂等、回滚。

一个很具体的例子:query_orders(order_id) 可以直接返回订单状态,风险较低;refund_order(order_id) 这种退款动作,不能只靠模型说“用户想退款”就执行。server 侧至少要检查调用者权限、订单状态、金额、是否重复退款,并且把调用记录写下来。

所以 MCP 不是让模型绕过系统权限。更准确地说,它把工具入口标准化了,但真正能不能执行、执行到哪一步,仍然由 server 和 Host 控制。

七、本地接入

本地 MCP 接入通常会有一个配置,告诉客户端怎么启动 server。形状大概是这样:

json
{
  "mcpServers": {
    "todo": {
      "command": "uv",
      "args": ["run", "python", "scripts/todo_mcp_server.py"],
      "env": {
        "TODO_FILE": "todos.json"
      }
    }
  }
}

这段配置表达几件事:

  • server 名字叫 todo
  • 客户端启动它时运行 uv run python scripts/todo_mcp_server.py
  • server 通过 TODO_FILE 找到待办文件。

配置写完以后,客户端会启动 server,做初始化和能力协商,然后请求 tools/listresources/list 之类的信息。模型真正要查待办时,客户端再把调用转给这个 server。

如果连接不上,先看三个地方。第一,命令本身能不能单独运行;第二,server 有没有把普通日志写到 stdout;第三,工作目录和环境变量是不是对。比如 TODO_FILE=todos.json 在项目根目录能找到,换到别的目录启动就可能变成找不到文件。

八、MCP 与函数调用

函数调用和 MCP 不是互相替代的关系。函数调用是模型和当前程序之间的工具协议,MCP 是客户端和外部 server 之间的工具协议。放到一条链路里,大概是这样:

08 里手写的是 Host 到 Tool 的直连。MCP 多了一层 server,把工具独立出来。工具少、只在一个脚本里用,直连就够了;工具要给多个客户端复用,MCP 的价值就出来了。

九、接入记录

MCP 工具也要留痕。至少记录四件事:

  • 调用了哪个 server。
  • 调用了哪个 tool。
  • 参数是什么。
  • 返回成功还是失败。

比如:

json
{
  "server": "todo",
  "tool": "get_todo",
  "arguments": {"todo_id": "todo-002"},
  "ok": true
}

排查时这条记录很有用。如果模型回答错了,先看 MCP 调用到底有没有发生;发生了,再看参数是不是对;参数对,再看 server 返回了什么。很多问题不是模型没能力,而是工具入口、参数、权限、工作目录在某一层就已经错了。