AI Agent 的外部连接层:MCP 协议原理、机制设计与实战开发

16 阅读23分钟

当我们让 AI 助手"查一下知识库"或"在 Jira 上建个工单"时,背后需要一套标准方式让 AI 与外部工具对话——这就是 MCP(Model Context Protocol) 要解决的问题。它的定位类似于 AI 世界的"USB 接口":工具开发者按协议实现一次 Server,所有支持 MCP 的 AI 应用即可即插即用,告别"每换一个平台就重写集成代码"的困境。

本文从为什么需要 MCP 讲起,逐步拆解其三角色架构、三大能力原语、传输方式与连接生命周期,再通过一个 Python 实战案例带你从零搭建可运行的 MCP Server。读完你将理解:AI Agent 是如何通过 MCP 协议"长出手脚"、安全地操作外部世界的。


目录

  1. MCP 是什么,解决了什么问题
  2. MCP 整体架构:三个角色,一套标准
  3. 协议基石:JSON-RPC 2.0
  4. 协议核心:三大能力原语
  5. 传输层:消息怎么送达?
  6. 连接生命周期:从握手到断开
  7. 实战:用 Python 开发一个 MCP Server
  8. 安全最佳实践
  9. MCP vs Function Calling vs 直接 CLI 调用
  10. 常见问题 FAQ

1. MCP 是什么,解决了什么问题

1.1 LLM 能做什么,不能做什么

大语言模型(LLM)的本质是一个概率预测器。训练时,它从海量文本中学习语言规律和知识——给定一段输入,预测下一个最合理的 token,不断重复,就生成了连贯的输出。这个过程让它能够理解语义、推理逻辑、写代码、归纳总结,表现出很强的通用能力。

但这套机制也决定了它有几条硬性的边界。

知识是静态的。 训练完成之后,模型的参数就固定下来了,它的"知识"是训练截止日期之前的文本快照。它不知道今天的新闻,不了解你公司内部的数据,也不清楚你的代码库里昨天发生了什么。

没有记忆,没有状态。 每次对话都是全新开始,上下文只存在于当前的 context window 里,窗口之外的内容它完全不知道。它无法主动"记住"什么,也没有持久化存储。

只能输出文字,无法执行操作。 LLM 的输出是 token 序列,它能写出"删除这条记录"的 SQL,但无法真正连接数据库执行它;它能给出"发送邮件"的步骤,但没有办法自己点击发送按钮。

这三条限制叠在一起,意味着单独的 LLM 更像一个博学的顾问——能分析、能建议、能解释,但所有的实际动作还是要人来做。

Agent:让 LLM 从"说"变成"做"

为了突破这些限制,AI Agent 的概念应运而生。Agent 的核心思路是给 LLM 配上工具:让它不只是输出文字,而是能调用函数、读取数据、执行操作,然后根据结果继续推理,形成一个"感知 → 思考 → 行动"的循环。

flowchart TD
    A["用户输入"] --> B["LLM 推理"]
    B -->|"输出结构化指令:'调用 query_db,参数 { sql: SELECT ... }'"| C["Agent(如 Claude Desktop、LangChain、自研框架)"]

    C -->|"解析 LLM 的输出,识别出工具调用意图<br/>代为执行:连接数据库,跑 SQL,拿到结果"| D["执行结果"]

    D -->|"把执行结果以文本形式<br/>重新塞回 LLM 的 context"| E["LLM 读取结果,继续推理"]

    E --> F["输出最终回答"]

    style B fill:#fff3e0
    style C fill:#e8f5e9

⚠️ 关键理解:LLM 只是输出了文字,它自己什么都没执行。"调用工具"这个动作,从头到尾都是 Agent 在执行,LLM 只是在用语言表达意图。

LLM 在整个过程中始终只做一件事:根据输入预测输出。"调用工具"这个动作,从头到尾都是 Agent 在执行,LLM 只是在用语言表达意图。这也是为什么工具调用的安全控制要放在 Agent 层而不是模型层——模型管不了,Agent 才能拦截和审计。

但这里又冒出一个新问题:Agent 要调用的工具五花八门——数据库、第三方 API、本地文件系统、内部业务系统——每个工具的接口格式、认证方式、错误处理都不一样。没有统一标准的话,每接一个新工具就要写一套对接代码,换一个 Agent 框架又要重写一遍。这就是 MCP 出现的原因:它定义了 Agent 与外部工具之间通信的统一规范,让工具只需实现一次,任何支持 MCP 的 Agent 都能直接接入。

