一次编写,被所有客户端共享的工具

MCP 解决的问题

上两篇我们用 Python 实现了工具注册、函数调用、Agent 循环。但这些工具都是"嵌在你自己代码里"的——Claude Desktop 用不到、Cursor 用不到、同事写的 Agent 也用不到。每个客户端想集成这些工具都要重新对接一次,这是 2023、2024 两年 AI 工具生态最大的痛点之一。

MCP(Model Context Protocol) 是 Anthropic 2024 年底开源的一个协议,用一句话总结它在做什么:把"工具"和"使用工具的客户端"之间的接口标准化。你用任何语言实现一个 MCP Server,把它注册到兼容 MCP 的客户端(Claude Desktop、Cursor、Cline、Windsurf 等),客户端的 Agent 就能自动发现、调用你这个 server 暴露的所有工具。类比成熟领域:MCP 之于 AI Agent,类似 LSP 之于 IDE、OpenAPI 之于 HTTP 服务。

本篇不做宏大叙事,直接动手写一个 MCP Server 接进 Claude Desktop,让你切身体会它解决的问题。

协议全貌(够用的简化版)

MCP 基于 JSON-RPC 2.0,通信方式支持两种:

  • stdio——客户端启动 server 子进程,通过标准输入输出通信。开发最简单,本地工具首选
  • HTTP + SSE——server 是独立服务,客户端通过 HTTP 连接。适合远程/多用户场景

Server 能向客户端暴露三类能力:

  • Tools——可调用的函数,和 Function Calling 概念一致
  • Resources——可读取的资源,比如文件、数据库表、API 端点
  • Prompts——可复用的 Prompt 模板

本篇只用 Tools 这一种,也是实际项目中用得最多的。

用 Python SDK 写第一个 MCP Server

官方 Python SDK 叫 mcp,装上就能用:

pip install mcp

下面实现一个能给 Agent 查询项目 git 信息的 MCP Server:

# mcp_git_server.py
import subprocess
from pathlib import Path
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("git-inspector")


def _run_git(args: list[str], cwd: Path) -> str:
    result = subprocess.run(
        ["git"] + args,
        cwd=str(cwd),
        capture_output=True,
        text=True,
        timeout=10,
    )
    if result.returncode != 0:
        return f"错误:{result.stderr.strip()}"
    return result.stdout.strip()


@mcp.tool()
def git_status(repo_path: str) -> str:
    """查看指定 Git 仓库的工作区状态。"""
    return _run_git(["status", "--short"], Path(repo_path))


@mcp.tool()
def git_log(repo_path: str, limit: int = 10) -> str:
    """查看最近的 N 条提交记录。"""
    return _run_git(["log", f"-{limit}", "--oneline"], Path(repo_path))


@mcp.tool()
def git_diff(repo_path: str) -> str:
    """查看当前未暂存的改动。"""
    return _run_git(["diff"], Path(repo_path))


@mcp.tool()
def current_branch(repo_path: str) -> str:
    """查看当前分支名。"""
    return _run_git(["branch", "--show-current"], Path(repo_path))


if __name__ == "__main__":
    mcp.run()

FastMCP 把 MCP 协议细节完全藏了起来。你只需要用 @mcp.tool() 装饰一个函数,SDK 会自动:

  • 从函数签名推断参数 schema(利用 Python 的类型注解)
  • 从 docstring 生成工具描述
  • 处理 JSON-RPC 协议层的请求响应

运行这个 server:

python mcp_git_server.py

默认用 stdio 模式,进程启动后通过 stdin/stdout 等待客户端连接。

接入 Claude Desktop

把 server 注册到 Claude Desktop。找到它的配置文件:

  • macOS:~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows:%APPDATA%\Claude\claude_desktop_config.json

编辑成这样(路径换成你自己的):

{
  "mcpServers": {
    "git-inspector": {
      "command": "python",
      "args": ["/absolute/path/to/mcp_git_server.py"]
    }
  }
}

重启 Claude Desktop。在对话框左下角应该能看到工具图标,点开能看到 git_statusgit_log 等几个工具已经被识别。这时候你问 Claude "/path/to/my/repo 这个仓库最近有哪些提交?",它会自动调用 git_log 工具。

整个过程你没写一行客户端代码。 Claude Desktop 不知道也不需要知道你的工具是 Python 写的还是 TypeScript 写的,不需要重新部署,也不用往它的二进制里塞代码。这就是协议标准化的直接收益。

Resources:给客户端读的内容

