9.5.4 MCP Server 开发
本节定位
前两节我们已经知道:
- MCP 要解决什么问题
- MCP 架构里 client 和 server 各自负责什么
这一节开始真正落地 server 视角,回答:
如果我要自己写一个 MCP Server,我到底应该从哪里开始?
学习目标
- 理解 MCP Server 的最小职责边界
- 学会定义工具描述、参数结构和调用入口
- 理解为什么 server 开发的重点是“暴露能力”,而不是“把业务逻辑写死”
- 看懂一个最小可运行的 Mock MCP Server
MCP Server 真正在做什么?
它不是“另一个普通后端”
普通后端往往直接面向业务接口。 而 MCP Server 更像:
把已有能力整理成一组可被 client 发现和调用的工具。
所以它的核心关注点通常是:
- 有哪些工具
- 工具怎样描述
- 参数怎样校验
- 结果怎样统一返回
一个直觉类比
MCP Server 很像一个有前台的工具库管理员:
- 客户端来问“你这里有什么工具”
- Server 列出能力清单
- 客户端再说“我要用哪个”
- Server 按约定执行并返回结果
这和“直接把所有业务函数散着写”非常不一样。
先定义一个最小工具
一个工具最少得有哪几样?
至少要有:
- 名称
- 描述
- 参数说明
- 实际执行逻辑
一个最小工具描述示例
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']}
你可以把这个结构理解成:
工具的对外说明书。
工具描述为什么不能写得太随意?
一个坏描述
bad_tool = {
"name": "search",
"description": "做搜索",
"parameters": {"q": {"type": "string"}}
}
print(bad_tool)
预期输出:
{'name': 'search', 'description': '做搜索', 'parameters': {'q': {'type': 'string'}}}
问题在于:
- 名字太模糊
- 描述太空
- 参数含义不清楚
一个更稳的描述
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']}
这里更好的地方在于:
- 工具边界更清楚
- 参数语义更清楚
- client 更容易正确使用
Server 的最小两项能力:列工具 + 调工具
一个最小可用的 MCP Server,通常至少要能:
- 列出可用工具
- 接受某个工具调用
先写一个最小 Server
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'}}}]
再加真正的执行逻辑
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% 可退款。'}
这已经是一个非常清楚的最小 server 骨架了。
参数校验为什么是 server 的责任之一?
因为 client 或模型都可能给错参数
例如:
bad_call = {"query_text": "退款政策"}
如果 server 直接执行,就可能报错或产生奇怪行为。
一个最小校验版本
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')
为什么这一步一定不能省?
因为 server 是能力边界守门人。 如果 server 不校验,整个工具系统就很难稳定。

读图提示
把 MCP Server 看成工具契约的守门人:它不仅暴露 list_tools,还要校验 call_tool 的参数、执行真实逻辑、统一返回结果,并把错误变成 client 能理解的结构。
一个更完整的最小 Server 版本
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'}

这个版本比上一版强在哪?
它已经具备了:
- 工具列出
- 参数校验
- 统一调用入口
- 统一错误返回
这已经非常接近真实工程里 server 的核心职责。
MCP Server 开发里最常见的坑
把业务逻辑和协议逻辑混在一起
结果会变成:
- 工具描述不清
- 扩展困难
- 调试困难
工具粒度太粗或太细
- 太粗:一个工具什么都干
- 太细:client 调用复杂度爆炸
返回结构不统一
有时返回文本,有时返回 dict,有时直接抛异常,后面会很难接。
怎么判断一个 MCP Server 设计得够不够好?
可以先问四个问题:
- client 能不能清楚知道有哪些工具
- 参数要求是不是明确
- 错误返回是不是统一
- 加新工具时结构会不会越来越乱
如果这四个问题都答得比较稳,server 设计通常就已经不错了。
小结
这一节最重要的不是“把一个类写出来”,而是理解:
MCP Server 的本质,是把一组可执行能力,用清晰可发现、可校验、可调用的方式暴露出来。
server 做得越清楚,client 侧越容易扩展,整个工具生态也越容易做大。
练习
- 给
BetterMCPServer再增加一个get_weather(city)工具。 - 为这个新工具补上参数校验逻辑。
- 想一想:工具粒度太粗和太细,各自会带来什么问题?
- 用自己的话解释:为什么说 MCP Server 开发的核心不只是“执行工具”,更是“暴露清晰边界”?