1.2 没有 MCP 之前:M×N 集成地狱

graph LR
    subgraph " "
        direction TB
        CP[ChatGPT Plugin] -->|自研集成| N1[Notion]
        CP -->|自研集成| G1[GitHub]
        CP -->|自研集成| J1[Jira]
        CL[Claude] -->|自研集成| N2["Notion ← 重复造轮子"]
        CL -->|自研集成| G2[GitHub]
        CU[Cursor] -->|自研集成| G3["GitHub ← 再造一遍"]
    end

    style CP fill:#ffcdd2
    style CL fill:#c8e6c9
    style CU fill:#bbdefb

M 个 AI 工具 × N 个外部服务 = M×N 套各自为政的集成代码。 每个 AI 厂商都自己定义调用格式、认证方式、错误处理——生态碎片化严重。

1.3 解法:一个标准协议,把 M×N 变成 M+N

MCP(Model Context Protocol,模型上下文协议) 就是为了解决这个问题而生的。

它的思路和 USB 完全一样——

类比没有 USB 的时代有 USB 之后
硬件每个外设一种专用接口(打印口、串口、PS/2……)统一 USB 口,即插即用
AI每个工具一套专用集成代码统一 MCP 协议,一次适配,处处可用
有了 MCP 之后:

  Claude  ─┐                ┌──▶ GitHub MCP Server
  ChatGPT ─┤── MCP 协议 ────┤──▶ Jira MCP Server
  Cursor  ─┘                └──▶ 知识库 MCP Server

  3 + 3 = 6 次适配(不再是 9 次)
  每新增一端,只需 +1

每个 AI 应用通过 MCP 协议直接连接所需的 Server——没有中心化的"总线"或网关,只有一套统一的通信标准。

3 + 3 = 6 次适配(不再是 9 次),每新增一端,只需 +1。

有了 MCP:

  • 工具开发者:按 MCP 协议写一个 Server,所有支持 MCP 的 AI 应用都能直接接入。
  • AI 应用开发者:实现一个 MCP Client,就能接入整个 MCP 生态的所有 Server。

1.4 MCP 到底是什么,不是什么

在开始深入之前,先校准一下认知:

MCP MCP 不是
一个开放的通信协议规范(类似 HTTP、USB)一个具体的软件产品或 SDK
基于 JSON-RPC 2.0 的消息格式标准某种编程语言或框架
AI 应用与外部工具之间的"通用翻译层"AI 模型本身的能力
Anthropic 发起,但完全开源的社区协议Anthropic 的私有技术

2. MCP 整体架构:三个角色,一套标准

理解了"为什么需要 MCP"之后,我们来看它的架构设计。MCP 世界里有三个核心角色:

2.1 Host、Client、Server

┌─────────────────────────────────────────┐
│              Host(宿主应用)              │
│  例:Claude Desktop / Cursor / 你的应用   │
│                                         │
│  ┌──────────┐  ┌──────────┐             │
│  │ Client A │  │ Client B │  ...        │
│  └────┬─────┘  └────┬─────┘             │
└───────┼──────────────┼──────────────────┘
        │              │
   MCP 协议         MCP 协议
        │              │
   ┌────▼─────┐   ┌────▼─────┐
   │ Server A │   │ Server B │
   │ (GitHub) │   │ (知识库)  │
   └──────────┘   └──────────┘

用一个更生活化的类比来理解:

角色是什么生活类比典型例子
Host用户直接使用的 AI 应用你的电脑Claude Desktop、Cursor、自研 AI 助手
ClientHost 内部负责对接协议的模块电脑上的 USB 控制器每个 Client 对应一个 Server 的连接
Server提供具体工具/数据的服务USB 外设(鼠标、U盘)GitHub Server、数据库 Server

几个关键点:

  • 一个 Host 可以包含多个 Client(就像电脑有多个 USB 口)。
  • 每个 Client 与一个 Server 保持 1:1 连接
  • Client 和 Server 之间的通信严格遵守 MCP 协议,双向通信——不是只有 Client 问、Server 答,Server 也可以主动发消息给 Client。

2.2 谁发起连接?

始终是 Client 主动连接 Server,不会反过来。这一点和 HTTP 的"客户端发起请求"是一样的。

