1. 从零搭一个轻量 MCP 服务,为什么选 Python + Flask/FastAPI
MCP 服务说白了就是一层“能力网关”:把模型调用、工具注册、状态上报这些动作收敛到几个 HTTP 接口里,让上层应用不用关心底层模型是谁、Key 怎么轮换。用 Python 搭这类服务的好处是生态全、上手快,Flask 和 FastAPI 都能在几十行代码内跑通一个可用的原型。如果你手头有一堆小工具想暴露给模型调用,又不想引入重型框架,这套组合足够撑起中小型场景。
我这次的目标很明确:用 Flask 做 Web 层(异步要求不高的场景它更省心),Gunicorn 做进程管理,模型能力统一走 TaoToken 的 API 通道。这样做的直接收益是——你只需要维护一个 Base URL 和一个 Key,切换模型时改一个 Model ID 就行,不用在代码里到处塞不同厂商的 SDK。
适合谁看:已经会写 Python 函数、想把自己的工具接进模型工作流的开发者;正在做 Agent 原型、需要一个稳定工具注册中心的同学;以及被多厂商 Key 管理搞烦了、想统一出口的团队。
整篇文章按“能跑起来”的标准写,每一步都有可复制的代码和命令。项目结构保持扁平,不搞多层目录,快速落地优先。下面从环境准备开始,一路到 Gunicorn 启动和 curl 验证。
2. TaoToken 前置准备:统一 Key 与 API 通道怎么配
在写业务代码之前,先把模型通道这件事定下来。TaoToken 在这里扮演的角色是“统一入口”:你拿到一个 API Key,配一个 Base URL,就能在代码里通过改 Model ID 来切换不同模型。对 MCP 服务来说,这意味着工具注册逻辑和模型调用逻辑可以解耦——工具层只管收发 JSON,模型层只认一个出口。
先做三件事。
第一,拿 Key。访问 https://taotoken.net/api-keys 生成一个 API Key,复制出来存好。这个 Key 后面会写进.env,不要硬编码进代码。
第二,确认 Base URL。API 通道地址是https://taotoken.net/api,注意这个地址不带任何查询参数,直接作为 OpenAI 兼容接口的 base 使用。
第三,选 Model ID。在模型对话页面 https://taotoken.net/model-chat 可以先试跑一下,确认你要用的模型能正常返回,再把对应的 Model ID 记下来。常见的做法是先用一个通用模型跑通链路,再换成更专用的。
把这三个值写进项目根目录的.env:
# .env TAOTOKEN_API_KEY=sk-你的key TAOTOKEN_BASE_URL=https://taotoken.net/api TAOTOKEN_MODEL_ID=你的模型ID MCP_PORT=8000 MCP_ENV=dev REGISTRY_EXPIRE=3600这里有个容易踩的坑:Base URL 结尾不要多加/v1或斜杠,不同 SDK 对路径拼接的处理不一样,多写一段很容易拼出//v1/chat/completions这种畸形路径,报 404。统一用https://taotoken.net/api,让 SDK 自己去拼。
依赖清单也一并定下来,版本别乱搭:
pip install flask==2.3.3 python-dotenv==1.0.0 gunicorn==21.2.0 requests==2.31.0如果你更倾向 FastAPI,把 flask 换成fastapi==0.110.0 uvicorn==0.29.0,其余不变。本文主体用 Flask 演示,FastAPI 的差异点会在对应位置标注。
Key 和通道准备好之后,MCP 服务本身就不需要再关心“模型从哪来”了。工具注册、状态上报这些接口只处理业务数据,真正要调模型时,统一走一个封装好的 client。这样后面换模型、加模型,都只动配置不动逻辑。
3. 可复制配置:项目结构、MCP 工具注册与 Gunicorn 启动
这一节是全文的核心,所有代码都可以直接复制。项目结构保持三个文件:
mcp-service/ ├── .env # 环境配置 ├── core.py # MCP 核心逻辑 + 模型调用封装 └── main.py # Flask 入口 + 路由3.1 core.py:核心逻辑与模型通道封装
先写core.py。这里做两件事:一是 MCP 的配置管理、服务注册、状态上报;二是把 TaoToken 的调用封装成一个函数,供工具层使用。
# core.py import os import time import requests from dotenv import load_dotenv load_dotenv() TAOTOKEN_API_KEY = os.getenv("TAOTOKEN_API_KEY") TAOTOKEN_BASE_URL = os.getenv("TAOTOKEN_BASE_URL", "https://taotoken.net/api") TAOTOKEN_MODEL_ID = os.getenv("TAOTOKEN_MODEL_ID") REGISTRY_EXPIRE = int(os.getenv("REGISTRY_EXPIRE", "3600")) class MCPCore: def __init__(self, expire=3600): self.configs = {} self.services = {} self.status = {} self.expire = expire def set_config(self, node_id, conf): self.configs[node_id] = conf return True def get_config(self, node_id): return self.configs.get(node_id) def register_service(self, node_id, service_info): self.services[node_id] = { "info": service_info, "update_time": int(time.time()) } return True def report_status(self, node_id, status_data): status_data["update_time"] = int(time.time()) self.status[node_id] = status_data return True def list_services(self): now = int(time.time()) alive = {} for node_id, item in self.services.items(): if now - item["update_time"] <= self.expire: alive[node_id] = item return alive mcp_core = MCPCore(expire=REGISTRY_EXPIRE) def call_model(prompt, model_id=None, timeout=30): """统一模型调用出口,所有工具都走这里""" if not TAOTOKEN_API_KEY: raise RuntimeError("TAOTOKEN_API_KEY 未配置") url = f"{TAOTOKEN_BASE_URL}/v1/chat/completions" headers = { "Authorization": f"Bearer {TAOTOKEN_API_KEY}", "Content-Type": "application/json" } payload = { "model": model_id or TAOTOKEN_MODEL_ID, "messages": [{"role": "user", "content": prompt}] } resp = requests.post(url, json=payload, headers=headers, timeout=timeout) resp.raise_for_status() data = resp.json() return data["choices"][0]["message"]["content"]注意call_model里的 URL 拼接:TAOTOKEN_BASE_URL是https://taotoken.net/api,后面接/v1/chat/completions,拼出来是https://taotoken.net/api/v1/chat/completions。这是 OpenAI 兼容接口的标准路径。如果你用的是别的 SDK,路径拼接规则可能不同,以 SDK 文档为准。
3.2 main.py:Flask 路由与工具注册
main.py负责暴露 HTTP 接口。除了配置、注册、状态三个基础接口,再加一个“工具调用”接口,演示怎么把模型能力接进来。
# main.py import os from flask import Flask, request, jsonify from dotenv import load_dotenv from core import mcp_core, call_model load_dotenv() app = Flask(__name__) MCP_PORT = int(os.getenv("MCP_PORT", "8000")) @app.route("/mcp/config/<node_id>", methods=["POST"]) def set_node_config(node_id): conf = request.get_json(silent=True) if not conf: return jsonify({"code": 400, "msg": "参数为空"}), 400 mcp_core.set_config(node_id, conf) return jsonify({"code": 200, "msg": "配置设置成功"}) @app.route("/mcp/config/<node_id>", methods=["GET"]) def get_node_config(node_id): conf = mcp_core.get_config(node_id) if conf is None: return jsonify({"code": 404, "msg": "配置不存在"}), 404 return jsonify({"code": 200, "data": conf}) @app.route("/mcp/registry/<node_id>", methods=["POST"]) def register_service(node_id): service_info = request.get_json(silent=True) or {} required = ["service_name", "service_port", "version"] if not all(k in service_info for k in required): return jsonify({"code": 400, "msg": "缺少必传字段"}), 400 mcp_core.register_service(node_id, service_info) return jsonify({"code": 200, "msg": "注册成功"}) @app.route("/mcp/registry", methods=["GET"]) def list_services(): return jsonify({"code": 200, "data": mcp_core.list_services()}) @app.route("/mcp/monitor/<node_id>", methods=["POST"]) def report_status(node_id): status_data = request.get_json(silent=True) if not status_data: return jsonify({"code": 400, "msg": "状态数据为空"}), 400 mcp_core.report_status(node_id, status_data) return jsonify({"code": 200, "msg": "状态上报成功"}) @app.route("/mcp/tool/echo", methods=["POST"]) def tool_echo(): """示例工具:把输入交给模型处理并返回""" body = request.get_json(silent=True) or {} prompt = body.get("prompt") if not prompt: return jsonify({"code": 400, "msg": "prompt 不能为空"}), 400 try: result = call_model(prompt, model_id=body.get("model_id")) return jsonify({"code": 200, "data": result}) except Exception as e: return jsonify({"code": 500, "msg": str(e)}), 500 if __name__ == "__main__": app.run(host="0.0.0.0", port=MCP_PORT, debug=True)如果你用 FastAPI,路由写法换成装饰器@app.post("/mcp/config/{node_id}"),请求体用 Pydantic 模型接收,其余逻辑不变。FastAPI 的自动文档在/docs,调试时比 Flask 方便一些。
3.3 Gunicorn 启动配置
开发环境直接python main.py就行,但生产环境要用 Gunicorn。在项目根目录建一个gunicorn.conf.py:
# gunicorn.conf.py import multiprocessing bind = "0.0.0.0:8000" workers = multiprocessing.cpu_count() * 2 + 1 worker_class = "sync" timeout = 60 accesslog = "-" errorlog = "-" loglevel = "info"启动命令:
gunicorn -c gunicorn.conf.py main:appmain:app的意思是“main 模块里的 app 对象”。Flask 和 FastAPI 都适用这个写法。worker 数量按 CPU 核数算,2 * cores + 1是个经验值,I/O 密集可以适当调大。
到这里,一个可运行的 MCP 服务就搭好了。下一节验证它是否真的能跑通。
4. 验证请求:curl 调用与成功结果确认
服务起来之后,按顺序验证四个动作:配置写入、服务注册、状态上报、模型调用。每一步都有明确的返回,任何一步失败都能快速定位。
先启动服务:
python main.py看到Running on http://0.0.0.0:8000就说明起来了。
动作一:写入节点配置
curl -X POST http://localhost:8000/mcp/config/node-01 \ -H "Content-Type: application/json" \ -d '{"model_path": "/opt/models", "max_tokens": 2048}'预期返回:
{"code": 200, "msg": "配置设置成功"}动作二:注册服务
curl -X POST http://localhost:8000/mcp/registry/node-01 \ -H "Content-Type: application/json" \ -d '{"service_name": "echo-tool", "service_port": 8000, "version": "1.0.0"}'预期返回{"code": 200, "msg": "注册成功"}。如果漏了version字段,会返回 400 和“缺少必传字段”,这是校验逻辑在起作用。
动作三:上报状态
curl -X POST http://localhost:8000/mcp/monitor/node-01 \ -H "Content-Type: application/json" \ -d '{"cpu": 12.5, "memory": 256, "response_time": 88}'预期返回{"code": 200, "msg": "状态上报成功"}。
动作四:调用模型工具
这是最关键的一步,验证 TaoToken 通道是否打通:
curl -X POST http://localhost:8000/mcp/tool/echo \ -H "Content-Type: application/json" \ -d '{"prompt": "用一句话解释什么是 MCP 服务"}'如果一切正常,你会拿到类似这样的返回:
{"code": 200, "data": "MCP 服务是一种把模型能力封装成标准接口的中间层,让上层应用通过统一入口调用不同模型。"}拿到这个返回,说明整条链路——Flask 路由 → MCPCore → call_model → TaoToken API → 模型返回——全部通了。
再验证一下服务列表接口:
curl http://localhost:8000/mcp/registry应该能看到node-01在存活列表里。如果超过REGISTRY_EXPIRE秒没有上报,它会自动从列表里消失,这是过期清理逻辑。
用 Gunicorn 启动后再跑一遍上面的 curl,确认生产模式下行为一致:
gunicorn -c gunicorn.conf.py main:appGunicorn 模式下debug=True不生效,热重载关闭,这是正常的。日志会打到标准输出,方便排查。
5. 常见报错排查:401、local proxy failed、reading choices 怎么解
这一节按真实报错来。下面这几个是我在搭这类服务时实际遇到过的,每个都给出定位方法和修复动作。
报错一:401 Unauthorized
返回体类似:
{"code": 500, "msg": "401 Client Error: Unauthorized for url: https://taotoken.net/api/v1/chat/completions"}原因基本是 Key 没配或配错。检查三处:.env里TAOTOKEN_API_KEY是否填了真实 Key;load_dotenv()是否在读取环境变量之前执行;Key 是否有多余空格或换行。修复后重启服务。如果用的是 Gunicorn,改完.env必须重启进程,环境变量不会热加载。
报错二:local proxy failed / Connection refused
这类报错通常出现在请求根本没发出去的时候。先确认TAOTOKEN_BASE_URL写的是https://taotoken.net/api,没有多余路径。再确认本机网络能正常访问该地址:
curl -I https://taotoken.net/api如果这条命令都失败,说明是网络层问题,不是代码问题。检查 DNS 和出网策略即可。
报错三:reading 'choices' / KeyError: 'choices'
完整报错类似:
KeyError: 'choices'或者:
TypeError: 'NoneType' object is not subscriptable这说明resp.json()返回的结构里没有choices字段。常见原因是模型返回了错误信息而不是正常结果,比如 Model ID 写错、额度不足、请求体格式不对。修复动作:在call_model里把原始返回打出来看:
data = resp.json() print("RAW RESPONSE:", data) return data["choices"][0]["message"]["content"]跑一次就能看到真实返回。如果是{"error": {"message": "model not found"}},那就是 Model ID 的问题,去模型对话页面确认正确的 ID。
报错四:OAuth / 认证方式不匹配
如果你用的是 Claude Code 或 Codex 这类工具,可能会遇到 OAuth 相关的报错。这类工具通常需要配置三件套:Base URL、API Key、Model ID。以 Codex 的auth.json为例,配置结构大致是:
{ "base_url": "https://taotoken.net/api", "api_key": "sk-你的key", "model": "你的模型ID" }三个字段缺一不可。Claude Code 的配置类似,在 settings 里填 Base URL 和 Key,Model ID 单独指定。如果只填了 Key 没填 Base URL,就会走默认端点导致认证失败。
报错五:端口占用
OSError: [Errno 98] Address already in use查占用进程:
netstat -tlnp | grep 8000要么杀掉进程,要么改.env里的MCP_PORT。Gunicorn 的bind也要同步改。
报错六:跨域问题
前端调用时报 CORS 错误。Flask 加flask-cors:
pip install flask-cors==4.0.0from flask_cors import CORS CORS(app, resources={r"/mcp/*": {"origins": "*"}})FastAPI 用CORSMiddleware,配置方式类似。生产环境把origins换成具体域名,别用*。
报错七:依赖冲突
Pydantic v2 和旧版 FastAPI 不兼容,报pydantic.errors.PydanticImportError。修复方式是升级 FastAPI 到 0.100 以上:
pip install "fastapi>=0.100.0" "pydantic>=2.0"Flask 这边一般不会有这个问题,但requests和urllib3的版本也要注意,太旧会报 SSL 相关错误,升级到本文依赖清单里的版本即可。
排查的核心思路就一条:先看原始返回,再看配置,最后看网络。大部分问题在第一步就能定位。
6. 把 MCP 服务接进长期工作流:Coding Plan 与后续扩展
服务跑通之后,下一步是让它真正进入你的日常工作流。如果你只是偶尔调一下模型,现在的形态就够了;但如果你要把它当成 Agent 的工具后端长期跑,有几个方向可以继续做。
第一,把内存存储换成 Redis。现在MCPCore用的是字典,进程重启数据就没了。多 worker 模式下,每个 worker 有独立内存,注册信息不共享。换成 Redis 后,set_config、register_service、report_status都改成读写 Redis,多进程就能看到同一份数据。改动量不大,接口签名不用变。
第二,加一个定时清理任务。现在过期服务只在list_services被调用时才过滤,不会主动删除。可以用APScheduler或简单的后台线程,每隔一段时间清理一次过期节点,避免内存无限增长。
第三,把工具注册做成动态的。现在tool_echo是写死的路由,每加一个工具就要改代码。更优雅的做法是维护一个工具注册表,用统一的/mcp/tool/<tool_name>路由分发,工具函数通过装饰器注册。这样新增工具只需要加一个函数,不用动路由。
第四,接入 Coding Plan 做长期编码任务。如果你的 MCP 服务要支撑 Agent 持续跑代码生成、代码审查这类任务,可以了解一下 Coding Plan:https://taotoken.net/coding-plan 。它适合需要长时间、高频调用模型的场景,配合你搭好的 MCP 服务,可以把工具调用和模型调用统一在一个通道里管理。
第五,日志和监控。现在日志用的是 Flask 默认输出,生产环境建议换成结构化日志,把每次模型调用的耗时、token 消耗、错误码都记下来。这样排查问题时不用靠猜,直接看日志就能定位。
最后提醒一个实操细节:Gunicorn 的 worker 数量不要盲目调大。模型调用是 I/O 密集操作,worker 太多会导致并发请求把上游打满,反而变慢。先用2 * cores + 1跑一段时间,观察响应时间和错误率,再决定要不要调整。如果发现大量请求排队,优先考虑加缓存或做请求合并,而不是无脑加 worker。
整套代码加起来不到 200 行,但覆盖了 MCP 服务的核心能力:配置管理、服务注册、状态上报、模型调用。你可以在这个骨架上按需扩展,不用推倒重来。