☰
MCP Server开发实战:用LLM建智能工具生态的完整指南(TaoToken统一Key接入篇)
2026/9/27 12:39:11 网站建设 项目流程

1. 为什么我要自己写一个 MCP Server

MCP Server 是 Model Context Protocol 服务端的简称,它做的事情说白了就是给大模型装上一双手:模型不再只会聊天,而是能通过标准协议去调用你写的工具函数,比如查数据库、读文件、调内部接口。适合谁?适合手里已经有一堆零散脚本、想让 LLM 自动编排这些脚本的开发者,也适合想把公司内部系统安全暴露给 AI 助手的团队。

我最早接触 MCP 是因为一个很具体的痛点:团队里有个查询订单状态的小工具,每次都要人工复制订单号到脚本里跑一遍,再把结果贴回对话框。后来想干脆让模型自己调,但直接给模型开 HTTP 接口又担心权限失控。MCP 的 Host / Client / Server 三层结构正好解决这个问题——Server 只暴露声明过的工具,Client 负责协议转换,Host 决定什么时候发起调用,边界清晰。

这篇会从零搭一个能跑的 MCP Server,重点放在模型调用环节怎么用 TaoToken 的统一 Key 打通,避免你在多个模型供应商之间来回切配置。全程给可复制的 config.toml 和 settings.json 骨架,最后附连通性验证和几个我踩过的报错。

2. 前置准备:TaoToken 统一 Key 与 MCP 运行环境

2.1 为什么模型调用层要单独抽出来

MCP Server 本身只负责工具执行,但工具执行完往往需要模型做二次加工,比如把查询结果总结成自然语言。如果每个工具都硬编码一个模型 SDK,后面换模型就是灾难。我的做法是把模型调用统一收敛到一个 OpenAI 兼容的入口,MCP Server 内部只认 base_url 和 api_key 两个变量。

TaoToken 在这里的角色就是那个统一入口,它提供 OpenAI 兼容的 API 通道,base_url 填https://taotoken.net/api,Key 在控制台生成。这样我的 MCP Server 代码里不需要出现任何具体模型厂商的名字,换模型只改一个字符串。

2.2 环境与依赖

Python 3.10 以上,我实测 3.11 最稳。核心依赖三个:mcp官方 SDK、fastapi做 HTTP 层、httpx做异步请求。装的时候注意 mcp 的版本,0.4 和 0.5 的 API 有差异,下面代码基于 0.5。

python -m venv venv source venv/bin/activate pip install "mcp>=0.5" fastapi uvicorn httpx pydantic

Key 的获取路径:登录后进控制台,在 API Keys 页面新建一个,复制出来存到环境变量,别写进代码。

export TAOTOKEN_API_KEY="sk-你的key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"

注意:Key 只显示一次,丢了就重新生成。生产环境建议用密钥管理服务注入,不要留在 shell history 里。

3. 可复制配置:config.toml 与 settings.json 骨架

3.1 config.toml:MCP Server 自身配置

这个文件放在项目根目录,描述 Server 名称、传输方式、以及模型调用参数。传输方式我选 stdio,本地开发最省事,不用管端口。

[server] name = "order-mcp" version = "0.1.0" transport = "stdio" [model] provider = "openai-compatible" base_url = "https://taotoken.net/api" api_key_env = "TAOTOKEN_API_KEY" default_model = "claude-sonnet-4" timeout = 60 max_retries = 2 [tools] enabled = ["get_order_status", "summarize_order"]

api_key_env这个设计很关键,配置文件里永远不出现明文 Key,只写环境变量名,代码运行时去读。

3.2 settings.json:客户端侧接入配置

如果你用 Cline 或 Claude Code 这类支持 MCP 的客户端,它们读的是 settings.json。下面这份是 Cline 的骨架,Claude Code 的字段名略有不同但结构一致。

{ "mcpServers": { "order-mcp": { "command": "python", "args": ["-m", "order_mcp.server"], "env": { "TAOTOKEN_API_KEY": "${env:TAOTOKEN_API_KEY}", "TAOTOKEN_BASE_URL": "https://taotoken.net/api" }, "disabled": false, "autoApprove": ["get_order_status"] } } }

autoApprove里放只读工具,写操作类工具不要放进去,否则模型可能在你没确认的情况下改数据。

3.3 CC Switch 接入配置

CC Switch 用来在多个 MCP Server 之间切换,它的配置是一个数组,每个元素指向一份 settings.json。我通常按项目分,一个项目一份。

{ "profiles": [ { "name": "order-dev", "settingsPath": "./config/settings.dev.json" }, { "name": "order-prod", "settingsPath": "./config/settings.prod.json" } ], "active": "order-dev" }

切换时只改active字段,不用动其他文件。这个设计在同时维护测试和生产两套工具时特别省心。

4. 写一个能跑的 MCP Server:订单查询工具

4.1 工具定义与注册

MCP 的工具注册靠装饰器,参数用 Pydantic 模型声明,SDK 会自动生成 JSON Schema 给模型看。下面这个get_order_status是只读工具,返回结构化数据。

from mcp.server import Server from mcp.server.stdio import stdio_server from pydantic import BaseModel, Field app = Server("order-mcp") class OrderQuery(BaseModel): order_id: str = Field(description="订单号,格式 ORD-开头") detail: bool = Field(default=False, description="是否返回明细") @app.tool() async def get_order_status(query: OrderQuery) -> dict: """查询订单当前状态,返回状态码和更新时间""" # 实际项目替换为数据库查询 mock = { "order_id": query.order_id, "status": "shipped", "updated_at": "2025-01-15T10:30:00Z", } if query.detail: mock["items"] = [{"sku": "A100", "qty": 2}] return mock

