Claude 与 Skill
用 Python 从零写一个 MCP 文档搜索服务器:三个决定模型表现的工具设计细节
作者用 MCP Python SDK 2.3.0 写了一个只读的 docs-search 服务器,并实测哪些工具设计会真正影响模型行为:返回带行号的文本让 Claude 引用正确行号的比例从 1/4 提升到 4/4。文章包含完整可运行的服务器代码、路径安全处理和不需要模型就能跑的测试脚本。
今天就能做
- 写 MCP 工具时让返回值带行号或结构化标记,模型引用准确率差异巨大(实测 1/4 vs 4/4)
- 所有来自模型的路径参数先 resolve() 再判断是否在允许目录内,别只查原始字符串
- 用 SDK 的 Client 做内存内测试,不需要接真实模型就能验证工具行为和越界拒绝
项目搭建:uv + 固定 SDK 版本
- 用 uv init --bare 创建单文件项目(Python 3.12),只生成 pyproject.toml,避免默认的 src/ 打包结构
- 安装 mcp[cli]==2.3.0 并固定版本:SDK 2.x 把服务器类改名为 MCPServer,1.x 的 FastMCP 导入写法跑不起来
- 准备一个 docs 文件夹放 Markdown 文档,再在文件夹外放一个 private.md 作为哨兵文件,专门用来验证越界访问会被拒绝
工具即函数:docstring 就是模型的说明书
- 每个带类型注解的函数在 @mcp.tool() 下自动变成工具,输入 schema 来自类型注解,工具描述来自 docstring
- docstring 相当于 agent 框架里的工具注册表,模型靠它决定调用哪个工具,所以要写清楚参数含义和返回内容
- stdio 传输下绝不能 print():协议规定 stdout 只能写 MCP 消息,日志用 logging 写到 stderr
返回格式直接影响模型表现
- 实测(Claude Code 2.1.308 + Haiku 5.5):read_doc 返回纯文本时,4 次运行只有 1 次引用对了行号
- 改成每行带编号后,4 次全部引用正确——工具返回值是模型接口的一部分,格式决定模型能做什么
- 原则:把模型需要引用的内容做成拿来即用的形式
路径安全:先 resolve 再检查
- 工具参数是模型给的不可信字符串,guides/../../private.md 这种路径没有可疑前缀,直接查字符串会漏掉
- inside_docs() 先调 resolve() 折叠 .. 段并跟随符号链接,再检查真实位置是否在 DOCS_DIR 内、后缀是否为 .md
- docs/ 里名为 leak.md 的符号链接指向外部文件,不 resolve 就看不出来
拒绝方式与无模型测试
- 拒绝越界路径时抛 ToolError:SDK 2.3.0 会把它作为错误结果发给客户端;其他异常只会变成笼统的 Error executing tool 加服务器日志里的堆栈
- read_only_hint 注解只是提示不是安全机制,访问控制必须自己在代码里强制执行
- 测试脚本用 SDK 的 Client 分别做内存内连接和 stdio 子进程两种方式跑同一套断言,包括四个越界路径和符号链接泄漏检查,最后输出 in-memory: ok 和 stdio: ok
原文:MCP Server Tutorial: Build a Docs Search Server in Python
来源:ninadpathak.com · 2026-10-22
查看原文本文由 AI 阅读原文后整理,未经人工逐字核对,内容以原文为准。
登录后可以点赞、查看和发表评论