9.5.4 MCP 服务器开发
- 理解 MCP 服务器的最小职责边界
- 学会定义工具描述、参数结构和调用入口
- 理解为什么服务器开发的重点是“暴露能力”,而不是“把业务逻辑写死”
- 看懂一个最小可运行的 Mock MCP Server
MCP 服务器真正在做什么?
Section titled “MCP 服务器真正在做什么?”它不是“另一个普通后端”
Section titled “它不是“另一个普通后端””普通后端往往直接面向业务接口。 而 MCP 服务器更像:
把已有能力整理成一组可被客户端发现和调用的工具。
所以它的核心关注点通常是:
- 有哪些工具
- 工具怎样描述
- 参数怎样校验
- 结果怎样统一返回
一个直觉类比
Section titled “一个直觉类比”MCP 服务器很像一个有前台的工具库管理员:
- 客户端来问“你这里有什么工具”
- 服务器列出能力清单
- 客户端再说“我要用哪个”
- 服务器按约定执行并返回结果
这和“直接把所有业务函数散着写”非常不一样。
先定义一个最小工具
Section titled “先定义一个最小工具”一个工具最少得有哪几样?
Section titled “一个工具最少得有哪几样?”至少要有:
- 名称
- 描述
- 参数说明
- 实际执行逻辑
一个最小工具描述示例
Section titled “一个最小工具描述示例”search_docs_tool = { "name": "search_docs", "description": "搜索课程文档并返回相关内容", "parameters": { "query": { "type": "string", "description": "要搜索的关键词" } }, "required": ["query"]}
print(search_docs_tool)预期输出:
{'name': 'search_docs', 'description': '搜索课程文档并返回相关内容', 'parameters': {'query': {'type': 'string', 'description': '要搜索的关键词'}}, 'required': ['query']}你可以把这个结构理解成:
工具的对外说明书。
工具描述为什么不能写得太随意?
Section titled “工具描述为什么不能写得太随意?”bad_tool = { "name": "search", "description": "做搜索", "parameters": {"q": {"type": "string"}}}
print(bad_tool)预期输出:
{'name': 'search', 'description': '做搜索', 'parameters': {'q': {'type': 'string'}}}问题在于:
- 名字太模糊
- 描述太空
- 参数含义不清楚
一个更稳的描述
Section titled “一个更稳的描述”good_tool = { "name": "search_course_docs", "description": "搜索课程 FAQ、政策和学习路线文档", "parameters": { "query": { "type": "string", "description": "用户要查询的主题,比如 退款政策 或 证书" } }, "required": ["query"]}
print(good_tool)预期输出:
{'name': 'search_course_docs', 'description': '搜索课程 FAQ、政策和学习路线文档', 'parameters': {'query': {'type': 'string', 'description': '用户要查询的主题,比如 退款政策 或 证书'}}, 'required': ['query']}这里更好的地方在于:
- 工具边界更清楚
- 参数语义更清楚
- 客户端更容易正确使用
服务器的最小两项能力:列工具 + 调工具
Section titled “服务器的最小两项能力:列工具 + 调工具”一个最小可用的 MCP 服务器,通常至少要能:
- 列出可用工具
- 接受某个工具调用
先写一个最小服务器
Section titled “先写一个最小服务器”class MockMCPServer: def __init__(self): self.tool_specs = [ { "name": "search_docs", "description": "搜索课程文档", "parameters": { "query": {"type": "string"} } } ]
def list_tools(self): return self.tool_specs
server = MockMCPServer()print(server.list_tools())预期输出:
[{'name': 'search_docs', 'description': '搜索课程文档', 'parameters': {'query': {'type': 'string'}}}]再加真正的执行逻辑
Section titled “再加真正的执行逻辑”class MockMCPServer: def __init__(self): self.kb = { "退款": "课程购买后 7 天内且学习进度低于 20% 可退款。", "证书": "完成所有项目并通过测试后可获得证书。" }
self.tool_specs = [ { "name": "search_docs", "description": "搜索课程文档", "parameters": { "query": {"type": "string"} } } ]
def list_tools(self): return self.tool_specs
def call_tool(self, name, arguments): if name != "search_docs": return {"error": "unknown_tool"}
query = arguments.get("query", "") for key, value in self.kb.items(): if key in query: return {"result": value} return {"result": "未找到相关文档"}
server = MockMCPServer()print(server.call_tool("search_docs", {"query": "退款政策是什么"}))预期输出:
{'result': '课程购买后 7 天内且学习进度低于 20% 可退款。'}这已经是一个非常清楚的最小服务器骨架了。
参数校验为什么是服务器的责任之一?
Section titled “参数校验为什么是服务器的责任之一?”因为客户端或模型都可能给错参数
Section titled “因为客户端或模型都可能给错参数”例如:
bad_call = {"query_text": "退款政策"}如果服务器直接执行,就可能报错或产生奇怪行为。
一个最小校验版本
Section titled “一个最小校验版本”def validate_search_docs(arguments): if "query" not in arguments: return False, "missing_query" if not isinstance(arguments["query"], str): return False, "query_must_be_string" return True, "ok"
print(validate_search_docs({"query": "退款政策"}))print(validate_search_docs({"query_text": "退款政策"}))预期输出:
(True, 'ok')(False, 'missing_query')为什么这一步一定不能省?
Section titled “为什么这一步一定不能省?”因为服务器是能力边界守门人。 如果服务器不校验,整个工具系统就很难稳定。

