资讯详情

第1章 MCP设计:用Python+Flask从零搭建工具调用服务,TaoToken统一Key接入实战

📅 2026/10/8 10:12:00 | 华诺云谱 👁 阅读
第1章 MCP设计:用Python+Flask从零搭建工具调用服务,TaoToken统一Key接入实战
1. 为什么要在本地搭一个 MCP 工具调用服务MCPModel Context Protocol这两年被讨论得很多但真正落到代码层面很多同学会卡在同一个地方大模型怎么知道本地有哪些函数可以调参数从哪来调用结果怎么回传如果每次都要手写一大段 JSON Schema维护成本会高到让人放弃。我自己的做法是把「函数注册」这件事做成类似 Flask 路由的体验。Flask 用app.route(/xxx)把一个函数变成 HTTP 接口那我们同样可以用registry.tool(nameplus)把一个普通 Python 函数变成大模型可调用的工具。这样你写业务逻辑时只关心函数本身Schema 推导、参数校验、执行分发全部交给注册中心。这篇文章要交付的是一条完整链路用 Python Flask 起一个本地 MCP Server暴露工具接口用inspect自动推导参数类型生成 OpenAI 标准工具描述再通过 TaoToken 的统一 Key 和 API 通道完成鉴权与请求转发让大模型真正调用到你本地的plus函数。适合已经会基本 Python 语法、想搞懂「工具调用到底怎么跑通」的开发者。全程可复制最后用 curl 验证一次端到端调用。核心检索词先明确MCP 工具调用服务、Python Flask 本地 MCP Server、大模型 function calling、TaoToken 统一 Key 接入。这几个词会贯穿全文你按这个思路搜也能找到同方向的资料。先说清楚整体架构避免后面看代码时迷路。整个系统分三层第一层是工具层就是你写的普通 Python 函数比如加法、查天气、读文件。它们不知道大模型的存在就是纯函数。第二层是注册与协议层也就是ToolRegistry。它负责把函数签名翻译成 JSON Schema维护name - {func, definition}的映射表并提供execute方法按名字调用。第三层是服务层Flask 提供两个路由一个/tools返回所有工具定义给大模型看一个/call接收模型返回的工具名和参数并执行。对外再通过 TaoToken 的 API 通道统一鉴权这样你的 Key 不用散落在各个客户端里。为什么强调「本地」因为工具往往要访问你本机的文件、数据库、内网服务放到公网既不安全也没必要。本地起 Flask通过统一的 API 通道转发请求是成本和安全性都比较平衡的方案。还有一个容易被忽略的点工具描述的质量直接决定模型调用准确率。inspect能推导出类型但推导不出「这个参数到底是什么意思」。所以description字段一定要认真写后面我会给出带参数说明的增强版写法。很多同学抱怨模型老是传错参数八成是描述太潦草。2. TaoToken 统一 Key 的前置准备在写 Flask 之前先把鉴权通道理清楚否则后面调不通你会怀疑是代码问题。TaoToken 在这里扮演的角色是「统一入口」你只需要在它那边拿一个 Key配置好 Base URL就能让不同客户端Claude Code、Cline、Codex 等走同一条通道访问模型不用每个工具单独维护一套凭证。你需要准备三样东西我称之为「三件套」缺一不可Base URLhttps://taotoken.net/apiAPI Key在控制台创建形如sk-开头的一串字符Model ID比如claude-sonnet-4-5这类具体模型标识获取 Key 的入口在控制台的 API Keys 页面创建后立刻复制保存页面刷新后就不再完整显示。如果你用的是 Claude Code 这类命令行工具还需要在它的配置文件里填 Base URL 和 Key如果是 Cline 这类编辑器插件则在 MCP 或 Provider 设置里填。不管哪种客户端本质都是这三件套。这里有个关键认知TaoToken 不是让你绕过什么而是把「多个模型、多个客户端、多个 Key」收敛成「一个 Key 一个 Base URL」。对本地 MCP Server 来说你只需要在转发请求时带上这个 Key服务端就能识别你的身份并路由到对应模型。配置时最容易踩的坑是 Base URL 写错。注意 API 地址是https://taotoken.net/api不要自己加/v1之类的后缀具体路径以接入文档为准。另一个坑是 Key 前后带了空格或换行复制到配置文件后请求直接 401肉眼还看不出来。建议用echo -n 你的key | wc -c检查长度是否符合预期。如果你打算长期做编码类 Agent 任务可以考虑 Coding Plan它在高频调用场景下更划算只是临时验证模型能力用模型对话页面就够了。这两个入口后面 CTA 部分我会再给一次现在先把环境准备好。环境依赖很简单一个requirements.txt搞定flask3.0.3 requests2.32.3Python 版本建议 3.10 以上因为get_type_hints对X | None这种新语法支持更好。装完依赖后目录结构建议这样组织后面代码都按这个路径来mcp-demo/ ├── app.py # Flask 入口 ├── core/ │ ├── __init__.py │ └── tool_provider.py # ToolRegistry ├── tools/ │ ├── __init__.py │ └── tool_1.py # 具体工具 └── requirements.txt这样分层的好处是工具可以按业务拆文件main里按需 import和 Flask 的蓝图思路一致。3. 可复制的 Flask 路由与工具注册配置这一节是全文核心直接给可运行代码。先写注册中心core/tool_provider.py它负责把函数变成工具定义import inspect import json from typing import Dict, Optional, Callable, get_type_hints, List class ToolRegistry: 独立注册中心提供 registry.tool(name, desc) 装饰器。 自动从函数签名推导 JSON Schema。 def __init__(self): self._tools: Dict[str, dict] {} def tool(self, name: Optional[str] None, description: str ): def decorator(func: Callable): nonlocal name if name is None: name func.__name__ sig inspect.signature(func) type_hints get_type_hints(func) if hasattr(func, __annotations__) else {} properties {} required [] for param_name, param in sig.parameters.items(): if param_name in (self, cls): continue param_type type_hints.get(param_name, str) json_type self._python_type_to_json(param_type) properties[param_name] { type: json_type, description: f{param_name} argument, } if param.default is inspect.Parameter.empty: required.append(param_name) else: properties[param_name][default] param.default parameters_schema { type: object, properties: properties, required: required, } tool_def { type: function, function: { name: name, description: description or func.__doc__ or , parameters: parameters_schema, }, } self._tools[name] {func: func, definition: tool_def} return func return decorator staticmethod def _python_type_to_json(py_type) - str: if py_type is str: return string elif py_type is int: return integer elif py_type is float: return number elif py_type is bool: return boolean elif py_type is list: return array elif py_type is dict: return object return string def get_tool_definitions(self) - List[dict]: return [t[definition] for t in self._tools.values()] def has(self, name: str) - bool: return name in self._tools def execute(self, name: str, arguments: dict) - str: if not self.has(name): raise KeyError(fTool {name} not found in registry) func self._tools[name][func] try: result func(**arguments) if not isinstance(result, str): result json.dumps(result, ensure_asciiFalse) return result except Exception as e: return fError executing tool {name}: {str(e)} registry ToolRegistry()重点看tool装饰器和self._tools。self._tools的结构是name - {func: ..., definition: ...}一个存函数本体一个存给模型看的定义。tool装饰器里用inspect.signature拿到参数列表用get_type_hints拿到类型标注再映射成 JSON Schema 的类型。没有默认值的参数进required有默认值的写进default。接着写具体工具tools/tool_1.pyfrom core.tool_provider import registry registry.tool(nameplus, descriptionPlus two integers) def add(a: int, b: int) - int: return a b registry.tool(nameget_weather, description查询指定城市的天气参数 city 为城市名) def get_weather(city: str) - dict: fake_db {北京: 晴 26℃, 上海: 多云 24℃} return {city: city, weather: fake_db.get(city, 未知)}注意get_weather的description写清楚了参数含义这比plus那种简单描述更利于模型判断。实测下来描述里带上参数说明模型传错参数的概率明显下降。然后是 Flask 入口app.py两个路由from flask import Flask, request, jsonify from core.tool_provider import registry import tools.tool_1 # noqa: F401 按需导入即完成注册 app Flask(__name__) app.route(/tools, methods[GET]) def list_tools(): return jsonify({tools: registry.get_tool_definitions()}) app.route(/call, methods[POST]) def call_tool(): payload request.get_json(forceTrue) name payload.get(name) arguments payload.get(arguments, {}) if not name: return jsonify({error: missing tool name}), 400 try: result registry.execute(namename, argumentsarguments) return jsonify({tool: name, result: result}) except KeyError as e: return jsonify({error: str(e)}), 404 if __name__ __main__: app.run(host127.0.0.1, port5000, debugTrue)import tools.tool_1这一行就是「按需导入」的关键和 Flask 注册蓝图一个道理导入即注册。启动后/tools返回所有工具定义/call执行指定工具。如果你要把这个服务接到 TaoToken 通道上做转发可以在app.py里加一个转发路由配置用 JSON 片段管理路径放在config/taotoken.json{ base_url: https://taotoken.net/api, api_key: sk-你的Key, model: claude-sonnet-4-5, timeout: 60 }读取配置后用requests.post把模型返回的tool_calls解析出来再回调本地/call。这样模型负责决策调哪个工具本地负责执行职责清晰。4. 验证请求与成功结果代码写完必须验证否则你不知道 Schema 推导对不对。先启动服务python app.py看到Running on http://127.0.0.1:5000就说明起来了。第一步验证工具列表curl -s http://127.0.0.1:5000/tools | python -m json.tool预期返回里能看到plus和get_weather两个定义plus的parameters.properties里a、b都是integerrequired是[a, b]。如果这里类型是string说明你的类型标注没生效检查函数有没有写a: int。第二步验证工具执行curl -s -X POST http://127.0.0.1:5000/call \ -H Content-Type: application/json \ -d {name: plus, arguments: {a: 3, b: 4}}预期返回{tool: plus, result: 7}注意result是字符串7因为execute里做了json.dumps统一转字符串这是为了回传给模型时格式一致。再测一个带字典返回的curl -s -X POST http://127.0.0.1:5000/call \ -H Content-Type: application/json \ -d {name: get_weather, arguments: {city: 北京}}预期返回{tool: get_weather, result: {\city\: \北京\, \weather\: \晴 26℃\}}。到这里本地链路就通了。第三步是端到端把/tools的定义塞进模型请求的tools字段模型返回tool_calls后你解析出name和arguments再 POST 到/call。用 TaoToken 通道时请求头带上Authorization: Bearer sk-你的KeyBase URL 用https://taotoken.net/api。成功时你会看到模型先返回一个tool_calls执行完把结果作为role: tool的消息再发回去模型给出最终自然语言回答。这一轮跑通MCP 工具调用就算真正落地了。5. 本篇常见错误排查排障部分按真实报错来遇到对号入座。401 Unauthorized九成是 Key 问题。检查Authorization头是不是Bearer加空格再加 Key检查 Key 有没有多余换行。用curl -v看请求头实际发出去的内容。如果 Key 确认没错还是 401去控制台确认这个 Key 是否被禁用或额度耗尽。local proxy failed / connection refused本地 Flask 没起来或者端口被占。先curl http://127.0.0.1:5000/tools确认本地通不通。如果本地通但转发失败检查 Base URL 是不是写成了带/v1的地址正确写法是https://taotoken.net/api。reading choices of undefined这个报错通常出现在解析模型响应时。原因是响应体不是预期的 JSON可能是鉴权失败返回了错误页也可能是超时返回空。先打印原始response.text再解析别直接response.json()[choices]。加上状态码判断非 200 直接抛出原始内容。OAuth / 认证流程报错如果你用的是 Claude Code 或 Codex 这类带 OAuth 的工具注意它们可能优先走自己的登录态。要在配置里显式指定 Base URL 和 Key覆盖默认认证。Codex 的auth.json里要写全三件套Base URL、Key、Model ID缺一个都可能回退到默认流程导致报错。工具找不到 KeyError: Tool xxx not found说明tools/tool_1.py没被导入。检查app.py里有没有import tools.tool_1或者你的工具文件没放在被导入的路径下。注册是导入时触发的不导入就不注册。参数类型不对导致执行失败模型传了字符串3但函数要int。可以在execute里加一层类型转换或者把 Schema 描述写得更明确。更稳的做法是在函数内部做校验返回清晰的错误信息模型看到错误后往往会自我修正重试。CORS 报错如果从浏览器前端直接调本地 Flask会跨域。开发阶段装flask-cors并CORS(app)生产环境别这么干走服务端转发。6. 把这条链路用起来工具注册中心跑通后扩展就很简单了新工具写个函数加装饰器import一下即可Schema 自动生成。真正要花心思的是工具描述和参数设计模型能不能选对工具、传对参数全看这两点。如果你要长期跑编码类 Agent 任务建议把 Key 和 Base URL 统一收敛到 TaoToken客户端只维护一份配置换模型时改 Model ID 就行不用动代码。需要创建 Key 去 API Keys 页面接入细节看接入文档想先验证模型对工具调用的理解能力用模型对话页面手动构造一轮tools请求最直观高频编码场景再考虑 Coding Plan。最后留一个实用技巧给ToolRegistry加一个list_names()方法启动时打印所有已注册工具名能第一时间发现「工具没注册上」这类低级问题。我试过在工具多起来之后这个日志比任何调试都管用。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