但连接建立后,两边都可以主动发消息。比如:

  • Client → Server:调用一个工具
  • Server → Client:通知"我的工具列表变了,请重新获取"

2.3 为什么要分 Host 和 Client?

你可能会问:Host 和 Client 有什么区别,直接叫一个不行吗?

分开是因为职责不同

  • Host 负责:用户交互界面、AI 模型调度、安全策略(比如弹窗问用户"是否允许执行这个操作")。
  • Client 负责:协议层的脏活——消息序列化、连接管理、能力协商。

这种分离让 Host 开发者可以专注用户体验,不用操心协议细节。

2.4 完整调用链:从用户指令到工具执行

现在我们把三个角色串起来,看一个完整的调用过程。这是最容易让人迷惑的地方——MCP Server 到底是被谁调用的?

sequenceDiagram
    participant U as 用户
    participant H as Host/Agent
    participant L as LLM
    participant C as MCP Client
    participant S as MCP Server
    participant E as 外部系统

    U->>H: "帮我在 Jira 创建一个 Bug ticket"
    H->>L: 转发用户请求
    Note right of L: 模型决定调用哪个 Tool<br/>生成结构化调用请求
    L-->>H: 需要调用 create_ticket
    H->>C: 转发 Tool 调用
    C->>S: tools/call
    S->>E: 调用 Jira API
    E-->>S: 返回结果
    S-->>C: 返回结果
    C-->>H: 返回结果
    H->>L: 把结果注入 context
    L-->>H: 生成最终回答
    H-->>U: "已成功创建工单 #12345"

关键理解:

  • MCP Server 不是被用户直接调用的
  • MCP Server 也不是被 LLM 直接调用的
  • 真正的调用链是:LLM 做决策 → Host 转发 → Client 发送 → Server 执行

LLM 看到的只是 Tool 的 namedescriptioninputSchema,它根据用户意图决定调哪个 Tool、传什么参数。Server 只是执行层,不涉及任何 AI 逻辑。


3. 协议基石:JSON-RPC 2.0

在讲 MCP 的具体功能之前,必须先理解它的底层消息格式——JSON-RPC 2.0

3.1 什么是 JSON-RPC?

JSON-RPC 是一个轻量级的远程过程调用协议。说白了就是:用 JSON 格式来表达"我要调用什么方法、传什么参数、你给我什么结果"。

MCP 的所有通信都是 JSON-RPC 2.0 消息。一共只有三种消息类型:

3.2 三种消息类型

① Request(请求)——"我要你做件事,请给我回复"

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "search_docs",
    "arguments": { "query": "退款政策" }
  }
}

关键字段:

  • id:唯一标识,用来匹配响应(你问的第几个问题,回答对应第几个)。
  • method:要调用的方法名。
  • params:参数。

② Response(响应)——"这是你要的结果"

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      { "type": "text", "text": "退款政策:30天内可全额退款……" }
    ]
  }
}

如果出错了,result 换成 error

{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32602,
    "message": "参数 query 不能为空"
  }
}

③ Notification(通知)——"告诉你一声,不用回复"

{
  "jsonrpc": "2.0",
  "method": "notifications/tools/list_changed"
}

注意:通知没有 id 字段,这就是区分请求和通知的方式。

3.3 批量请求(Batching)

MCP 支持将多个请求打包成一个数组一次性发送,减少通信开销:

[
  { "jsonrpc": "2.0", "id": 1, "method": "tools/list" },
  { "jsonrpc": "2.0", "id": 2, "method": "resources/list" }
]

Server 会返回一个数组,包含每个请求的响应。

⚠️ 注意:协议规定所有实现必须能接收 batch 消息,但发送 batch 是可选的。另外,initialize 请求不得放在 batch 中发送——因为初始化完成前不能发送其他请求。

3.4 为什么选 JSON-RPC 而不是 REST?

对比项REST APIJSON-RPC 2.0
消息格式URL + HTTP 方法 + Body统一 JSON 格式
双向通信天然不支持(Server 无法主动推送)天然支持(双方都能发消息)
适合场景CRUD 资源操作方法调用、工具执行
消息匹配靠 HTTP 连接本身id 字段精确匹配

MCP 需要双向通信(Server 要能主动通知 Client),需要灵活的方法调用(不是简单的增删改查),所以 JSON-RPC 是更合适的选择。


4. 协议核心:三大能力原语