一个更完整的最小服务器版本
Section titled “一个更完整的最小服务器版本”class BetterMCPServer: def __init__(self): self.kb = { "退款": "课程购买后 7 天内且学习进度低于 20% 可退款。", "证书": "完成所有项目并通过测试后可获得证书。" }
def list_tools(self): return [ { "name": "search_docs", "description": "搜索课程文档", "parameters": { "query": {"type": "string"} } } ]
def validate(self, name, arguments): if name != "search_docs": return False, "unknown_tool" if "query" not in arguments: return False, "missing_query" if not isinstance(arguments["query"], str): return False, "query_must_be_string" return True, "ok"
def call_tool(self, name, arguments): ok, msg = self.validate(name, arguments) if not ok: return {"error": msg}
query = arguments["query"] for key, value in self.kb.items(): if key in query: return {"result": value} return {"result": "未找到相关文档"}
server = BetterMCPServer()print(server.list_tools())print(server.call_tool("search_docs", {"query": "证书怎么获得"}))print(server.call_tool("search_docs", {"wrong": "证书怎么获得"}))预期输出:
[{'name': 'search_docs', 'description': '搜索课程文档', 'parameters': {'query': {'type': 'string'}}}]{'result': '完成所有项目并通过测试后可获得证书。'}{'error': 'missing_query'}
这个版本比上一版强在哪?
Section titled “这个版本比上一版强在哪?”它已经具备了:
- 工具列出
- 参数校验
- 统一调用入口
- 统一错误返回
这已经非常接近真实工程里服务器的核心职责。
MCP 服务器开发里最常见的坑
Section titled “MCP 服务器开发里最常见的坑”把业务逻辑和协议逻辑混在一起
Section titled “把业务逻辑和协议逻辑混在一起”结果会变成:
- 工具描述不清
- 扩展困难
- 调试困难
工具粒度太粗或太细
Section titled “工具粒度太粗或太细”- 太粗:一个工具什么都干
- 太细:客户端调用复杂度爆炸
返回结构不统一
Section titled “返回结构不统一”有时返回文本,有时返回 dict,有时直接抛异常,后面会很难接。
怎么判断一个 MCP 服务器设计得够不够好?
Section titled “怎么判断一个 MCP 服务器设计得够不够好?”可以先问四个问题:
- 客户端能不能清楚知道有哪些工具
- 参数要求是不是明确
- 错误返回是不是统一
- 加新工具时结构会不会越来越乱
如果这四个问题都答得比较稳,服务器设计通常就已经不错了。
学完这一页,至少保留这张证据卡:
- 能力
- 服务器暴露的资源、Prompt 或工具
- 契约
- schema、传输、权限和错误形式
- 调用轨迹
- 发现、调用、响应和失败处理
- 失败检查
- 架构不兼容、缺少认证、不安全工具或服务器错误
- 集成动作
- 在加入自主能力前先验证服务端契约
这一节最重要的不是“把一个类写出来”,而是理解:
MCP 服务器的本质,是把一组可执行能力,用清晰可发现、可校验、可调用的方式暴露出来。
服务器做得越清楚,客户端侧越容易扩展,整个工具生态也越容易做大。
- 给
BetterMCPServer再增加一个get_weather(city)工具。 - 为这个新工具补上参数校验逻辑。
- 想一想:工具粒度太粗和太细,各自会带来什么问题?
- 用自己的话解释:为什么说 MCP 服务器开发的核心不只是“执行工具”,更是“暴露清晰边界”?
参考实现与讲解
- 可以把
get_weather(city)注册成一个带描述和 schema 的 tool,并返回类似{city, condition, source}的小型结构化结果。除非正文已经接入真实 API,否则用占位数据即可。 - 校验
city是否存在、是否为非空字符串、是否不是异常大的输入。输入无效时要返回清晰错误,而不是静默猜测。 - 工具粒度太粗会隐藏关键选择,错误也难定位;粒度太细会迫使 Agent 规划大量小调用,增加延迟、路由错误和上下文噪声。
- MCP Server 开发的核心是暴露边界:工具做什么、接受什么输入、返回什么输出和错误、需要什么权限。执行工具只是契约的一部分。