MCP 从入门到实战:用 Python 构建一个真正可用的本地知识便签服务
MCP 从入门到实战:用 Python 构建一个真正可用的本地知识便签服务
MCP(Model Context Protocol,模型上下文协议)正在成为 AI 应用连接外部数据与工具的一种标准方式。本文不会停留在概念介绍,而是带你从零实现一个 MCP Server:它可以新增便签、搜索便签、以资源形式读取内容,还能向 AI 客户端提供可复用的 Prompt。最后,我们会使用 Python Client 和 MCP Inspector 完成连接、调用与调试。
本文适合第一次接触 MCP 的开发者。你只需要具备基础 Python 知识,跟着步骤即可完成实践。
一、MCP 到底是什么?
MCP 全称 Model Context Protocol,可以理解为一套让 AI 应用连接外部系统的开放协议。
大模型本身只擅长处理输入给它的内容。它默认不知道你的本地文件、数据库、内部 API,也不能直接操作真实业务系统。如果每接入一种数据源都编写一套私有适配代码,应用和工具之间会形成大量重复集成:
AI 应用 A ── 私有适配 ── 文件系统
AI 应用 A ── 私有适配 ── 数据库
AI 应用 B ── 私有适配 ── 文件系统
AI 应用 B ── 私有适配 ── 数据库
MCP 在中间定义统一协议后,关系变成:
AI 应用 ── MCP Client ── MCP Server ── 文件、数据库、API、业务系统
MCP Server 用统一方式声明“我有哪些工具、资源和提示模板”,MCP Client 用统一方式发现和调用这些能力。
一个便于新手理解的比喻是:MCP 类似 AI 应用的通用扩展接口。它规定连接与通信方式,但并不规定你必须使用哪个模型、数据库或业务框架。
MCP 不是什么?
MCP 经常被误解,需要先排除几个错误认识:
- MCP 不是大模型,也不会让模型本身变得更聪明;
- MCP 不是 Agent 框架,不负责任务规划和循环执行;
- MCP 不是函数调用的替代品,MCP Tool 最终仍可能映射为模型工具调用;
- MCP 不是数据库,它只是提供访问数据库的标准接口;
- MCP Server 不一定部署在远程,也可以是客户端启动的本地进程。
MCP 解决的是连接标准化。模型是否调用工具、Agent 如何规划、应用怎样管理状态,仍由上层系统决定。
二、先搞懂 Host、Client 与 Server
MCP 采用 Host–Client–Server 架构。
| 角色 | 作用 | 通俗理解 |
|---|---|---|
| Host | 承载用户交互、模型与权限策略的 AI 应用 | 使用插件的应用程序 |
| Client | Host 内部负责与某个 Server 通信的协议客户端 | 一条独立连接 |
| Server | 对外提供 Tools、Resources、Prompts 等能力 | 插件能力提供方 |
需要特别注意:Host 和 Client 不是同一个概念。
一个 Host 可以同时连接多个 MCP Server,并为每个 Server 创建独立 Client:
┌─ MCP Client A ─ MCP 文件服务
用户 ─ AI Host ─ 模型 ──┼─ MCP Client B ─ MCP 数据库服务
└─ MCP Client C ─ MCP 搜索服务
这样设计有利于隔离连接、权限和上下文。文件 Server 不需要知道数据库 Server 的存在,Server 之间也不应该默认共享数据。
一次连接通常发生什么?
从协议角度看,连接大致经历以下过程:
- Client 与 Server 建立传输连接;
- 双方执行初始化并协商协议版本和能力;
- Client 查询 Server 提供的 Tools、Resources、Prompts;
- Host 根据用户请求和权限选择能力;
- Client 发起调用,Server 返回结构化结果;
- 连接关闭或继续处理后续请求。
MCP 消息基于 JSON-RPC 2.0 的请求、响应和通知模型。日常使用 SDK 时通常不用手写 JSON-RPC,但了解这一点有助于排查请求 ID、错误响应和超时问题。
三、MCP Server 的三种核心能力
初学 MCP 最容易混淆 Tools、Resources 和 Prompts。判断方法是看谁控制、有没有副作用、返回的是什么。
3.1 Tools:执行动作
Tool 是可以被模型或 Host 选择调用的函数,例如:
- 查询订单;
- 搜索知识库;
- 计算数据;
- 创建便签;
- 调用业务 API。
Tool 有名称、描述、参数 Schema 和返回结果。它可能产生副作用,所以写入、删除、发送等操作必须有清晰权限和确认机制。
模型判断需要搜索
→ Client 调用 search_notes
→ Server 执行 Python 函数
→ 返回搜索结果
→ 结果进入模型上下文
3.2 Resources:提供可读取的上下文
Resource 更像“可寻址的数据”,由应用选择读取,例如:
file:///project/README.mdnotes://indexnotes://42db://schema/users
Resource 通常用于读取,不应把明显的修改操作伪装成 Resource。它可以返回文本或二进制内容,并带有 URI、名称和 MIME Type。
3.3 Prompts:提供可复用的交互模板
Prompt 是 Server 暴露的参数化模板,例如:
- 总结指定主题的便签;
- 根据代码生成审查清单;
- 按固定格式分析数据。
Prompt 的价值在于将某个数据源或工具的最佳使用方式一起封装起来。它通常由用户或 Host 显式选择,而不是被模型当作普通工具随意调用。
3.4 快速对照
| 能力 | 核心问题 | 控制方倾向 | 是否可能有副作用 |
|---|---|---|---|
| Tool | “执行什么动作?” | 模型/应用 | 可能有 |
| Resource | “读取什么内容?” | 应用 | 通常没有 |
| Prompt | “怎样组织一次交互?” | 用户/应用 | 本身没有 |
如果你在设计 Server 时拿不准,就问自己:这是一个动词、一个可寻址名词,还是一个对话模板?
四、MCP 与 Function Calling、RAG、Agent 的关系
这四个概念处于不同层次,并不互相替代。
| 技术 | 解决的问题 |
|---|---|
| Function Calling | 模型怎样用结构化参数表达“我要调用某个函数” |
| MCP | AI 应用怎样发现、连接并调用外部能力 |
| RAG | 怎样检索外部知识并把证据加入模型上下文 |
| Agent | 怎样围绕目标规划、行动、观察并循环执行 |
它们可以组成完整链路:
用户提出任务
↓
Agent 判断需要外部知识
↓
模型通过 Function Calling 选择工具
↓
MCP Client 调用 MCP Server 的检索 Tool
↓
Server 执行 RAG 检索并返回证据
↓
Agent 基于证据继续完成任务
所以,MCP 更像连接层,而不是整个 AI 应用架构。
五、传输方式:stdio 与 Streamable HTTP
MCP 需要传输层承载协议消息。实践中重点理解两种方式。
5.1 stdio:适合本地 Server
Client 启动一个 Server 子进程,通过标准输入和标准输出交换消息:
MCP Client ── stdin/stdout ── 本地 MCP Server 进程
优点:
- 不需要监听端口;
- 本地部署简单;
- 进程生命周期由 Client 管理;
- 适合文件、IDE、开发工具等本地能力。
stdio 有一条非常重要的规则:不要向 stdout 打印调试信息。stdout 是协议通道,普通 print() 可能破坏消息。日志应输出到 stderr 或写入日志文件。
5.2 Streamable HTTP:适合远程服务
远程 Server 通常通过 Streamable HTTP 提供 MCP 端点:
MCP Client ── HTTPS ── 网关/鉴权 ── 远程 MCP Server
它适合团队共享服务、云端数据源和集中式部署。与本地 stdio 相比,必须额外处理:
- HTTPS;
- 身份认证与授权;
- Origin 校验;
- 限流和超时;
- 会话与负载均衡;
- 审计日志;
- 服务端请求伪造等网络风险。
早期 MCP 示例中经常出现独立 HTTP+SSE 传输。新项目应优先查看当前 SDK 对 Streamable HTTP 的支持,不要盲目复制过时教程。
本文先使用 stdio,因为它依赖最少、最适合第一次跑通 MCP。
六、实战目标:本地知识便签 MCP Server
我们将实现一个 notes-assistant Server,提供以下能力:
Tools
add_note:新增便签;search_notes:按关键词搜索;delete_note:删除便签,并要求显式确认。
Resources
notes://index:读取便签索引;notes://{note_id}:读取指定便签。
Prompt
summarize_topic:生成指定主题的总结任务模板。
数据保存在本地 JSON 文件中,不需要数据库、API Key 或外部网络。
七、准备开发环境
7.1 环境要求
- Python 3.10 或更高版本;
- 推荐使用
uv管理虚拟环境,也可以使用pip; - 一个支持 MCP 的客户端,或者直接使用本文的 Python Client。
7.2 创建项目
mkdir mcp-notes-demo
cd mcp-notes-demo
uv init
uv add "mcp[cli]"
如果没有安装 uv,也可以使用传统方式:
python -m venv .venv
# Windows PowerShell
.venv\Scripts\Activate.ps1
# macOS / Linux
source .venv/bin/activate
pip install "mcp[cli]"
最终目录如下:
mcp-notes-demo/
├── data/
│ └── notes.json # 首次写入时自动创建
├── server.py
├── client.py
└── pyproject.toml
不要手动创建 notes.json,代码会自动完成初始化。
八、完整实现 MCP Server
创建 server.py:
from __future__ import annotations
import json
import logging
import sys
from datetime import datetime, timezone
from pathlib import Path
from threading import Lock
from uuid import uuid4
from mcp.server.fastmcp import FastMCP
# stdio 模式下 stdout 用于 MCP 协议,日志必须写到 stderr
logging.basicConfig(
stream=sys.stderr,
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
)
logger = logging.getLogger("notes-mcp")
mcp = FastMCP("notes-assistant")
BASE_DIR = Path(__file__).resolve().parent
DATA_DIR = BASE_DIR / "data"
NOTES_FILE = DATA_DIR / "notes.json"
FILE_LOCK = Lock()
def ensure_storage() -> None:
DATA_DIR.mkdir(parents=True, exist_ok=True)
if not NOTES_FILE.exists():
NOTES_FILE.write_text("[]", encoding="utf-8")
def load_notes() -> list[dict]:
ensure_storage()
with FILE_LOCK:
try:
content = NOTES_FILE.read_text(encoding="utf-8")
data = json.loads(content)
except (OSError, json.JSONDecodeError) as exc:
logger.exception("failed to read notes")
raise RuntimeError("便签存储暂时不可用") from exc
if not isinstance(data, list):
raise RuntimeError("便签文件格式错误")
return data
def save_notes(notes: list[dict]) -> None:
ensure_storage()
# 先写临时文件,再替换正式文件,降低写到一半损坏数据的概率
temp_file = NOTES_FILE.with_suffix(".tmp")
payload = json.dumps(notes, ensure_ascii=False, indent=2)
with FILE_LOCK:
try:
temp_file.write_text(payload, encoding="utf-8")
temp_file.replace(NOTES_FILE)
except OSError as exc:
logger.exception("failed to save notes")
raise RuntimeError("便签保存失败") from exc
@mcp.tool()
def add_note(title: str, content: str, tags: list[str] | None = None) -> dict:
"""新增一条本地便签。
Args:
title: 便签标题,不能为空,最长 100 个字符。
content: 便签正文,不能为空,最长 5000 个字符。
tags: 可选标签列表,每条最多 20 个字符。
"""
title = title.strip()
content = content.strip()
normalized_tags = [tag.strip() for tag in (tags or []) if tag.strip()]
if not title or len(title) > 100:
raise ValueError("title 长度必须在 1 到 100 之间")
if not content or len(content) > 5000:
raise ValueError("content 长度必须在 1 到 5000 之间")
if len(normalized_tags) > 10:
raise ValueError("tags 最多包含 10 项")
if any(len(tag) > 20 for tag in normalized_tags):
raise ValueError("每个 tag 最长 20 个字符")
notes = load_notes()
note = {
"id": uuid4().hex[:12],
"title": title,
"content": content,
"tags": normalized_tags,
"created_at": datetime.now(timezone.utc).isoformat(),
}
notes.append(note)
save_notes(notes)
logger.info("created note id=%s", note["id"])
return note
@mcp.tool()
def search_notes(keyword: str = "", limit: int = 10) -> list[dict]:
"""按标题、正文或标签搜索便签;keyword 为空时返回最近便签。"""
keyword = keyword.strip().casefold()
if not 1 <= limit <= 50:
raise ValueError("limit 必须在 1 到 50 之间")
notes = load_notes()
notes.reverse()
if keyword:
notes = [
note
for note in notes
if keyword
in " ".join(
[
note.get("title", ""),
note.get("content", ""),
*note.get("tags", []),
]
).casefold()
]
# 搜索列表只返回摘要,完整内容通过 Resource 获取
return [
{
"id": note["id"],
"title": note["title"],
"preview": note["content"][:120],
"tags": note.get("tags", []),
"created_at": note["created_at"],
}
for note in notes[:limit]
]
@mcp.tool()
def delete_note(note_id: str, confirm: bool = False) -> dict:
"""删除指定便签。只有 confirm=true 时才会真正删除。"""
note_id = note_id.strip()
if not confirm:
return {
"deleted": False,
"message": "这是写操作,请确认 note_id 后以 confirm=true 再次调用。",
}
notes = load_notes()
remaining = [note for note in notes if note.get("id") != note_id]
if len(remaining) == len(notes):
return {"deleted": False, "message": "未找到对应便签"}
save_notes(remaining)
logger.info("deleted note id=%s", note_id)
return {"deleted": True, "id": note_id}
@mcp.resource("notes://index")
def get_notes_index() -> str:
"""返回所有便签的 Markdown 索引。"""
notes = load_notes()
if not notes:
return "# 便签索引\n\n当前没有便签。"
lines = ["# 便签索引", ""]
for note in reversed(notes):
tags = ", ".join(note.get("tags", [])) or "无标签"
lines.append(
f'- [{note["title"]}](notes://{note["id"]}) '
f'— `{tags}` — {note["created_at"]}'
)
return "\n".join(lines)
@mcp.resource("notes://{note_id}")
def get_note(note_id: str) -> str:
"""通过 notes://{note_id} 读取一条完整便签。"""
note = next(
(item for item in load_notes() if item.get("id") == note_id),
None,
)
if note is None:
raise ValueError(f"未找到便签:{note_id}")
tags = ", ".join(note.get("tags", [])) or "无标签"
return (
f'# {note["title"]}\n\n'
f'- ID:`{note["id"]}`\n'
f'- 标签:{tags}\n'
f'- 创建时间:{note["created_at"]}\n\n'
f'{note["content"]}'
)
@mcp.prompt()
def summarize_topic(topic: str, style: str = "要点式") -> str:
"""生成一个基于本地便签总结指定主题的提示模板。"""
return f"""请总结本地便签中与“{topic}”有关的内容。
执行要求:
1. 先调用 search_notes 搜索“{topic}”;
2. 需要完整内容时读取对应的 notes://{{note_id}} Resource;
3. 只根据便签中的内容总结,不要补充未出现的事实;
4. 如果没有相关便签,明确说明资料不足;
5. 使用“{style}”输出,并在结论后标注便签标题。
"""
if __name__ == "__main__":
# stdio 是本地 MCP Server 最常见的传输方式
mcp.run(transport="stdio")
代码里有哪些关键点?
FastMCP("notes-assistant")创建 Server;@mcp.tool()从类型注解和 Docstring 生成工具定义;@mcp.resource()声明固定 Resource 和动态 Resource 模板;@mcp.prompt()暴露参数化 Prompt;- 工具内部仍然执行普通 Python 代码,MCP 并没有替代业务逻辑;
- 所有外部参数都在 Server 端校验,不能只信任模型;
- 删除操作要求
confirm=true,体现高风险动作的二次确认思想; - 搜索只返回摘要,完整内容按需读取,避免无意义地膨胀上下文。
九、使用 MCP Inspector 调试 Server
不要一开始就把 Server 接进复杂 AI 客户端。先用 MCP Inspector 独立验证协议和工具,可以大幅降低排错难度。
在项目目录执行:
uv run mcp dev server.py
如果使用的是 pip 环境,可按当前 SDK 提供的 CLI 方式启动 Inspector。终端会显示本地调试地址,浏览器打开后完成以下检查:
- 确认 Server 初始化成功;
- 查看 Tools 列表,检查参数 Schema 和描述;
- 调用
add_note:
{
"title": "MCP 学习记录",
"content": "MCP Server 可以通过 Tools、Resources 和 Prompts 暴露能力。",
"tags": ["MCP", "Python"]
}
- 调用
search_notes,关键词填写MCP; - 打开
notes://index; - 使用新增便签的 ID 读取
notes://{note_id}; - 查看
summarize_topicPrompt 的展开结果; - 先以
confirm=false调用删除,再确认是否符合预期。
如果 Tools 页面没有工具,优先检查装饰器是否在 mcp.run() 之前执行;如果 Server 直接退出,检查终端中的 stderr 日志。
十、编写 Python MCP Client,完整验证协议调用
Inspector 适合人工调试,自动化测试更适合使用 Client SDK。
创建 client.py:
import asyncio
import sys
from pathlib import Path
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
BASE_DIR = Path(__file__).resolve().parent
async def main() -> None:
server = StdioServerParameters(
command=sys.executable,
args=[str(BASE_DIR / "server.py")],
)
async with stdio_client(server) as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
# 必须先初始化,初始化阶段会完成版本和能力协商
await session.initialize()
tools = await session.list_tools()
print("可用工具:", [tool.name for tool in tools.tools])
created = await session.call_tool(
"add_note",
arguments={
"title": "第一次调用 MCP Tool",
"content": "这条便签由 Python MCP Client 创建。",
"tags": ["MCP", "实战"],
},
)
print("新增结果:", created.content)
searched = await session.call_tool(
"search_notes",
arguments={"keyword": "MCP", "limit": 5},
)
print("搜索结果:", searched.content)
resources = await session.list_resources()
print("固定资源:", [str(item.uri) for item in resources.resources])
index = await session.read_resource("notes://index")
print("便签索引:", index.contents[0].text)
prompts = await session.list_prompts()
print("可用 Prompt:", [item.name for item in prompts.prompts])
prompt = await session.get_prompt(
"summarize_topic",
arguments={"topic": "MCP", "style": "表格"},
)
print("Prompt:", prompt.messages[0].content)
if __name__ == "__main__":
asyncio.run(main())
运行:
uv run python client.py
或者在已激活的虚拟环境中:
python client.py
如果终端依次显示工具列表、新增结果、搜索结果、资源和 Prompt,说明你已经真正跑通了:
Client 启动 Server 子进程
→ initialize 能力协商
→ list_tools 能力发现
→ call_tool 工具调用
→ read_resource 资源读取
→ get_prompt 获取模板
这比“客户端界面里好像出现了一个工具”更有价值,因为我们验证了每一层。
十一、接入支持 MCP 的 AI 客户端
不同客户端的配置文件位置和格式不完全相同,但本地 stdio Server 的核心配置通常包含:
- Server 名称;
- 启动命令
command; - 命令参数
args; - 必要的环境变量
env。
很多客户端使用类似下面的 JSON 结构:
{
"mcpServers": {
"notes-assistant": {
"command": "uv",
"args": [
"--directory",
"C:/projects/mcp-notes-demo",
"run",
"python",
"server.py"
]
}
}
}
请把路径替换成自己的绝对路径。Windows JSON 中可以使用正斜杠 /;如果使用反斜杠,则必须写成 \\。
保存配置并重启或刷新客户端后,测试这些自然语言请求:
帮我创建一条便签:标题“学习计划”,正文“本周完成 MCP Server 实战”,标签是 MCP 和 Python。
搜索包含 MCP 的便签,并告诉我每条便签的标题。
删除刚才创建的学习计划便签。执行前先让我确认。
当用户提出请求后,并不是 MCP Server 在调用模型。真实过程是:
- AI 客户端把工具定义提供给模型;
- 模型判断应该调用哪个 Tool;
- 客户端显示或执行工具调用;
- MCP Client 将调用发送给 Server;
- Server 执行业务函数并返回结果;
- 客户端再把结果交给模型组织回答。
如果工具在 Inspector 和 Python Client 中正常、但 AI 客户端里不可见,问题通常在客户端配置、启动路径或客户端日志,而不是业务代码。
十二、如何设计高质量 MCP Tool
工具能运行不代表模型会正确使用。Tool 的设计质量会直接影响工具选择准确率。
12.1 名称要明确
推荐:
search_notes
get_order_status
create_support_ticket
不推荐:
handle
execute
process_data
12.2 描述要告诉模型“何时使用”
差的描述:
搜索工具。
更好的描述:
按标题、正文或标签搜索本地便签。当用户询问过去记录、学习笔记或已保存信息时使用;keyword 为空时返回最近便签。
12.3 参数应该窄而具体
不要提供一个万能参数:
def execute(payload: dict) -> dict:
...
优先使用类型明确的参数:
def search_notes(keyword: str, limit: int = 10) -> list[dict]:
...
类型注解不仅帮助开发者,也会进入 JSON Schema,帮助模型生成正确参数。
12.4 返回模型真正需要的信息
工具返回内容要做到:
- 结构稳定;
- 字段含义明确;
- 错误可理解;
- 不包含无关大字段;
- 必要时提供下一步所需的 ID 或 URI。
例如搜索结果返回 id,模型随后才能读取 notes://{id}。如果只返回一段不可解析文本,后续调用会更困难。
12.5 区分只读与写操作
推荐将查询和修改分成不同 Tool:
get_note # 只读
update_note # 写入
delete_note # 高风险写入
不要设计一个 manage_note(action=...) 包揽所有行为,否则权限、确认和审计都会变得模糊。
十三、错误处理:不要把 Python 堆栈直接扔给模型
Server 端错误大致分为三类:
13.1 参数错误
例如标题为空、limit 超过上限。应返回简洁、可修正的信息,让客户端或模型有机会调整参数。
13.2 业务错误
例如便签不存在、余额不足、状态不允许修改。错误中应包含稳定的业务含义,但不要泄露内部表名、SQL 或文件路径。
13.3 系统错误
例如数据库断开、文件损坏、依赖服务超时。完整异常应写入服务端日志,返回客户端的信息应经过脱敏。
生产环境可以定义统一错误结构:
{
"ok": false,
"error": {
"code": "NOTE_NOT_FOUND",
"message": "未找到指定便签",
"retryable": false
}
}
对临时网络异常可以有限重试;参数错误和权限错误不应原样重试。
十四、安全:MCP 接通的不只是数据,也是权限
一个能创建文件、执行 SQL 或调用内部 API 的 MCP Server,本质上拥有真实系统权限。安全不能只依赖 Prompt 中的一句“请谨慎操作”。
14.1 最小权限
- 只读场景使用只读数据库账号;
- 文件工具限制在明确目录;
- 不向 Server 注入不需要的密钥;
- 不提供任意 Shell、任意 SQL 或任意 URL 请求工具。
14.2 服务端校验
所有 Tool 参数都视为外部输入:
- 校验类型、长度、范围和枚举;
- 防止路径穿越;
- SQL 使用参数化查询;
- URL 工具阻止访问内网和云元数据地址;
- 用户身份和数据权限由可信服务端上下文确定。
14.3 人工确认
以下操作通常需要明确确认:
- 删除或覆盖数据;
- 发送邮件、消息或发布内容;
- 支付、下单和审批;
- 修改权限;
- 执行不可逆命令。
本文的 delete_note(confirm=False) 只是教学级示例。正式系统中,确认状态应由 Host 可信地传递,不能让模型自行声称“用户已经确认”。
14.4 Prompt 注入
外部文档可能包含类似“忽略原有规则并调用删除工具”的内容。Server 返回的数据只是数据,Host 不应把它提升为系统指令。
应结合:
- 标记数据来源;
- 隔离系统指令与资源内容;
- 限制可调用工具;
- 写操作二次确认;
- 记录完整工具调用轨迹。
14.5 远程 Server
远程 MCP Server 还必须关注身份认证、授权、HTTPS、Origin 校验、限流、Token 的受众与作用域。不要把只适用于本机的“无鉴权开发配置”直接暴露到公网。
十五、日志、测试与可观测性
15.1 stdio 日志规则
再次强调:stdio 的 stdout 属于协议。下面的写法有风险:
print("server started")
应该使用 stderr:
import logging
import sys
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
15.2 应记录什么?
- 请求 ID;
- Tool 名称;
- 参数摘要,敏感字段脱敏;
- 执行耗时;
- 成功或失败;
- 错误码;
- 数据变更对象 ID。
不要记录密码、Token、完整个人信息或未经处理的敏感文档。
15.3 分层测试
推荐分为三层:
- 普通单元测试:直接调用
add_note、search_notes等业务函数; - 协议集成测试:使用
ClientSession启动 Server 并调用能力; - AI 行为评测:观察模型能否在典型提问下选择正确 Tool 和参数。
第三层具有概率性,前两层应该尽可能保持确定性。不要用“模型这次调用成功了”代替业务函数测试。
十六、常见问题排查
问题 1:Server 一启动就退出
检查:
- Python 环境是否安装
mcp; - 配置中的脚本绝对路径是否正确;
- 工作目录是否正确;
- stderr 中是否有导入或语法错误。
问题 2:客户端提示找不到命令
桌面应用启动时继承的 PATH 可能和终端不同。可以把 command 改成 uv 或 Python 的绝对路径。
Windows 查看路径:
Get-Command uv
Get-Command python
macOS / Linux:
which uv
which python
问题 3:日志导致协议解析失败
检查是否使用 print() 向 stdout 输出了普通文本。stdio Server 的调试信息全部改用 stderr。
问题 4:Inspector 能用,AI 客户端不能用
通常检查:
- 客户端配置文件是否放在正确位置;
- 修改配置后是否刷新或重启;
- JSON/TOML 是否存在语法错误;
- 启动命令在客户端环境中是否可执行;
- 客户端是否允许并启用了该 Server。
问题 5:工具可见,但模型总选错
优先优化:
- Tool 名称;
- Tool 描述;
- 参数名称和 Schema;
- 重叠工具的职责边界;
- Tool 返回结果。
不是所有问题都应该通过增加更长的系统 Prompt 解决。
问题 6:动态 Resource 没出现在固定资源列表
notes://{note_id} 属于 Resource Template,和固定的 notes://index 不是同一类列表。客户端需要支持资源模板,或者直接使用已知 URI 读取。
十七、如何把示例升级到生产项目
当前示例适合学习,但 JSON 文件不适合多实例、高并发生产服务。可以按以下顺序升级。
第一阶段:替换存储
- 使用 SQLite 或 MySQL 保存便签;
- 增加用户 ID 和数据权限;
- 写操作增加事务;
- 对常用查询建立索引。
第二阶段:增加语义检索
- 对便签分块并生成 Embedding;
- 将向量写入向量数据库;
search_notes同时支持关键词与语义检索;- 返回来源和相关性分数。
第三阶段:远程部署
- 切换到 SDK 当前支持的 Streamable HTTP;
- 部署在 HTTPS 后;
- 接入身份认证、授权、限流和审计;
- 配置健康检查、超时和监控。
第四阶段:Agent 工作流
- Agent 通过 MCP 搜索资料;
- 根据资料生成结构化草稿;
- 审核 Agent 检查引用与事实;
- 高风险动作进入人工确认节点。
每一步都应该先建立测试和权限边界,再扩大 Server 能力。
十八、什么时候适合使用 MCP?
适合:
- 希望同一套工具被多个 AI 客户端复用;
- 需要统一暴露文件、数据库、搜索或内部 API;
- 希望能力可发现、参数有 Schema、调用可审计;
- 正在构建可插拔的 AI 应用或 Agent 平台。
不一定适合:
- 应用内部只有一个固定函数,且没有复用需求;
- 普通 REST API 已经满足所有非 AI 系统集成;
- 只是想让模型回答一段静态文本;
- 团队尚未准备好处理工具权限和安全问题。
MCP 不是为了替换所有 API。通常是 MCP Server 在内部继续调用已有 REST API、数据库 SDK 或本地函数,然后以 AI 应用容易发现和使用的方式对外暴露。
总结
学完本文,应该记住以下几点:
- MCP 是 AI 应用连接外部数据、工具和工作流的开放协议;
- Host 承载应用与模型,Client 管理协议连接,Server 提供能力;
- Tool 用于执行动作,Resource 用于读取数据,Prompt 用于复用交互模板;
- stdio 适合本地 Server,Streamable HTTP 适合远程服务;
- MCP 可以与 Function Calling、RAG 和 Agent 配合,但它们不是同一层技术;
- Server 端必须负责参数校验、权限、确认、错误处理和审计;
- 最有效的学习方式,是先用 Inspector 和 Python Client 跑通完整协议链路。
MCP 的代码入门并不困难,真正的工程难点在于:如何把外部能力设计成边界清晰、权限最小、返回稳定、可被模型正确理解的接口。先把本文的本地便签服务跑通,再尝试替换为真实数据库或知识库,你就完成了从“理解 MCP”到“能开发 MCP”的关键一步。
参考资料
更多推荐


所有评论(0)