MCP协议实战:从零构建AI Agent连接外部工具的完整指南

3 阅读11分钟

本文将带你深入理解 Model Context Protocol(MCP)的核心架构,并通过完整的代码示例,手把手教你搭建 MCP Server,实现 AI Agent 与外部工具的无缝对接。

一、引言

如果你最近在关注 AI 工具链的发展,一定听过一个词:MCP。

2024 年底,Anthropic 正式开源了 Model Context Protocol,目标很明确——为大语言模型提供一套标准化的"连接外部世界"的协议。你可以把它理解为 AI 领域的 USB-C 接口:不管你用什么模型、什么客户端,只要遵循 MCP 协议,就能以统一的方式调用外部工具、访问数据源、执行操作。

在 MCP 出现之前,每个 AI 应用都需要为不同的工具写不同的集成代码。接一个数据库要写一套,接一个 API 又要写一套,维护成本极高。MCP 的出现,把这个 M x N 的问题变成了 M + N 的问题——工具提供方只需要实现一个 MCP Server,客户端只需要实现一个 MCP Client,双方就能互相连接。

本文不讲空话,我们直接从协议架构讲起,然后用 Python 和 TypeScript 写出完整可运行的 MCP Server,最后接入 Claude Desktop 和 Cursor 进行实测。

二、MCP 协议简介

2.1 MCP 是什么

MCP(Model Context Protocol)是一个基于 JSON-RPC 2.0 的开放协议,定义了 AI 模型与外部工具之间的通信标准。它的核心理念是:

  • 标准化:统一的协议规范,任何工具只需实现一次即可被所有兼容客户端调用
  • 安全性:内置权限控制机制,工具的能力边界由 Server 端明确声明
  • 可组合性:多个 MCP Server 可以同时连接到一个客户端,Agent 按需调用

2.2 核心概念

MCP 协议定义了三种核心原语(Primitive):

原语说明典型用途
Tools模型可以调用的函数查询数据库、调用 API、执行计算
Resources模型可以读取的数据源文件内容、数据库记录、配置信息
Prompts预定义的提示词模板代码审查模板、SQL 生成模板

其中 Tools 是最常用的,也是本文的重点。一个 Tool 的定义包括名称、描述、参数的 JSON Schema,模型根据这些信息决定何时以及如何调用它。

2.3 通信机制

MCP 支持两种传输方式:

Stdio 传输:客户端以子进程方式启动 MCP Server,通过标准输入/输出进行通信。适合本地场景,配置简单,也是目前最常用的方式。

SSE(Server-Sent Events)传输:基于 HTTP 的传输方式,Server 作为独立服务运行。适合远程部署和多客户端共享的场景。最新的协议版本已经演进为 Streamable HTTP 传输,兼容性更好。

三、架构设计

3.1 整体架构

一个完整的 MCP 系统包含三个角色:

+------------------+       +------------------+       +------------------+
|                  |       |                  |       |                  |
|   MCP Host       | <---> |   MCP Client     | <---> |   MCP Server     |
|  (Claude Desktop |       |  (协议客户端)     |       |  (工具提供方)     |
|   Cursor 等)     |       |                  |       |                  |
+------------------+       +------------------+       +------------------+
  • Host:最终用户使用的 AI 应用,负责管理 Client 实例和用户交互
  • Client:协议客户端,维护与 Server 之间的一对一连接,处理协议层的握手、能力协商
  • Server:工具提供方,暴露 Tools、Resources、Prompts 供客户端调用

3.2 请求生命周期

一次典型的工具调用流程如下:

  1. 客户端启动 Server,完成 initialize 握手,交换双方支持的能力
  2. 客户端调用 tools/list 获取 Server 暴露的所有工具定义
  3. 用户发送消息,模型根据工具定义判断需要调用某个工具
  4. 客户端向 Server 发送 tools/call 请求,携带工具名和参数
  5. Server 执行工具逻辑,返回结果
  6. 模型根据工具返回结果生成最终回复

整个过程中,协议保证了类型安全和错误处理的一致性。

