第3篇:原生SDK极简入门
这篇只做一件事:把第2篇里的一个 FastMCP 工具,改写为原生SDK版本。
1. 你需要先知道的 JSON-RPC
工具调用时,客户端发这样的请求:
{"jsonrpc": "2.0","id": 1,"method": "tools/call","params": {"name": "search_recipes_by_ingredient","arguments": { "ingredient": "番茄" }}
}
服务器要回这样的响应(内容放在 result.content[0].text
里,通常是字符串化的 JSON):
{"jsonrpc": "2.0","id": 1,"result": {"content": [{ "type": "text", "text": "{\"success\": true, \"recipes\": [], \"count\": 0}" }]}
}
记住两点:
- 参数来自
request.params.arguments
- 返回值需要构造成
CallToolResult(content=[TextContent(...)])
2. 只改一个工具:FastMCP → 原生SDK
沿用第2篇的内部函数(在 app/models.py
):
_search_recipes_by_ingredient(ingredient: str) -> Dict
FastMCP 版本(第2篇)
@mcp.tool()
def search_recipes_by_ingredient(ingredient: str) -> Dict:"""根据食材查询食谱"""return _search_recipes_by_ingredient(ingredient)
原生SDK 版本(极简)
# 需要:pip install mcp
import json
from mcp.server.models import CallToolRequest, CallToolResult
from mcp.types import TextContentfrom .models import _search_recipes_by_ingredient # 直接复用第2篇内部函数class SearchRecipesTool:async def handle(self, request: CallToolRequest) -> CallToolResult:# 1) 手动取参数ingredient = (request.params.arguments or {}).get("ingredient", "").strip()# 2) 最小的参数校验if not ingredient:return CallToolResult(content=[TextContent(type="text", text=json.dumps({"success": False, "error": "缺少食材参数"}, ensure_ascii=False))])# 3) 业务逻辑:复用第2篇函数result = _search_recipes_by_ingredient(ingredient)# 4) 按MCP要求封装响应return CallToolResult(content=[TextContent(type="text", text=json.dumps(result, ensure_ascii=False))])
就这么简单:手动取参、调用旧逻辑、封装标准返回。
3. 为什么原生SDK要"这么写"
显式参数:你清楚看到参数来自哪里,怎么校验
- FastMCP:
def search(ingredient: str)
- 魔法注入 - 原生SDK:
request.params.arguments.get("ingredient")
- 明确来源
结构化返回:约束你把数据放进 content
,和客户端约定一致
- FastMCP:
return {"success": True}
- 直接返回 - 原生SDK:
CallToolResult(content=[TextContent(...)])
- 标准封装
错误可控:返回结构里能带上错误信息,前后端更稳
- FastMCP:异常或None可能导致不明确的错误
- 原生SDK:统一的错误格式
{"success": False, "error": "..."}
适用场景对比表
场景 | FastMCP | 原生SDK |
---|---|---|
快速原型 | 推荐 | 过度 |
高性能需求 | 一般 | 推荐 |
定制协议 | 不支持 | 推荐 |
总结
原生SDK的本质就是:显式控制每个细节,换取更多的灵活性。