MCP Server 可以向 Client 暴露三种类型的能力,协议里叫 Primitives(原语)

graph LR
    subgraph 原语["MCP Server 能提供什么?"]
        T["Tools<br/>工具"]
        R["Resources<br/>资源"]
        P["Prompts<br/>提示词模板"]
    end

    T -.- T1["AI 调用 → '帮我执行'"]
    R -.- R1["AI 读取 → '帮我看'"]
    P -.- P1["用户选择 → '帮我写'"]

    style T fill:#ffcdd2
    style R fill:#c8e6c9
    style P fill:#bbdefb

4.1 Tools(工具)—— AI 可以"动手做"的事情

一句话理解: Tool 就是 AI 可以调用的函数。

例子: 搜索知识库、发送邮件、创建 Jira 工单、执行数据库查询。

关键特征:

  • AI 模型主动决定是否调用(模型判断用户意图后,选择合适的工具)。
  • 可以有副作用(会改变外部状态,比如发邮件、写数据库)。
  • 需要人工确认(Host 应该在执行前弹窗问用户"是否允许")。

Tool 的定义长什么样:

{
  "name": "search_docs",
  "description": "搜索内部知识库文档,返回最相关的结果",
  "inputSchema": {
    "type": "object",
    "properties": {
      "query": { "type": "string", "description": "搜索关键词" },
      "top_k": { "type": "integer", "description": "返回数量", "default": 5 }
    },
    "required": ["query"]
  },
  "annotations": {
    "title": "知识库搜索",
    "readOnlyHint": true,
    "openWorldHint": false
  }
}

这里有两个重要部分:

  • inputSchema:用 JSON Schema 定义参数格式,AI 模型会根据这个生成正确的参数。
  • annotations(工具标注):这是最新版协议的重要特性,用于告诉 Host 这个工具的"性质":
标注字段含义默认值例子
readOnlyHint是否只读(不会修改外部状态)false搜索=true,删除=false
destructiveHint是否有破坏性true删除文件=true
idempotentHint是否幂等(重复调用结果一样)false查询=true,发邮件=false
openWorldHint是否会访问外部网络true调用第三方 API=true

💡 这些标注是"提示(hint)",不是强制约束。Host 可以根据这些标注调整安全策略,比如:destructiveHint=true 的工具执行前一定要弹窗确认。

注意默认值的设计哲学是安全优先(fail-safe):默认认为工具可能修改状态(readOnlyHint=false)、有破坏性(destructiveHint=true)、不幂等(idempotentHint=false)、会访问外网(openWorldHint=true)。这样即使开发者忘记设置标注,Host 也会采取最严格的安全策略。

Tool 还支持结构化输出(Structured Output):(⚡ 2025-06-18 版新增)

如果你希望工具返回的不只是文本,还能返回机器可解析的 JSON 数据,可以定义 outputSchema

{
  "name": "get_user_info",
  "description": "查询用户信息",
  "inputSchema": { ... },
  "outputSchema": {
    "type": "object",
    "properties": {
      "name": { "type": "string" },
      "email": { "type": "string" },
      "plan": { "type": "string", "enum": ["free", "pro", "enterprise"] }
    }
  }
}

定义了 outputSchema 后,工具返回的响应中会同时包含 content(给 AI 模型看的文本)和 structuredContent(结构化 JSON 数据),调用方可以直接用结构化数据做后续处理。

4.2 Resources(资源)—— AI 可以"阅读"的数据

一句话理解: Resource 就是 AI 可以读取的数据源,类似 REST 里的 GET 接口。

例子: 一份文档的内容、一个数据库表的 schema、一张配置文件。

关键特征:

  • 用户或应用决定是否读取(不是 AI 自己决定的)。
  • 只读,不会产生副作用。
  • 通过 URI 标识,比如 file:///docs/refund-policy.mddb://users/schema
{
  "uri": "docs://knowledge-base/refund-policy",
  "name": "退款政策文档",
  "mimeType": "text/markdown",
  "description": "公司退款政策的完整文档"
}

Resource 支持两种发现方式:

  • 直接列表resources/list):Server 列出所有可用资源,Client 选择要读取的。
  • 模板resources/templates/list):Server 提供 URI 模板,Client 动态填参数。比如 db://tables/{table_name}/schema,Client 填入具体表名来获取不同资源。

4.3 Prompts(提示词模板)—— 预制的交互剧本