四、实战代码

4.1 用 Python 构建 MCP Server

我们来构建一个实际有用的 MCP Server:一个支持查询天气和汇率的工具服务。

首先安装依赖:

pip install mcp httpx

完整的 Server 实现如下:

# weather_server.py
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
import httpx
import json

# 创建 Server 实例
server = Server("weather-tools")


@server.list_tools()
async def list_tools() -> list[Tool]:
    """声明本 Server 提供的所有工具"""
    return [
        Tool(
            name="get_weather",
            description="查询指定城市的当前天气信息,包括温度、湿度、天气状况",
            inputSchema={
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "城市名称,例如:北京、上海、Tokyo"
                    }
                },
                "required": ["city"]
            }
        ),
        Tool(
            name="get_exchange_rate",
            description="查询两种货币之间的实时汇率",
            inputSchema={
                "type": "object",
                "properties": {
                    "from_currency": {
                        "type": "string",
                        "description": "源货币代码,例如:USD、CNY、EUR"
                    },
                    "to_currency": {
                        "type": "string",
                        "description": "目标货币代码,例如:USD、CNY、EUR"
                    }
                },
                "required": ["from_currency", "to_currency"]
            }
        )
    ]


@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
    """处理工具调用请求"""
    if name == "get_weather":
        return await handle_get_weather(arguments)
    elif name == "get_exchange_rate":
        return await handle_get_exchange_rate(arguments)
    else:
        raise ValueError(f"未知工具: {name}")


async def handle_get_weather(arguments: dict) -> list[TextContent]:
    city = arguments["city"]
    async with httpx.AsyncClient() as client:
        # 使用 wttr.in 的免费 API
        resp = await client.get(
            f"https://wttr.in/{city}?format=j1",
            timeout=10.0
        )
        resp.raise_for_status()
        data = resp.json()

    current = data["current_condition"][0]
    result = {
        "city": city,
        "temperature_c": current["temp_C"],
        "humidity": current["humidity"],
        "description": current["weatherDesc"][0]["value"],
        "wind_speed_kmph": current["windspeedKmph"]
    }
    return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False))]


async def handle_get_exchange_rate(arguments: dict) -> list[TextContent]:
    from_curr = arguments["from_currency"].upper()
    to_curr = arguments["to_currency"].upper()
    async with httpx.AsyncClient() as client:
        resp = await client.get(
            f"https://api.exchangerate-api.com/v4/latest/{from_curr}",
            timeout=10.0
        )
        resp.raise_for_status()
        data = resp.json()

    rate = data["rates"].get(to_curr)
    if rate is None:
        return [TextContent(type="text", text=f"不支持的货币代码: {to_curr}")]

    result = {
        "from": from_curr,
        "to": to_curr,
        "rate": rate,
        "description": f"1 {from_curr} = {rate} {to_curr}"
    }
    return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False))]


async def main():
    async with stdio_server() as (read_stream, write_stream):
        await server.run(read_stream, write_stream, server.create_initialization_options())


if __name__ == "__main__":
    import asyncio
    asyncio.run(main())

这段代码的关键点:

  • @server.list_tools() 装饰器注册工具列表,返回的 inputSchema 遵循 JSON Schema 规范,模型依赖这些信息来理解工具的能力和参数要求
  • @server.call_tool() 装饰器处理实际的工具调用,根据工具名分发到不同的处理函数
  • 返回值统一使用 TextContent 类型,这是 MCP 协议规定的标准格式

4.2 用 TypeScript 构建 MCP Server

如果你更熟悉 TypeScript 生态,下面是等价的实现:

// src/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({
  name: "weather-tools",
  version: "1.0.0",
});

