阶段4(上):LangGraph、Function Calling、MCP(SDK 2.x)、Agent 评测与运维、Agent 设计专题;run_examples 设置 RUN_EXAMPLES=1

This commit is contained in:
Shikui Wang committed 2026-10-23 16:06:54 +08:00
1 parent 6650f7eb6d
commit caaec830b3
38 files changed
+3515 -64

No files matched your search

+122
View File
@@ -0,0 +1,122 @@
---
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 怎么选?
@@ -0,0 +1,67 @@
# requires: mcp, pydantic
"""一个内部知识库 / 工单的 MCP 服务:tools(搜索、创建工单)、resources(文档)、prompts(模板)。
基于 MCP Python SDK 2.x 编写(1.x 中的 FastMCP 在 2.x 改名为 MCPServer),以官方文档为准:
https://py.sdk.modelcontextprotocol.io/
运行方式:
python kb_server.py # stdio 方式启动,供 Cursor / Claude Desktop 等客户端接入
python test_kb_server.py # 进程内客户端测试(不需要启动服务)
"""
from __future__ import annotations
import os
from pydantic import BaseModel, Field
from mcp.server.mcpserver import MCPServer
DOCS = {
"vpn": "VPN 连接失败:先确认账号未过期,再重新安装客户端;仍失败请提交 IT 工单。",
"expense": "差旅报销:一线城市住宿标准每晚 600 元,需在出差结束后 30 天内提交。",
}
TICKETS: dict[str, dict] = {}
mcp = MCPServer("internal-kb", instructions="公司内部知识库和 IT 工单工具。只读工具可以直接调用,创建工单前先确认问题描述。")
class SearchHit(BaseModel):
doc_id: str
snippet: str
@mcp.tool()
def search_docs(query: str, top_k: int = 3) -> list[SearchHit]:
"""在内部知识库中按关键词搜索文档,返回文档 ID 和摘要。用于回答公司制度和 IT 问题。"""
hits = [SearchHit(doc_id=k, snippet=v[:40]) for k, v in DOCS.items()
if any(w and w in v for w in query.split())]
return hits[:top_k]
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 只创建一次。"""
if ticket.request_id not in TICKETS:
TICKETS[ticket.request_id] = {"id": f"T{len(TICKETS) + 1:04d}", **ticket.model_dump()}
return TICKETS[ticket.request_id]
@mcp.resource("kb://docs/{doc_id}")
def read_doc(doc_id: str) -> str:
"""读取一篇知识库文档的全文。"""
return DOCS.get(doc_id, "文档不存在")
@mcp.prompt()
def answer_with_citation(question: str) -> str:
"""基于知识库回答问题的提示词模板。"""
return f"请先调用 search_docs 检索,再根据检索结果回答,并标注 doc_id。问题:{question}"
if __name__ == "__main__" and os.environ.get("RUN_EXAMPLES") != "1":
mcp.run() # 默认 stdio;也可以 mcp.run("streamable-http")
@@ -0,0 +1,50 @@
# requires: mcp, pydantic, pytest, anyio
import json
import anyio
import pytest
from mcp import Client
from kb_server import TICKETS, mcp
@pytest.fixture
def anyio_backend():
return "asyncio"
async def _session():
async with Client(mcp) as c: # 测试时直接传入服务对象,进程内通信
tools = {t.name: t for t in (await c.list_tools()).tools}
assert set(tools) == {"search_docs", "create_ticket"}
assert "request_id" in json.dumps(tools["create_ticket"].input_schema)
r = await c.call_tool("search_docs", {"query": "VPN 失败"})
assert not r.is_error and "vpn" in json.dumps(r.structured_content, ensure_ascii=False)
args = {"ticket": {"title": "VPN 无法连接", "category": "it", "request_id": "req-1"}}
first = await c.call_tool("create_ticket", args)
second = await c.call_tool("create_ticket", args)
assert first.structured_content == second.structured_content and len(TICKETS) == 1
bad = await c.call_tool("create_ticket", {"ticket": {"title": "x", "category": "other", "request_id": "r"}})
assert bad.is_error # 参数校验失败以错误结果返回给客户端
res = await c.read_resource("kb://docs/expense")
assert "600" in res.contents[0].text
p = await c.get_prompt("answer_with_citation", {"question": "住宿标准"})
assert "search_docs" in p.messages[0].content.text
return r
@pytest.mark.anyio
async def test_kb_server():
await _session()
if __name__ == "__main__":
r = anyio.run(_session)
print("search_docs 结构化结果:", r.structured_content)
print("工单:", TICKETS)
+30 -3
View File
@@ -4,12 +4,39 @@ title: MCP 模型上下文协议
type: index
domain: ai
stack: [mcp]
tags: []
tags: [MCP, 工具, 集成]
week: [10, 11]
---
# MCP 模型上下文协议
协议概念、tools / resources / prompts、服务端与客户端。
把工具和数据接入 AI 应用的开放协议。BOSS直聘 2026-10-23 的调研中约 9 个岗位提到 MCP / Function Calling。目标:能写一个 MCP 服务、写测试、接入 Cursor 和自己的 Agent,并讲清楚安全设计。
> 本专题建设中:课程、企业场景、面试题和示例代码会陆续补充。
## 学习路线
| 顺序 | 课程 | 级别 | 预计 |
| --- | --- | --- | --- |
| 1 | [MCP 协议和服务开发:tools / resources / prompts、传输方式、接入客户端](01-protocol-server.md) | 进阶 | 3 小时 |
## 企业场景
- [企业内部 MCP 服务:统一接入、鉴权、审计和版本管理](scenarios/internal-tools-gateway.md)
## 面试题
- [MCP 面试题](interview.md)
## 示例代码
- [examples/kb-server](examples/kb-server/):知识库 + 工单 MCP 服务(MCP Python SDK 2.x:tools、resources、prompts、pydantic 参数校验、幂等),带进程内客户端测试
## 在 16 周计划中
- [第 10 周:工具调用循环和 MCP](../../../plan/week10-agent-m1-tools-mcp.md)、[第 11 周:接入 LangGraph](../../../plan/week11-agent-m2-langgraph.md)
## 官方资料
协议和 SDK 更新很快,以官方文档为准:
- 协议:https://modelcontextprotocol.io/
- Python SDK:https://py.sdk.modelcontextprotocol.io/ (1.x → 2.x 迁移指南:https://py.sdk.modelcontextprotocol.io/v2/migration/)
+61
View File
@@ -0,0 +1,61 @@
---
id: ai.mcp.interview
title: MCP 面试题
type: interview
domain: ai
stack: [mcp]
tags: [MCP, 面试]
level: intermediate
related: [ai.mcp.protocol-server]
week: [10, 11, 16]
---
# MCP 面试题
## 基础
### 【基础】MCP 是什么?解决什么问题?
**参考答案**:模型上下文协议,一个让 AI 应用以统一方式接入外部工具和数据的开放协议。解决 M 个 AI 应用 × N 个工具的重复集成问题:工具方实现一次 MCP 服务,所有支持 MCP 的应用都能使用。
**追问**:MCP 和 OpenAPI 有什么区别?→ OpenAPI 描述 HTTP 接口;MCP 面向 AI 应用,除了工具还定义了资源、提示词模板、双向能力(sampling、elicitation)和会话,工具描述直接供模型理解。
### 【基础】tools、resources、prompts 有什么区别?
**参考答案**:tools 由模型决定调用,可以有副作用;resources 是只读的上下文数据,由应用或用户选择加入;prompts 是预定义的提示词模板,由用户选择使用。
**追问**:查询数据库表结构应该做成 tool 还是 resource?→ 如果希望用户 / 应用主动选择放入上下文,用 resource;如果希望模型按需获取,用 tool。很多客户端对 tools 支持最好,实践中常做成 tool。
### 【基础】MCP 有哪些传输方式?
**参考答案**:stdio(本地子进程,标准输入输出)和 Streamable HTTP(远程服务)。消息格式都是 JSON-RPC 2.0。早期的 HTTP + SSE 传输已被 Streamable HTTP 取代。
**追问**:stdio 服务为什么不能用 print 打日志?→ 标准输出是协议通道,打印的内容会破坏消息格式;日志写到标准错误。
## 进阶
### 【进阶】MCP 和 Function Calling 是什么关系?
**参考答案**:层次不同。Function Calling 是模型与应用之间的接口;MCP 是应用与工具服务之间的接口。应用从 MCP 服务获取工具列表,转成模型的工具定义,模型发起调用后,应用再通过 MCP 执行。
**追问**:LangGraph 怎么使用 MCP 工具?→ 使用 MCP 适配器(如 `langchain-mcp-adapters`,以官方文档为准)把 MCP 工具转成 LangChain 工具,交给 Agent 使用。
### 【进阶】远程 MCP 服务怎么做安全?
**参考答案**:HTTPS;基于 OAuth 的授权(规范为 HTTP 传输定义了授权流程);以用户身份访问业务系统;工具级权限和确认;限流、审计;校验 Origin 等防止 DNS 重绑定类攻击(以规范的安全建议为准)。
**追问**:安装第三方 MCP 服务有什么风险?→ 本地服务能以你的权限执行代码、读取文件;工具描述也可能包含诱导模型的指令(工具投毒)。只使用可信来源并审查代码和权限。
## 场景题
### 【场景题】公司有 5 个团队都要接入工单系统,怎么设计?
**参考答案**:由工单系统团队提供一个远程 MCP 服务,统一鉴权(SSO 令牌、用户身份透传)、审计、限流;工具规范命名和描述,写操作带幂等 ID;各团队的 Agent 和开发工具都通过它接入。详见[企业内部 MCP 服务](scenarios/internal-tools-gateway.md)。
**追问**:工单系统接口改版怎么办?→ 只需要改 MCP 服务内部实现,工具接口保持兼容;需要改工具时做版本标记和废弃周期。
### 【场景题】你在项目中写过 MCP 服务吗?讲一下。
**参考答案**(按自己的项目讲):服务提供了哪些工具 / 资源,为什么这样划分;参数怎么校验;有副作用的工具怎么保证幂等;怎么测试(进程内客户端测试);接入了哪些客户端(Cursor、自己的 LangGraph Agent);遇到的问题(例如 SDK 版本升级的不兼容、stdio 日志污染)。
**追问**:怎么验证模型能正确使用你的工具?→ 准备一组问题做评测,检查是否选对工具、参数是否正确,根据失败案例改进工具描述。
@@ -0,0 +1,78 @@
---
id: ai.mcp.scenario.internal-tools-gateway
title: 企业内部 MCP 服务:统一接入、鉴权、审计和版本管理
type: scenario
domain: ai
stack: [mcp, fastapi, nginx]
tags: [MCP, 企业集成, 鉴权, 审计, 工具治理, Streamable HTTP]
level: advanced
prerequisites: [ai.mcp.protocol-server]
est_hours: 2.5
related: [ai.function-calling.scenario.safe-business-tools, ai.mcp.interview]
week: [10, 11]
---
# 企业内部 MCP 服务:统一接入、鉴权、审计和版本管理
## 业务背景与问题
各团队都在做 AI 助手(客服 Agent、研发助手、运营报表 Agent),每个团队都自己对接一遍工单、知识库、CRM 接口,出现:
1. 同一个系统被对接了 4 次,接口变更时要改 4 个地方;
2. 有团队把 CRM 的高权限账号写在了 Agent 的配置里;
3. 员工在 Cursor 里装了来源不明的 MCP 服务,可能读取本地代码和密钥;
4. 出了问题不知道是哪个 Agent、哪个用户调用的。
## 约束
- 已有统一登录(OAuth 2.0 / OIDC)。
- 各业务系统的接口和权限体系保持不变。
## 方案设计
1. **按领域提供 MCP 服务**:工单服务、知识库服务、CRM 只读服务,由各系统的负责团队维护,部署为 **Streamable HTTP** 远程服务。
2. **鉴权**:MCP 规范为 HTTP 传输定义了基于 OAuth 2.1 的授权流程;服务从令牌中识别用户,调用业务系统时使用用户身份,业务系统按原权限校验。不在客户端配置高权限账号。
3. **网关**:Nginx / API 网关统一做 TLS、限流、访问日志;服务内部记录工具级审计日志(用户、客户端、工具、参数摘要、结果、耗时)。
4. **工具治理**:工具命名和描述规范;写操作工具标注并要求确认;工具变更做版本记录,删除工具前先标记废弃。
5. **客户端白名单**:公司发布允许接入的 MCP 服务清单;本地 stdio 服务只允许使用内部审核过的。
## 关键代码
带鉴权的远程服务(示意,SDK 2.x 的鉴权参数为 `token_verifier` / `auth`,具体写法以官方文档为准):
```python
from mcp.server.mcpserver import MCPServer
mcp = MCPServer(
"ticket-service",
token_verifier=CompanyTokenVerifier(issuer="https://sso.example.com"), # 校验 SSO 签发的令牌
auth=AuthSettings(issuer_url="https://sso.example.com", resource_server_url="https://mcp.example.com/tickets"),
)
@mcp.tool()
async def create_ticket(ticket: TicketIn, ctx: Context) -> dict:
user = current_user(ctx) # 从令牌中取用户
audit.log(user=user.id, tool="create_ticket", args=ticket.model_dump())
return await ticket_api.create(ticket, on_behalf_of=user.token, idempotency_key=ticket.request_id)
```
## 取舍
| 选择 | 优点 | 缺点 |
| --- | --- | --- |
| **按领域的远程 MCP 服务(本方案)** | 一次实现多处复用;权限和审计集中 | 需要部署和维护;网络调用多一跳 |
| 每个 Agent 自己对接接口 | 灵活 | 重复建设,权限分散 |
| 一个大而全的 MCP 服务 | 部署简单 | 工具太多,上下文膨胀;团队边界不清 |
| 本地 stdio 服务 | 适合访问本地文件、开发工具 | 不适合共享的业务系统 |
## 易错点
- 远程服务没有鉴权,或者用一个共享令牌。
- 工具返回了超出用户权限的数据。
- 工具描述随意修改,导致各 Agent 的行为突然变化:工具描述也要评审和回归测试。
- 只监控 HTTP 状态码,不知道工具执行的业务结果。
## 面试怎么讲
> “我们把工单、知识库这些内部能力按领域做成远程 MCP 服务,由系统负责团队维护,各个 Agent 和 Cursor 都通过它接入,避免重复对接。服务使用 Streamable HTTP,令牌来自公司 SSO,调用业务系统时用用户身份,原有权限照常生效;写操作工具要求确认并带幂等 ID;网关做限流,服务记录工具级审计日志;工具描述和接口一样要评审和做回归。”