一句话理解: Prompt 是 Server 预定义好的提示词模板,用户可以选择使用。

例子: "帮我做 Code Review"的提示词模板、"帮我写 SQL"的提示词模板。

关键特征:

  • 用户主动选择触发(比如在聊天框里选一个模板)。
  • 可以带参数(模板里的变量由用户填入)。
  • 返回的是完整的 messages 数组,可以直接塞给 AI 模型。
{
  "name": "code_review",
  "description": "对指定代码进行 Code Review",
  "arguments": [
    {
      "name": "code",
      "description": "要 review 的代码",
      "required": true
    },
    {
      "name": "language",
      "description": "编程语言",
      "required": false
    }
  ]
}

4.4 三种原语对比总结

Tools(工具)Resources(资源)Prompts(模板)
类比函数/API文件/数据预制剧本
谁决定使用AI 模型用户/应用用户
是否有副作用可能有没有(只读)没有
是否需要确认推荐确认通常不需要不需要
发现方式tools/listresources/listprompts/list
调用方式tools/callresources/readprompts/get

5. 传输层:消息怎么送达?

协议定义好了"说什么",传输层解决的是"怎么送达"。MCP 支持两种传输方式:

5.1 stdio(标准输入/输出)

原理: Host 直接启动 Server 进程,通过进程的 stdin/stdout 传递 JSON-RPC 消息。

graph LR
    H["Host 进程"] -->|"写入 stdin"| S["Server 进程"]
    S -->|"读取 stdout"| H

特点:

  • 最简单,不需要网络配置。
  • Server 作为子进程运行,生命周期由 Host 管理。
  • 只能本机通信——Host 和 Server 必须在同一台机器上。

适合场景: 本地开发工具(如 Cursor 调用本地的 Git Server)、桌面应用。

5.2 Streamable HTTP

原理: Server 作为 HTTP 服务运行,Client 通过 HTTP 请求与之通信。Server 可以用 SSE(Server-Sent Events)流式推送响应。

graph LR
    C["Client"] -->|"HTTP POST(发送请求)"| S["Server(/mcp 端点)"]
    C -->|"HTTP GET(打开 SSE 流)"| S
    S -.->|"SSE 流(推送通知/响应)"| C
    C -->|"HTTP DELETE(终止会话)"| S

特点:

  • 单一端点:Server 只需暴露一个 HTTP 端点(如 /mcp),同时支持 POST、GET、DELETE。
  • 支持远程访问——Server 可以部署在云端。
  • 支持会话管理(通过 Mcp-Session-Id 请求头)。
  • 支持流式响应(Server 可以边处理边推送结果)。
  • Client 可以通过 GET 请求打开独立的 SSE 流,接收 Server 主动推送的通知——这是 MCP 双向通信的关键。
  • 可以无状态部署,天然支持水平扩展(但协议也允许通过 Session ID 维护有状态会话)。

适合场景: 生产环境部署、多用户共享的 Server、需要远程访问的场景。

⚠️ 旧版协议(2024-11-05 版)使用的是 HTTP+SSE 双端点方案(/sse + /messages 两个端点),已在 2025-03-26 版中被 Streamable HTTP 单端点方案取代。如果你看到旧教程提到双端点,请忽略。

5.3 如何选择?

flowchart TD
    Q{"你的 Server 需要远程访问吗?"}
    Q -->|"不需要,只在本机"| A["→ stdio"]
    Q -->|"需要远程 / 多用户"| B["→ Streamable HTTP"]
stdioStreamable HTTP
网络要求无(本机进程)需要 HTTP 网络
部署复杂度中等
远程访问
水平扩展
典型用法claude desktop 本地配置云端 MCP 服务

6. 连接生命周期:从握手到断开

一个 MCP 连接从建立到关闭,经历三个阶段:

  ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
  │  Initialize │────▶│  Operation  │────▶│  Shutdown   │
  │  初始化握手  │     │  正常工作    │     │  优雅关闭    │
  └─────────────┘     └─────────────┘     └─────────────┘

6.1 阶段一:初始化(握手)

Client 和 Server 需要先"自我介绍",交换各自支持的能力:

sequenceDiagram
    participant C as Client
    participant S as Server

    C->>S: initialize 请求(我是谁,我支持什么)
    S-->>C: initialize 响应(我是谁,我支持什么)
    C->>S: initialized 通知(收到,握手完成!)

