☰
第1章 MCP设计:用Python+Flask从零搭建工具调用服务,TaoToken统一Key接入实战
2026/10/8 10:11:44 网站建设 项目流程

1. 为什么要在本地搭一个 MCP 工具调用服务

MCP(Model Context Protocol)这两年被讨论得很多,但真正落到代码层面,很多同学会卡在同一个地方:大模型怎么知道本地有哪些函数可以调?参数从哪来?调用结果怎么回传?如果每次都要手写一大段 JSON Schema,维护成本会高到让人放弃。

我自己的做法是:把「函数注册」这件事做成类似 Flask 路由的体验。Flask 用@app.route("/xxx")把一个函数变成 HTTP 接口,那我们同样可以用@registry.tool(name="plus")把一个普通 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 URL:https://taotoken.net/api
  • API 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搞定:

flask==3.0.3 requests==2.32.3

Python 版本建议 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(f"Tool '{name}' not found in registry") func = self._tools[name]["func"] try: result = func(**arguments) if not isinstance(result, str): result = json.dumps(result, ensure_ascii=False) return result except Exception as e: return f"Error 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.py:

from core.tool_provider import registry @registry.tool(name="plus", description="Plus two integers") def add(a: int, b: int) -> int: return a + b @registry.tool(name="get_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(force=True) name = payload.get("name") arguments = payload.get("arguments", {}) if not name: return jsonify({"error": "missing tool name"}), 400 try: result = registry.execute(name=name, arguments=arguments) return jsonify({"tool": name, "result": result}) except KeyError as e: return jsonify({"error": str(e)}), 404 if __name__ == "__main__": app.run(host="127.0.0.1", port=5000, debug=True)

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都是integer,required是["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-你的Key,Base 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()方法,启动时打印所有已注册工具名,能第一时间发现「工具没注册上」这类低级问题。我试过在工具多起来之后,这个日志比任何调试都管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询