Files
zhuangang/topics/ai/mcp/01-protocol-server.md
T

123 lines
5.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 怎么选?