initialize 请求示例:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "roots": { "listChanged": true }
    },
    "clientInfo": {
      "name": "MyAIApp",
      "version": "1.0.0"
    }
  }
}

Server 响应示例:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "tools": { "listChanged": true },
      "resources": { "subscribe": true }
    },
    "serverInfo": {
      "name": "knowledge-base-server",
      "version": "2.0.0"
    }
  }
}

重点:能力协商。 双方通过 capabilities 字段告诉对方自己支持什么。比如 Server 说"我支持 tools",那 Client 后续才能调用 tools/listtools/call。如果 Server 没声明某个能力,Client 就不该尝试使用它。

6.2 阶段二:正常工作

握手完成后,双方就可以自由通信了:

Client → Server(常见操作):

  • tools/list — 列出所有可用工具
  • tools/call — 调用某个工具
  • resources/list — 列出所有资源
  • resources/read — 读取某个资源
  • prompts/list — 列出提示词模板
  • prompts/get — 获取某个模板的内容
  • completion/complete — 请求自动补全
  • ping — 心跳检测

Server → Client(主动通知和请求):

  • notifications/tools/list_changed — 通知工具列表变了
  • notifications/resources/updated — 通知某个资源更新了
  • elicitation/create — 主动向用户收集信息(比如弹窗让用户选择一个选项)(⚡ 2025-06-18 版新增)

💡 Elicitation(主动询问) 是 2025-06-18 版本新增的特性,设计仍在演进中:Server 在处理请求的过程中,可以反过来向用户提问。比如一个部署工具在执行时发现有两个环境可选,它可以通过 Elicitation 让用户选择"部署到 staging 还是 production"。使用时需要 Client 在初始化阶段声明 elicitation 能力。

6.3 阶段三:优雅关闭

MCP 协议没有定义专门的 closeshutdown JSON-RPC 方法——关闭是通过底层传输机制来完成的:

stdio 模式:

  1. Client 关闭 Server 子进程的 stdin;
  2. 等待 Server 自行退出;
  3. 如果超时未退出,发送 SIGTERM
  4. 仍未退出则发送 SIGKILL

Streamable HTTP 模式:

  1. Client 向 MCP endpoint 发送 HTTP DELETE 请求(携带 Mcp-Session-Id 头);
  2. Server 清理会话资源,关闭关联的 SSE 流。

7. 实战:用 Python 开发一个 MCP Server

7.1 需求

我们要做一个知识库搜索 MCP Server,提供:

  • 一个 Tool:search_docs — 搜索知识库
  • 一个 Resource:docs://knowledge-base/summary — 知识库概要

7.2 环境准备

# 需要 Python 3.10+
pip install "mcp[cli]" httpx
# [cli] extra 提供 mcp dev / mcp run / mcp install 等 CLI 命令
# 如果不需要 CLI 工具,也可以只装:pip install mcp httpx

7.3 完整代码

"""knowledge_base_server.py - 知识库搜索 MCP Server"""

import os
import httpx
from mcp.server.fastmcp import FastMCP
from mcp.types import ToolAnnotations

# ── 初始化 Server ──────────────────────────────
mcp = FastMCP(
    name="knowledge-base-server",
    instructions="内部知识库搜索服务",
)

# ── 配置 ──────────────────────────────────────
KB_API_URL = os.getenv("KB_API_URL", "https://kb.example.com/api")
KB_TOKEN = os.getenv("KB_TOKEN", "")  # 必须从环境变量获取


# ── Tool:搜索知识库 ──────────────────────────
@mcp.tool(
    title="知识库搜索",
    annotations=ToolAnnotations(
        readOnlyHint=True,          # 只读操作,不会修改数据
        destructiveHint=False,
        openWorldHint=True,         # 会访问外部 API
    ),
)
async def search_docs(query: str, top_k: int = 5) -> str:
    """
    搜索内部知识库,返回最相关的文档片段。

    Args:
        query: 搜索关键词
        top_k: 返回结果数量,默认 5
    """
    try:
        async with httpx.AsyncClient(timeout=30) as client:
            resp = await client.post(
                f"{KB_API_URL}/search",
                json={"query": query, "top_k": top_k},
                headers={"Authorization": f"Bearer {KB_TOKEN}"},
            )
            resp.raise_for_status()
            results = resp.json().get("results", [])
    except httpx.HTTPError as e:
        return f"搜索服务暂时不可用:{e}"

    if not results:
        return "未找到相关文档。"

    # 拼成可读文本返回给 AI
    lines = []
    for i, doc in enumerate(results, 1):
        title = doc.get("title", "无标题")
        snippet = doc.get("snippet", "")
        score = doc.get("score", 0)
        lines.append(f"{i}. **{title}**(相关度 {score:.2f})\n   {snippet}")

    return "\n\n".join(lines)