// 注册天气查询工具
server.tool(
  "get_weather",
  "查询指定城市的当前天气信息,包括温度、湿度、天气状况",
  {
    city: z.string().describe("城市名称,例如:北京、上海、Tokyo"),
  },
  async ({ city }) => {
    const resp = await fetch(`https://wttr.in/${city}?format=j1`);
    const data = await resp.json();
    const current = data.current_condition[0];

    return {
      content: [
        {
          type: "text" as const,
          text: JSON.stringify({
            city,
            temperature_c: current.temp_C,
            humidity: current.humidity,
            description: current.weatherDesc[0].value,
            wind_speed_kmph: current.windspeedKmph,
          }),
        },
      ],
    };
  }
);

// 注册汇率查询工具
server.tool(
  "get_exchange_rate",
  "查询两种货币之间的实时汇率",
  {
    from_currency: z.string().describe("源货币代码,例如:USD、CNY"),
    to_currency: z.string().describe("目标货币代码,例如:USD、CNY"),
  },
  async ({ from_currency, to_currency }) => {
    const from = from_currency.toUpperCase();
    const to = to_currency.toUpperCase();
    const resp = await fetch(
      `https://api.exchangerate-api.com/v4/latest/${from}`
    );
    const data = await resp.json();
    const rate = data.rates[to];

    return {
      content: [
        {
          type: "text" as const,
          text: JSON.stringify({
            from,
            to,
            rate,
            description: `1 ${from} = ${rate} ${to}`,
          }),
        },
      ],
    };
  }
);

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

main().catch(console.error);

TypeScript SDK 的一个优势是可以直接使用 zod 来定义参数 Schema,写起来更简洁。SDK 内部会自动把 zod schema 转换为 JSON Schema 格式。

4.3 接入 Claude Desktop

Server 写好之后,接入 Claude Desktop 非常简单。编辑配置文件:

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

添加如下配置:

{
  "mcpServers": {
    "weather-tools": {
      "command": "python",
      "args": ["/absolute/path/to/weather_server.py"]
    }
  }
}

如果是 TypeScript 版本:

{
  "mcpServers": {
    "weather-tools": {
      "command": "npx",
      "args": ["tsx", "/absolute/path/to/src/index.ts"]
    }
  }
}

重启 Claude Desktop 后,你应该能在输入框旁边看到工具图标。试着问"北京今天天气怎么样",Claude 会自动调用你的 get_weather 工具并返回结果。

4.4 接入 Cursor

Cursor 从 0.45 版本开始支持 MCP。在项目根目录创建 .cursor/mcp.json

{
  "mcpServers": {
    "weather-tools": {
      "command": "python",
      "args": ["/absolute/path/to/weather_server.py"]
    }
  }
}

也可以在 Cursor 的 Settings 页面中直接配置全局 MCP Server。配置完成后,在 Cursor 的 Agent 模式下,模型就可以使用你注册的工具了。

五、常见坑与最佳实践

5.1 工具描述决定调用质量

这是新手最容易忽略的问题。模型完全依赖 descriptioninputSchema 来决定是否以及如何调用工具。描述写得不好,模型要么不调用,要么传错参数。

反面示例:

Tool(
    name="query",
    description="查询数据",
    inputSchema={"type": "object", "properties": {"q": {"type": "string"}}}
)

正面示例:

Tool(
    name="search_documents",
    description="在知识库中搜索文档。支持自然语言查询,返回最相关的前5条结果,每条包含标题、摘要和相关度评分。",
    inputSchema={
        "type": "object",
        "properties": {
            "query": {
                "type": "string",
                "description": "搜索关键词或自然语言描述,例如:'如何配置 Nginx 反向代理'"
            },
            "max_results": {
                "type": "integer",
                "description": "返回结果数量上限,默认为 5",
                "default": 5
            }
        },
        "required": ["query"]
    }
)

核心原则:把工具描述当作给模型看的 API 文档来写。名称要有语义,描述要说清楚功能边界,参数要给出示例值。

5.2 错误处理要规范

MCP 协议定义了 isError 字段来标记工具调用是否失败。很多人直接抛异常让 Server 崩溃,正确的做法是捕获异常并返回结构化的错误信息:

@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
    try:
        if name == "get_weather":
            return await handle_get_weather(arguments)
        raise ValueError(f"未知工具: {name}")
    except httpx.TimeoutException:
        return [TextContent(
            type="text",
            text="请求超时,请稍后重试"
        )]
    except httpx.HTTPStatusError as e:
        return [TextContent(
            type="text",
            text=f"API 请求失败,状态码: {e.response.status_code}"
        )]
    except Exception as e:
        return [TextContent(
            type="text",
            text=f"工具执行出错: {str(e)}"
        )]

这样即使工具调用失败,模型也能拿到有意义的错误信息,可能会尝试换一种方式解决问题,而不是直接报错。

5.3 调试技巧

开发 MCP Server 时,最痛苦的是调试。因为 Stdio 传输模式下,标准输出被协议占用了,你不能用 print 来调试。推荐以下方式:

方法一:使用 MCP Inspector

官方提供了一个可视化调试工具:

npx @modelcontextprotocol/inspector python weather_server.py

它会启动一个 Web UI,你可以手动调用工具、查看请求响应,非常直观。

方法二:使用 logging 模块写文件日志

import logging

logging.basicConfig(
    filename="/tmp/mcp-server.log",
    level=logging.DEBUG,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger("weather-server")

切记不要往 stdout 写任何非协议内容,否则会导致通信失败。

方法三:使用 stderr 输出调试信息

import sys
print("debug info here", file=sys.stderr)

stderr 不会干扰 Stdio 传输,Claude Desktop 等客户端通常会把 stderr 的内容记录到日志中。

5.4 性能考量

如果你的工具涉及耗时操作(数据库查询、外部 API 调用),需要注意以下几点:

  • 设置合理的超时时间:避免一个工具调用阻塞整个 Server。httpx 和 fetch 都支持 timeout 参数,建议设置为 10-30 秒。
  • 考虑连接复用:如果频繁调用外部 API,不要每次都创建新的 HTTP Client。可以在 Server 初始化时创建一个共享的 Client 实例。
  • 结果大小控制:模型的上下文窗口有限,返回过大的结果会浪费 token。尽量在 Server 端做好数据筛选和摘要。

5.5 安全注意事项

MCP Server 本质上是在执行代码,安全问题不容忽视:

  • 对用户输入做校验和清洗,防止注入攻击,特别是涉及数据库查询或命令执行的工具
  • 避免暴露敏感信息,API Key 等凭证通过环境变量传入,不要硬编码
  • 使用 env 字段在配置中安全地传递环境变量:
{
  "mcpServers": {
    "my-server": {
      "command": "python",
      "args": ["server.py"],
      "env": {
        "API_KEY": "your-api-key-here"
      }
    }
  }
}

六、总结

MCP 协议正在成为 AI 工具链的基础设施标准。通过本文,你应该已经了解了:

  1. MCP 的核心架构:Host-Client-Server 三层模型,基于 JSON-RPC 2.0 的通信协议,Tools/Resources/Prompts 三种原语
  2. 如何构建 MCP Server:Python 和 TypeScript 两种语言的完整实现,包括工具注册、参数定义、调用处理
  3. 如何接入主流客户端:Claude Desktop 和 Cursor 的配置方法
  4. 开发中的最佳实践:工具描述的写法、错误处理的规范、调试技巧、性能和安全考量

MCP 的生态还在快速发展中。目前已经有大量社区贡献的 MCP Server,覆盖了数据库(PostgreSQL、MySQL)、云服务(AWS、GCP)、开发工具(GitHub、GitLab)、生产力工具(Slack、Notion)等众多场景。在动手从零开始写之前,建议先到 MCP 官方的 Server 仓库和社区目录中看看有没有现成的方案。

如果你想进一步深入,推荐阅读 MCP 协议的官方规范文档(modelcontextprotocol.io),里面对 Resources、Prompts、Sampling 等高级特性有详细说明。

希望这篇文章能帮你快速上手 MCP 开发。如果在实践中遇到问题,欢迎在评论区交流。


本文基于 MCP 协议规范及 Python SDK / TypeScript SDK 的最新版本撰写,部分 API 可能随版本更新而变化,请以官方文档为准。