--- id: ai.mcp.protocol-server title: MCP 协议和服务开发:tools / resources / prompts、传输方式、接入客户端 type: lesson domain: ai stack: [mcp] tags: [MCP, Model Context Protocol, tools, resources, prompts, stdio, Streamable HTTP] level: intermediate prerequisites: [ai.function-calling.tool-loop] est_hours: 3 related: [ai.mcp.scenario.internal-tools-gateway, ai.ai-coding.index, ai.mcp.interview] week: [10, 11] order: 1 --- # MCP 协议和服务开发:tools / resources / prompts、传输方式、接入客户端 > 基于 MCP Python SDK 2.x 编写。**注意 1.x → 2.x 有不兼容变化**(例如 `FastMCP` 改名为 `MCPServer`,导入路径变为 `mcp.server.mcpserver`),网上很多教程还是 1.x 写法。以官方文档为准:https://modelcontextprotocol.io/ 、https://py.sdk.modelcontextprotocol.io/ ## 解释说明 **MCP(Model Context Protocol,模型上下文协议)** 是把“工具和数据”接入 AI 应用的开放协议。类比:USB 接口——一个 MCP 服务写好之后,Cursor、Claude Desktop、Claude Code、你自己的 Agent 都能接入,不用为每个应用写一遍集成。 | 角色 | 说明 | | --- | --- | | Host(宿主) | 用户使用的 AI 应用,例如 Cursor、你的客服系统 | | Client(客户端) | 宿主内部和某个服务保持连接的组件,一个服务一个客户端 | | Server(服务) | 提供能力的程序,例如“内部知识库服务”“数据库查询服务” | 服务端提供三类能力: | 能力 | 谁决定使用 | 例子 | | --- | --- | --- | | **tools(工具)** | 模型决定调用 | 搜索文档、创建工单 | | **resources(资源)** | 应用 / 用户选择后作为上下文 | 某篇文档、某张表的结构 | | **prompts(提示词模板)** | 用户选择 | “基于知识库回答”模板 | 客户端也可以向服务端提供能力:sampling(服务端请求客户端的模型生成内容)、roots(告诉服务端可访问的目录)、elicitation(服务端向用户索要补充信息)。 **传输方式**:`stdio`(宿主启动服务子进程,通过标准输入输出通信,适合本地工具)和 **Streamable HTTP**(远程服务,支持多客户端,需要鉴权)。早期的 HTTP + SSE 传输已被 Streamable HTTP 取代。底层消息格式是 JSON-RPC 2.0。 ### MCP 和 Function Calling 的关系 Function Calling 是**模型和应用之间**的接口(模型说要调什么);MCP 是**应用和工具提供方之间**的接口(应用从哪里发现工具、怎么调用)。Agent 从 MCP 服务拿到工具列表 → 转成 Function Calling 的工具定义交给模型 → 模型决定调用 → Agent 通过 MCP 调用服务。 ## 案例 公司内部有知识库和 IT 工单系统。写一个 MCP 服务:`search_docs`(只读)、`create_ticket`(有副作用,带幂等 ID)、文档资源、回答模板。开发人员在 Cursor 里接入后,可以直接问“VPN 连不上怎么办”;同一个服务也接入了团队的 LangGraph 客服 Agent。 ## 代码示例 服务端(完整版本 [examples/kb-server/kb_server.py](examples/kb-server/kb_server.py)): ```python from pydantic import BaseModel, Field from mcp.server.mcpserver import MCPServer mcp = MCPServer("internal-kb", instructions="公司内部知识库和 IT 工单工具。") @mcp.tool() def search_docs(query: str, top_k: int = 3) -> list[SearchHit]: """在内部知识库中按关键词搜索文档,返回文档 ID 和摘要。""" # 文档字符串就是工具描述 ... class TicketIn(BaseModel): title: str = Field(min_length=4, max_length=80) category: str = Field(pattern="^(it|hr|finance)$") request_id: str = Field(description="客户端生成的唯一 ID,用于幂等") @mcp.tool() def create_ticket(ticket: TicketIn) -> dict: """创建一张工单。会产生副作用:同一个 request_id 只创建一次。""" ... @mcp.resource("kb://docs/{doc_id}") def read_doc(doc_id: str) -> str: return DOCS.get(doc_id, "文档不存在") if __name__ == "__main__": mcp.run() # stdio;远程部署用 mcp.run("streamable-http") ``` 客户端测试(进程内连接,不需要启动服务,完整版本 [test_kb_server.py](examples/kb-server/test_kb_server.py)): ```python from mcp import Client async with Client(mcp) as c: tools = await c.list_tools() r = await c.call_tool("search_docs", {"query": "VPN 失败"}) print(r.structured_content) # {'result': [{'doc_id': 'vpn', 'snippet': ...}]} ``` 接入 Cursor:在项目的 `.cursor/mcp.json` 中配置(格式以 Cursor 文档为准): ```json { "mcpServers": { "internal-kb": { "command": "python", "args": ["topics/ai/mcp/examples/kb-server/kb_server.py"] } } } ``` ## 易错点 - 按 1.x 教程写 `from mcp.server.fastmcp import FastMCP`,在 2.x 中直接报错;先确认 SDK 版本。 - stdio 服务里用 `print` 输出调试信息:会污染标准输出(协议通道),日志要写到标准错误。 - 工具描述写得太简单,模型不知道什么时候该用。 - 远程(HTTP)服务没有鉴权就暴露在网络上。 - 有副作用的工具没有幂等,客户端重试时重复执行。 - 一个服务暴露了几十个工具,占用大量上下文;按领域拆分服务,或在客户端按需启用。 ## 面试常问 - MCP 是什么?解决什么问题?和 Function Calling 的关系? - tools、resources、prompts 的区别? - stdio 和 Streamable HTTP 怎么选?