# ── Resource:知识库概要 ──────────────────────
@mcp.resource("docs://knowledge-base/summary")
async def kb_summary() -> str:
    """知识库概要信息:文档数量、最后更新时间等"""
    async with httpx.AsyncClient(timeout=10) as client:
        resp = await client.get(
            f"{KB_API_URL}/stats",
            headers={"Authorization": f"Bearer {KB_TOKEN}"},
        )
        resp.raise_for_status()
        stats = resp.json()

    return (
        f"知识库概要:\n"
        f"- 文档总数:{stats.get('total_docs', '未知')}\n"
        f"- 最后更新:{stats.get('last_updated', '未知')}\n"
        f"- 覆盖主题:{', '.join(stats.get('topics', []))}"
    )


# ── 启动入口 ──────────────────────────────────
if __name__ == "__main__":
    mcp.run(transport="stdio")  # 本地开发用 stdio

7.4 运行与调试

方式一:MCP Inspector(推荐调试时使用)

npx @modelcontextprotocol/inspector python knowledge_base_server.py

浏览器打开后可以:

  • 查看 Server 声明的 Tools / Resources / Prompts
  • 手动调用 Tool,查看输入输出
  • 检查 JSON-RPC 消息流

方式二:接入 Claude Desktop

编辑 Claude Desktop 配置文件:

  • macOS:~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows:%APPDATA%\Claude\claude_desktop_config.json
{
  "mcpServers": {
    "knowledge-base": {
      "command": "python",
      "args": ["/path/to/knowledge_base_server.py"],
      "env": {
        "KB_API_URL": "https://kb.your-company.com/api",
        "KB_TOKEN": "your-secret-token"
      }
    }
  }
}

重启 Claude Desktop,在聊天框中输入"帮我查一下退款政策",AI 就会自动调用 search_docs 工具。

7.5 如果需要远程部署(Streamable HTTP)

只需将初始化和启动方式改为:

mcp = FastMCP(
    name="knowledge-base-server",
    instructions="内部知识库搜索服务",
    host="0.0.0.0",
    port=8000,
)

if __name__ == "__main__":
    mcp.run(transport="streamable-http")

Client 通过 http://your-server:8000/mcp 连接即可。


8. 安全最佳实践

MCP Server 本质上是在给 AI 模型"开权限",安全至关重要。

8.1 核心原则

flowchart TD
    A["最小权限原则"]
    A --> B["输入验证"]
    A --> C["人工确认循环"]
    A --> D["密钥安全"]

8.2 具体措施

领域做法原因
输入验证对所有 Tool 参数做严格校验,永远不要把参数直接拼接到 SQL/命令中AI 生成的参数可能被 Prompt Injection 操控
人工确认有副作用的操作(写入、删除、发送),必须让 Host 弹窗确认AI 可能误判用户意图
最小权限Server 只请求必要的权限,数据库账号只给只读权限限制爆炸半径
密钥管理Token/密码通过环境变量传入,不硬编码在代码里防止泄露到代码仓库
Tool Annotations正确设置 readOnlyHintdestructiveHint 等标注让 Host 能智能决定安全策略
传输安全远程部署时使用 HTTPS + OAuth 2.1防止中间人攻击和未授权访问
DNS Rebinding 防护Streamable HTTP Server 必须验证 Origin 头;本地运行时绑定 127.0.0.1 而非 0.0.0.0防止恶意网页通过 DNS 重绑定攻击本地 Server
日志审计记录所有 Tool 调用的入参和结果事后追溯和异常检测

8.3 OAuth 2.1 授权

对于远程部署的 Streamable HTTP Server,MCP 协议内建了 OAuth 2.1 授权框架:

  1. Client 首次连接时,Server 返回 401 并指明授权端点;
  2. Client 引导用户完成 OAuth 登录(授权码流程 + PKCE,PKCE 是所有客户端的强制要求);
  3. 获取 Access Token 后,后续请求通过 Authorization: Bearer <token> 携带;
  4. Token 过期后自动刷新。

