1. 从零开发一个 MCP:为什么我建议你先搭 config.yaml 骨架
MCP(Model Context Protocol)是 Anthropic 提出的一套工具调用协议,说白了就是让大模型用统一格式去"点菜"——声明要调用哪个工具、传什么参数、拿回什么结果。它用 JSON Schema 描述工具接口,支持动态发现、参数校验和结果类型化,所以模型不用为每个外部系统单独写适配代码。适合谁?适合手里有一堆脚本、API、数据库查询逻辑,想让 AI 助手直接调用的开发者。Python 生态里目前上手最快的是 fastmcp,几行代码就能把一个普通函数注册成 MCP 工具。
但很多人第一次写 MCP 会踩同一个坑:工具函数写完了,启动脚本也跑起来了,结果接入客户端时发现工具没加载、参数对不上、路径写错。问题往往不在业务逻辑,而在项目骨架没搭好——工具注册散落在代码里,配置项硬编码在启动脚本里,改一个端口要翻三个文件。我试过把工具注册和运行参数全部收进一个 config.yaml,代码只负责读配置、注册、启动,后面加工具就是往 yaml 里加一段,改端口就是改一行。这篇就按这个思路,用 Python + fastmcp 搭一个可复用的最小骨架,包含 config.yaml、启动代码、本地验证和常见报错排查。
2. TaoToken 前置:MCP 服务要接的模型入口怎么准备
MCP 服务本身只是"工具提供方",它需要一个支持 MCP 的客户端(比如 hermes、Claude Code 这类)来调用。而客户端背后要连大模型,模型入口这块可以用 TaoToken 统一管理。它的 API 地址是 https://taotoken.net/api,兼容常见的 OpenAI 风格调用方式,MCP 客户端配置模型时把 base_url 指过去就行。
你需要先去控制台拿一个 API Key,然后按客户端要求填到配置里。这一步不复杂,但顺序别搞反:先把 MCP 服务跑起来、本地验证工具能被调用,再去配客户端。否则工具没通、模型也没通,报错会混在一起,排查起来很痛苦。
具体入口我列一下,按需取用:
- 拿 Key、管理额度:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=mcp_python_fastmcp
- 控制台总览:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=mcp_python_fastmcp
- 接入文档(看 base_url 和参数格式):https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=mcp_python_fastmcp
- 想先在网页里试模型对话:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=mcp_python_fastmcp
- 长期跑编码类 Agent,看 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=mcp_python_fastmcp
注意:MCP 服务监听的是本地端口,客户端连的是模型 API,两者是两条独立的链路。先把本地这条跑通,再去配模型那条。
3. 可复制配置:config.yaml 骨架与 fastmcp 启动代码
3.1 项目结构
先定目录,后面所有路径都基于它:
mcp-demo/ ├── config.yaml ├── server.py ├── tools/ │ ├── __init__.py │ └── domain_ip.py └── requirements.txtrequirements.txt 内容:
fastmcp>=2.0.0 requests>=2.31.0 pyyaml>=6.0安装:
pip install -r requirements.txt3.2 config.yaml:把工具注册和运行参数都收进来
这个文件是整个骨架的核心。它分两块:server 段管运行参数(host、port、transport、base_path),tools 段管要注册哪些工具模块。加工具只改这里,不动 server.py。
server: name: "domain-ip-lookup" transport: "streamable-http" host: "0.0.0.0" port: 9069 base_path: "/mcp" tools: - module: "tools.domain_ip" enabled: true # 以后加工具就在这里追加一行 # - module: "tools.weather" # enabled: true字段说明用表格对照一下:
| 字段 | 作用 | 常用值 |
|---|---|---|
| server.name | MCP 服务实例名,客户端加载列表里显示 | 自定义字符串 |
| server.transport | 传输方式 | streamable-http / stdio |
| server.host | 监听地址 | 0.0.0.0 或 127.0.0.1 |
| server.port | 监听端口 | 9069 等未占用端口 |
| server.base_path | HTTP 访问路径 | /mcp |
| tools[].module | 工具模块的导入路径 | tools.xxx |
| tools[].enabled | 是否启用该模块 | true / false |
3.3 工具模块:tools/domain_ip.py
工具函数本身保持纯粹,只关心业务,不关心怎么启动。用@mcp.tool装饰器注册,description 写清楚,模型靠它判断什么时候调用。
import socket import requests from fastmcp import FastMCP def register(mcp: FastMCP) -> None: @mcp.tool( description="查询指定域名的IP地址以及IP的物理归属地,包含国家、省份、城市、运营商信息" ) async def lookup_domain_ip_location(domain: str) -> str: """ Args: domain: 要查询的域名,例如 baidu.com,无需带 http 前缀 """ try: ip = socket.gethostbyname(domain) resp = requests.get( f"http://ip-api.com/json/{ip}", params={"lang": "zh-CN"}, timeout=5, ) resp.raise_for_status() data = resp.json() if data["status"] != "success": return f"查询失败:{data.get('message', '未知错误')}" return ( f"域名:{domain}\n" f"解析IP:{ip}\n" f"国家:{data.get('country', '未知')}\n" f"省份:{data.get('regionName', '未知')}\n" f"城市:{data.get('city', '未知')}\n" f"运营商:{data.get('isp', '未知')}" ) except socket.gaierror: return f"域名解析失败:无法解析 {domain},请检查域名是否正确" except requests.RequestException as e: return f"归属地接口请求异常:{str(e)}" except Exception as e: return f"查询异常:{str(e)}"tools/init.py 留空即可。
3.4 server.py:读配置、动态注册、启动
这段代码只做三件事:读 yaml、按 tools 列表动态导入并调用各模块的 register、按 server 段启动。加工具不用改它。
import importlib import yaml from fastmcp import FastMCP def load_config(path: str = "config.yaml") -> dict: with open(path, "r", encoding="utf-8") as f: return yaml.safe_load(f) def build_server(cfg: dict) -> FastMCP: server_cfg = cfg["server"] mcp = FastMCP(server_cfg["name"]) for item in cfg.get("tools", []): if not item.get("enabled", True): continue module = importlib.import_module(item["module"]) if not hasattr(module, "register"): raise AttributeError(f"模块 {item['module']} 缺少 register(mcp) 函数") module.register(mcp) return mcp if __name__ == "__main__": cfg = load_config() mcp = build_server(cfg) s = cfg["server"] mcp.run( transport=s["transport"], host=s["host"], port=s["port"], base_path=s.get("base_path", "/mcp"), )启动:
python server.py看到服务监听在 0.0.0.0:9069 就说明起来了。这套骨架的好处是:工具模块之间互不干扰,config.yaml 就是唯一的"注册表",团队协作时谁加工具谁改自己那行。
4. 验证请求:确认工具真的被加载了
服务跑起来不等于工具注册成功。fastmcp 的 streamable-http 传输下,可以用 MCP 的 initialize 和 tools/list 请求来验证。先发一个初始化请求:
curl -s -X POST http://127.0.0.1:9069/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "curl-test", "version": "1.0"} } }'返回里会带 session 相关信息。接着列工具:
curl -s -X POST http://127.0.0.1:9069/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{ "jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {} }'如果返回的 JSON 里 tools 数组包含lookup_domain_ip_location,说明工具注册成功。再调一次工具本身:
curl -s -X POST http://127.0.0.1:9069/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{ "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "lookup_domain_ip_location", "arguments": {"domain": "baidu.com"} } }'正常会返回域名、解析 IP、国家、省份、城市、运营商。到这一步,MCP 服务这条链路就通了。之后在客户端(比如 hermes)的 config.yaml 里加:
mcp_servers: domain-ip-lookup-server: url: http://127.0.0.1:9069/mcp启动客户端会话,在 MCP Servers 加载列表里能看到domain-ip-lookup-server,就可以在聊天框里让它调用这个工具干活了。
5. 本篇常见错排查:config.yaml 与 fastmcp 的坑
5.1 工具没出现在 tools/list 里
最常见的原因是 config.yaml 里 module 路径写错,或者模块没有register函数。server.py 里我加了 hasattr 检查,如果模块缺 register 会直接抛 AttributeError,启动时就报出来,比运行到一半才发现好。另外注意 enabled 字段,写成false或漏写导致被跳过。
5.2 端口被占用或客户端连不上
OSError: [Errno 98] Address already in use说明 9069 被占了,改 config.yaml 里的 port 即可,不用动代码。客户端连不上时先确认 host:本地客户端用 127.0.0.1,跨机器访问才用 0.0.0.0 并检查防火墙。base_path 要和客户端 url 里的路径一致,默认 /mcp。
5.3 工具调用返回"查询异常"
ip-api.com 有频率限制,短时间大量请求会被限流。生产环境建议换带 Key 的归属地接口,或者加本地缓存。另外socket.gethostbyname只支持 IPv4,遇到纯 IPv6 域名会解析失败,需要的话换成socket.getaddrinfo。
5.4 异步函数里用同步 requests 阻塞
fastmcp 的工具函数可以是 async,但里面调requests.get是同步阻塞的,高并发下会拖慢事件循环。量小无所谓,量大建议换httpx.AsyncClient。这是骨架阶段容易忽略、上线后才暴露的问题。
5.5 yaml 缩进错误导致读不到配置
yaml 对缩进敏感,tools 列表项前面的短横线和缩进层级要对齐。读配置失败时先单独跑一段python -c "import yaml; print(yaml.safe_load(open('config.yaml')))"确认能解析。
6. 把骨架用起来:下一步怎么扩展
这套骨架跑通后,加新工具就是三步:在 tools/ 下新建模块、写 register 函数、在 config.yaml 的 tools 列表加一行。server.py 完全不用动。如果你打算长期跑编码类 Agent、频繁调用 MCP 工具,可以看下 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=mcp_python_fastmcp
模型入口和 Key 管理在控制台:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=mcp_python_fastmcp
接入参数和 base_url 格式以文档为准:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=mcp_python_fastmcp
想先在网页里验证模型能不能正常对话,再去接 MCP:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=mcp_python_fastmcp
最后提醒一句:config.yaml 里的 base_path 和客户端 url 路径必须一致,这个坑我见过太多次,工具明明注册成功,客户端就是加载不出来,查半天发现是路径差了一个斜杠。