跳转到内容

9.5.4 MCP 服务器开发

  • 理解 MCP 服务器的最小职责边界
  • 学会定义工具描述、参数结构和调用入口
  • 理解为什么服务器开发的重点是“暴露能力”,而不是“把业务逻辑写死”
  • 看懂一个最小可运行的 Mock MCP Server

普通后端往往直接面向业务接口。 而 MCP 服务器更像:

把已有能力整理成一组可被客户端发现和调用的工具。

所以它的核心关注点通常是:

  • 有哪些工具
  • 工具怎样描述
  • 参数怎样校验
  • 结果怎样统一返回

MCP 服务器很像一个有前台的工具库管理员:

  • 客户端来问“你这里有什么工具”
  • 服务器列出能力清单
  • 客户端再说“我要用哪个”
  • 服务器按约定执行并返回结果

这和“直接把所有业务函数散着写”非常不一样。


至少要有:

  • 名称
  • 描述
  • 参数说明
  • 实际执行逻辑
search_docs_tool = {
"name": "search_docs",
"description": "搜索课程文档并返回相关内容",
"parameters": {
"query": {
"type": "string",
"description": "要搜索的关键词"
}
},
"required": ["query"]
}
print(search_docs_tool)

预期输出:

Terminal window
{'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)

预期输出:

Terminal window
{'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)

预期输出:

Terminal window
{'name': 'search_course_docs', 'description': '搜索课程 FAQ、政策和学习路线文档', 'parameters': {'query': {'type': 'string', 'description': '用户要查询的主题,比如 退款政策 或 证书'}}, 'required': ['query']}

这里更好的地方在于:

  • 工具边界更清楚
  • 参数语义更清楚
  • 客户端更容易正确使用

服务器的最小两项能力:列工具 + 调工具

Section titled “服务器的最小两项能力:列工具 + 调工具”

一个最小可用的 MCP 服务器,通常至少要能:

  1. 列出可用工具
  2. 接受某个工具调用
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())

预期输出:

Terminal window
[{'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": "退款政策是什么"}))

预期输出:

Terminal window
{'result': '课程购买后 7 天内且学习进度低于 20% 可退款。'}

这已经是一个非常清楚的最小服务器骨架了。


参数校验为什么是服务器的责任之一?

Section titled “参数校验为什么是服务器的责任之一?”

因为客户端或模型都可能给错参数

Section titled “因为客户端或模型都可能给错参数”

例如:

bad_call = {"query_text": "退款政策"}

如果服务器直接执行,就可能报错或产生奇怪行为。

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": "退款政策"}))

预期输出:

Terminal window
(True, 'ok')
(False, 'missing_query')

因为服务器是能力边界守门人。 如果服务器不校验,整个工具系统就很难稳定。

MCP 服务器工具契约图


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": "证书怎么获得"}))

预期输出:

Terminal window
[{'name': 'search_docs', 'description': '搜索课程文档', 'parameters': {'query': {'type': 'string'}}}]
{'result': '完成所有项目并通过测试后可获得证书。'}
{'error': 'missing_query'}

MCP 服务器校验与返回结果图

它已经具备了:

  • 工具列出
  • 参数校验
  • 统一调用入口
  • 统一错误返回

这已经非常接近真实工程里服务器的核心职责。


把业务逻辑和协议逻辑混在一起

Section titled “把业务逻辑和协议逻辑混在一起”

结果会变成:

  • 工具描述不清
  • 扩展困难
  • 调试困难
  • 太粗:一个工具什么都干
  • 太细:客户端调用复杂度爆炸

有时返回文本,有时返回 dict,有时直接抛异常,后面会很难接。


怎么判断一个 MCP 服务器设计得够不够好?

Section titled “怎么判断一个 MCP 服务器设计得够不够好?”

可以先问四个问题:

  1. 客户端能不能清楚知道有哪些工具
  2. 参数要求是不是明确
  3. 错误返回是不是统一
  4. 加新工具时结构会不会越来越乱

如果这四个问题都答得比较稳,服务器设计通常就已经不错了。


学完这一页,至少保留这张证据卡:

能力
服务器暴露的资源、Prompt 或工具
契约
schema、传输、权限和错误形式
调用轨迹
发现、调用、响应和失败处理
失败检查
架构不兼容、缺少认证、不安全工具或服务器错误
集成动作
在加入自主能力前先验证服务端契约

这一节最重要的不是“把一个类写出来”,而是理解:

MCP 服务器的本质,是把一组可执行能力,用清晰可发现、可校验、可调用的方式暴露出来。

服务器做得越清楚,客户端侧越容易扩展,整个工具生态也越容易做大。


  1. BetterMCPServer 再增加一个 get_weather(city) 工具。
  2. 为这个新工具补上参数校验逻辑。
  3. 想一想:工具粒度太粗和太细,各自会带来什么问题?
  4. 用自己的话解释:为什么说 MCP 服务器开发的核心不只是“执行工具”,更是“暴露清晰边界”?
参考实现与讲解
  1. 可以把 get_weather(city) 注册成一个带描述和 schema 的 tool,并返回类似 {city, condition, source} 的小型结构化结果。除非正文已经接入真实 API,否则用占位数据即可。
  2. 校验 city 是否存在、是否为非空字符串、是否不是异常大的输入。输入无效时要返回清晰错误,而不是静默猜测。
  3. 工具粒度太粗会隐藏关键选择,错误也难定位;粒度太细会迫使 Agent 规划大量小调用,增加延迟、路由错误和上下文噪声。
  4. MCP Server 开发的核心是暴露边界:工具做什么、接受什么输入、返回什么输出和错误、需要什么权限。执行工具只是契约的一部分。