这意味着你不需要自己实现鉴权中间件——协议已经规定好了标准流程。


9. MCP vs Function Calling vs 直接 CLI 调用

这三者经常被混淆,我们来梳理本质区别。

9.1 三者定位

graph TB
    subgraph AI应用["AI 应用"]
        U["用户说:'帮我查一下退款政策'"]
        U --> M["AI 模型推理<br/>'需要搜索知识库'"]
    end

    M --> FC["Function Calling<br/>(模型层接口)"]
    M --> MCP["MCP Tool<br/>(协议层标准)"]
    M --> CLI["CLI 调用<br/>(操作系统层)"]

    FC --> API["模型 API<br/>供应商绑定"]
    MCP --> Server["MCP Server<br/>标准化通信"]
    CLI --> Shell["shell 命令<br/>无标准化"]

    style MCP fill:#c8e6c9

9.2 核心区别对比

对比维度Function CallingMCP直接 CLI 调用
是什么模型 API 的参数格式约定独立的通信协议直接执行系统命令
标准化每家模型供应商各有格式统一开放协议无标准
谁定义工具开发者在 API 调用时传入MCP Server 自行声明,Client 动态发现开发者硬编码
工具发现❌ 无法动态发现tools/list 自动发现❌ 无
供应商绑定绑定特定模型供应商不绑定任何模型不绑定
双向通信
安全机制开发者自行实现协议内建(Annotations、OAuth、确认流程)几乎没有
适合场景快速原型、简单集成标准化生态、多应用共享脚本、自动化

9.3 它们不是互斥的

一个典型的 AI 应用中,三者经常协同工作

  1. 用户说"帮我搜一下退款政策";
  2. AI 模型通过 Function Calling 的格式,告诉Agent应用"我想调用 search_docs";
  3. Agent应用通过 MCP 协议,将这个调用转发给知识库 MCP Server;
  4. MCP Server 内部可能通过 CLI 调用 grepcurl 来执行实际操作。
flowchart LR
    A["Function Calling<br/>(模型怎么表达意图)"] --> B["MCP 协议<br/>(应用怎么传递请求)"]
    B --> C["CLI 调用<br/>(Server 怎么执行)"]

10. 常见问题 FAQ

Q1:MCP 只能和 Claude 一起用吗?

不是。 MCP 是开放协议,任何 AI 应用都可以实现。目前已支持的 Host 包括 Claude Desktop、Cursor、Windsurf、Cline 等,OpenAI 也已宣布支持。

Q2:MCP Server 必须用 Python 写吗?

不是。 协议不限制语言。官方提供了 Python 和 TypeScript 的 SDK,社区还有 Go、Rust、Java、C# 等实现。

Q3:MCP Server 是有状态的还是无状态的?

都可以。 协议通过 Mcp-Session-Id 支持有状态会话,但也完全允许无状态设计。对于 Streamable HTTP 模式,推荐无状态设计以便水平扩展;如果确实需要会话状态(比如维护一个数据库连接),可以通过 Session ID 实现。

Q4:Tool 和 Resource 有什么区别?什么时候用哪个?

判断标准用 Tool用 Resource
谁决定使用?AI 模型自主决定用户/应用主动选择
有没有副作用?可能有没有(只读)
需要传参数吗?通常需要通常不需要(URI 定位)

经验法则:如果这个操作是"AI 根据对话判断要不要做的",用 Tool;如果是"提前把某些数据塞给 AI 当背景知识",用 Resource。

Q5:怎么调试 MCP Server?

推荐使用 MCP Inspector

npx @modelcontextprotocol/inspector python your_server.py

它会启动一个网页界面,你可以直观地看到所有工具、资源、模板,手动调用并查看请求/响应。

Q6:生产环境该怎么部署?

  • 小规模/内部使用: stdio 模式,Server 作为本地进程运行。
  • 多用户/远程访问: Streamable HTTP 模式,部署在云端,配合 OAuth 2.1 鉴权、HTTPS 加密、负载均衡。

Q7:MCP Server 在哪里运行?是在云端吗?

看你选择。 stdio 模式下 Server 是 Host 启动的本地子进程,运行在你自己的机器上;Streamable HTTP 模式下可以部署在任何能提供 HTTP 服务的地方——本地、云端、Docker 容器都行。协议不限制部署位置,只规定通信方式。


附:推荐阅读