123 lines
5.4 KiB
Markdown
123 lines
5.4 KiB
Markdown
---
|
||
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 怎么选?
|