Field里的 description 不是写给人看的,是写给模型看的。模型靠这段文字判断什么时候该调这个工具、参数怎么填。我试过把 description 写得很含糊,结果模型经常传错 order_id 格式,补上格式说明后就准了。

4.2 接入 TaoToken 做结果总结

工具返回的是 JSON,但用户想听人话。这里加一个summarize_order工具,内部调 TaoToken 的 API 把 JSON 转成自然语言。

import os import httpx TAOTOKEN_BASE = os.environ["TAOTOKEN_BASE_URL"] TAOTOKEN_KEY = os.environ["TAOTOKEN_API_KEY"] async def call_llm(prompt: str, model: str = "claude-sonnet-4") -> str: async with httpx.AsyncClient(timeout=60) as client: resp = await client.post( f"{TAOTOKEN_BASE}/v1/chat/completions", headers={"Authorization": f"Bearer {TAOTOKEN_KEY}"}, json={ "model": model, "messages": [{"role": "user", "content": prompt}], "temperature": 0.3, }, ) resp.raise_for_status() return resp.json()["choices"][0]["message"]["content"] @app.tool() async def summarize_order(order_id: str) -> str: """把订单 JSON 转成一句人话总结""" raw = await get_order_status(OrderQuery(order_id=order_id, detail=True)) prompt = f"用一句话总结这个订单状态,不要加前缀:{raw}" return await call_llm(prompt)

注意 base_url 后面要拼/v1/chat/completions,这是 OpenAI 兼容协议的标准路径。TaoToken 的 API 地址是https://taotoken.net/api,拼完就是完整的请求地址。

4.3 启动入口

async def main(): async with stdio_server() as (read, write): await app.run(read, write, app.create_initialization_options()) if __name__ == "__main__": import asyncio asyncio.run(main())

stdio 模式下 Server 通过标准输入输出和 Client 通信,所以启动后终端不会有任何输出,这是正常的,别以为卡死了。

5. 验证请求:确认工具真的被调起来了

5.1 用 MCP Inspector 做连通性验证

官方有个 Inspector 工具,能直接列出 Server 暴露的工具并手动调用。

npx @modelcontextprotocol/inspector python -m order_mcp.server

打开它给的本地地址,左侧应该能看到get_order_status和summarize_order两个工具。点进去填order_id: ORD-123,执行,右侧返回 JSON 就说明 Server 通了。

5.2 验证模型调用链路

工具通了不代表模型调用通了。单独测一下 TaoToken 的通道:

curl -s https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "claude-sonnet-4", "messages": [{"role": "user", "content": "回复 OK 两个字母"}] }'

返回里有choices[0].message.content且内容是 OK,说明 Key 和 base_url 都对。这一步能省掉后面大量排查时间,因为 MCP 层报错经常把模型层的错误吞掉。

5.3 端到端跑一次

在 Cline 里输入「帮我查一下 ORD-123 的状态并总结」,正常流程是:模型先调get_order_status,拿到 JSON,再调summarize_order,最后把总结返回。你会在工具调用面板看到两次调用记录。如果只看到一次,说明模型没理解要串联,检查工具 description 是否写清楚了依赖关系。

6. 本篇常见报错排查

6.1 401 Unauthorized

九成是 Key 没读到。先确认echo $TAOTOKEN_API_KEY有值,再确认 settings.json 里的${env:...}语法被客户端支持。有些客户端不解析这种占位符,得直接写值或者用它的密钥管理功能。另外检查 Authorization 头是不是Bearer加空格,少个空格也会 401。

6.2 工具列表为空

Server 启动了但 Client 看不到工具,通常是启动命令的工作目录不对。python -m order_mcp.server要求order_mcp是包,目录下得有__init__.py。如果报No module named,在 settings.json 的 args 里加-u并确认 cwd 字段指向项目根目录。

6.3 模型调用超时

默认 timeout 60 秒,长文本总结可能不够。在 config.toml 里把 timeout 调到 120,同时给 httpx 客户端也设上。另外max_retries设 2 就够,设太多遇到持续性错误会拖很久。如果频繁超时,先单独用 curl 测模型通道,排除是网络还是模型本身慢。

6.4 工具参数校验失败

模型传的参数类型不对,比如把布尔值传成字符串 "true"。解决办法是在 Pydantic 模型里加model_config = {"strict": True},让校验更严格,同时在 description 里明确写「布尔值,不是字符串」。我遇到过模型把 detail 传成 "yes",加上严格模式后它会自动纠正。

6.5 stdio 模式下日志污染

stdio 模式下 Server 的 stdout 是协议通道,任何 print 都会破坏协议导致 Client 解析失败。所有调试输出必须走 stderr:

import sys print("debug info", file=sys.stderr)

这个坑我踩过,现象是 Client 报 JSON 解析错误,但 Server 看起来正常运行,查了半天才发现是一行 print。

7. 把链路跑顺之后

工具生态能不能落地,关键不在工具写得多花哨,而在模型调用这一层稳不稳。把 TaoToken 的统一 Key 接进来之后,我的 config.toml 里再也没出现过第二个 base_url,换模型只改default_model一个字段。如果你后面要接更多工具,建议按功能拆多个 MCP Server,用 CC Switch 管理,别全塞一个进程里,否则一个工具报错会拖垮整条链路。

需要生成 Key 的话去控制台新建,接入细节看接入文档,想先验证模型通道是否通可以直接在模型对话里发一条测试消息。长期做编码类 Agent 的话,Coding Plan 的额度模型更适合高频调用场景。

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

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

立即咨询