Tools 是"主动调用";Resources 是"被动读取"。比如你希望客户端能把某个配置文件、某段代码片段、某个 API 的响应作为上下文自动加载,用 Resources 表达更合适:

@mcp.resource("config://database")
def database_config() -> str:
    """数据库连接配置。"""
    return Path(".env").read_text()


@mcp.resource("file://{path}")
def read_file(path: str) -> str:
    """读取指定路径的文件内容。"""
    return Path(path).read_text()

支持模板化的 URI({path} 是参数)。客户端 UI 会把这些 Resources 展示给用户选择,或者自动检索。

实务上多数工具场景 Tools 就够用。Resources 的主要价值是给"用户主动选择上下文"的场景一个标准化入口。

用 Python 写客户端

反过来场景:你想在自己的 Python Agent 里使用 MCP 生态的现成 server(比如文件系统、数据库、各类 SaaS 的 MCP server)。

pip install mcp
# mcp_client_demo.py
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client


async def main():
    # 启动一个子进程 server 并连接
    server_params = StdioServerParameters(
        command="python",
        args=["mcp_git_server.py"],
    )

    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()

            # 列出所有可用工具
            tools = await session.list_tools()
            for t in tools.tools:
                print(f"- {t.name}: {t.description}")

            # 调用一个工具
            result = await session.call_tool(
                "git_status",
                {"repo_path": "/your/repo/path"},
            )
            print(result.content[0].text)


asyncio.run(main())

把这段和上一篇的 Agent 结合,就能让你的 Python Agent 直接消费任何 MCP server 暴露的工具。mcp 官方也提供了把 MCP 工具桥接成 OpenAI Function Calling 工具的适配层,用起来几乎无感。

生态现状和选型建议

2025 年下半年开始 MCP 生态爆发,几百个 server 覆盖了大部分常见场景:

  • 官方/社区核心 server——Filesystem、GitHub、Git、Postgres、Fetch、Puppeteer 等基础能力
  • SaaS 厂商自建——Cloudflare、Linear、Notion、Sentry 等都上线了官方 MCP server
  • 聚合器——Smithery、mcp.so 做了 server 目录,能一键安装

实务选型建议:

  • 你要给自己的 Agent 加能力——优先找现成的 MCP server 接入,避免重复造轮子
  • 你要对外暴露能力(给客户/同事用)——写一个 MCP Server 比写 REST API 更合适,因为你立刻能被所有 MCP 客户端复用
  • 你在做纯后端系统——不需要 MCP,直接用 Function Calling 就好。MCP 的价值在于跨客户端共享

什么时候不值得用 MCP:纯服务端、单一消费者的场景。这时候 MCP 的协议开销没有带来任何跨客户端收益,直接 Function Calling 更简单。

调试 MCP Server 的实用技巧

MCP Server 运行在 stdio 模式时,stderr 会被客户端吞掉,print() 调试会弄乱协议。正确做法:

用 logging 写文件——调试期间把所有日志写本地文件,别污染 stdout:

import logging
logging.basicConfig(
    filename="/tmp/mcp_git.log",
    level=logging.DEBUG,
    format="%(asctime)s %(message)s",
)

用 MCP Inspector——官方出的 GUI 调试工具,可以手动调用任意 tool 查看响应,不用先接 Claude Desktop。

npx @modelcontextprotocol/inspector python mcp_git_server.py

打开模拟模式——写单元测试时用 mcp.server.fastmcp.testing 里的工具跳过真实 stdio 直接 in-process 调用。

本篇要点

  • MCP 把"AI Agent 的工具接口"标准化,一次编写被所有兼容客户端复用
  • Python SDK mcpFastMCP 让写 server 只剩一个装饰器的工作量
  • stdio 模式开发最快,只要客户端配置文件里把 command 指到你的脚本就自动接入
  • Resources 补充了"可读上下文"的场景,但 Tools 仍是最常用的能力
  • 生态选型:自用找现成 server 接入,对外输出能力写自己的 server
  • 调试避开 stdout,用文件 logging 或 MCP Inspector

下一篇

前面所有内容都依赖调用云端 API,付费也可能有隐私顾虑。第 10 篇转到 本地模型——用 Ollama 在自己的电脑上跑开源 LLM,用 OpenAI SDK 无缝切换云端和本地。对学习、敏感数据处理、边缘部署都是关键能力。

参考资料

版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。

(采用 CC BY-NC-SA 4.0 许可协议进行授权)

本文标题:MCP 协议:Agent 的通用接口

本文链接:https://www.sshipanoo.com/blog/ai/ai-for-python/09-MCP协议/

本文最后一次更新为 天前,文章中的某些内容可能已过时!