☰
Python的MCP Server开发实战:用uv与Type Hints构建可调试的TaoToken工具服务
2026/10/7 20:02:39 网站建设 项目流程

1. 从零写一个能调试的 MCP Server,为什么我最后选了 uv + Type Hints

MCP Server 说白了就是给大模型外挂的一双手:模型自己不会算数、不会查库、不会调你的内部接口,但它能通过 MCP 协议发现你注册的 tool,然后按你声明的参数结构发起调用。Python 的 MCP Server 开发,核心就三件事——用 SDK 起一个服务、用装饰器注册工具、用 Type Hints 和 Docstring 把工具的语义讲清楚。适合谁?适合已经会写 Python 函数、想让自己的脚本被 Claude Desktop、Cline、Codex 这类客户端直接调起来的同学。

我一开始也是 pip 一把梭,pip install mcp就开干,结果在依赖解析和启动方式上反复踩坑:本地能跑,换台机器就报版本冲突;mcp dev能起,mcp install又挂。后来换成 uv 管理依赖,配合 Type Hints 约束入参出参,整个链路才稳定下来。这篇就按「初始化项目 → 写工具 → 本地 stdio 联调 → 接 TaoToken 统一 Key 通道 → 排错」的顺序走一遍,代码都能直接复制。

先说清楚 MCP Server 到底在干嘛。你可以把它理解成一个「函数注册中心」:你写普通 Python 函数,加个@mcp.tool()装饰器,SDK 会自动读取函数的类型注解和文档字符串,生成一份 JSON Schema 描述,告诉模型「这个工具叫什么、要什么参数、返回什么」。模型看到这份描述后,决定什么时候调用它。所以 Type Hints 不是可选项,它是模型理解你工具的唯一入口——你写a: int,模型就知道要传整数;你写a: str | None,某些旧版本 typer 直接崩给你看,这就是后面要讲的坑。

uv 在这里的价值是「可复现」。MCP Server 通常要装进客户端配置里长期运行,依赖一旦漂移,客户端启动就失败。uv 用pyproject.toml+uv.lock锁死版本,uv run直接拉起虚拟环境,不用手动 activate。对 MCP 这种「配置一次、长期被调用」的场景,这点比 pip 省心太多。

2. 用 uv 初始化项目并接入 TaoToken 统一 Key 通道

这一节把地基打好:装 uv、建项目、加依赖,再把 TaoToken 的 API 通道配进来。TaoToken 在这里的角色是「统一 Key / API 通道」——你的 MCP 工具如果需要调用大模型能力(比如做文本润色、意图识别),不用每个客户端各配一套 Key,走同一个 Base URL 和 Key 就行。官网入口在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 地址是 https://taotoken.net/api 。

先装 uv。macOS / Linux 用官方脚本,Windows 用 pip 也行:

# macOS / Linux curl -LsSf https://astral.sh/uv/install.sh | sh # 或者已经有 Python 环境,直接 pip 装 pip install uv

装完验证一下:

uv --version # uv 0.5.x 之类

然后初始化项目。注意uv init会生成pyproject.toml、.python-version和main.py,我们把它改成 server 目录:

uv init mcp-tool-server cd mcp-tool-server

加 MCP SDK 和 CLI 工具。mcp[cli]这个 extra 会带上mcp命令行,用来跑 inspector 和安装到客户端:

uv add "mcp[cli]"

这一步 uv 会自动创建.venv并写好uv.lock。装完你的pyproject.toml大概长这样,注意requires-python和依赖版本:

[project] name = "mcp-tool-server" version = "0.1.0" description = "A debuggable MCP tool server built with uv and Type Hints" readme = "README.md" requires-python = ">=3.10" dependencies = [ "mcp[cli]>=1.2.0", "httpx>=0.27.0", ] [build-system] requires = ["hatchling"] build-backend = "hatchling.build"

这里我额外加了httpx,因为后面工具里要发 HTTP 请求到 TaoToken 的 API 通道。如果你暂时不接模型能力,可以不加。

接下来配置 TaoToken 的接入信息。MCP Server 本身不强制你用什么模型通道,但一旦工具里要调模型,就需要 Base URL + Key + Model ID 三件套。我习惯用环境变量,避免把 Key 写进代码:

# 写入项目根目录的 .env(记得加进 .gitignore) TAOTOKEN_BASE_URL=https://taotoken.net/api TAOTOKEN_API_KEY=sk-你的Key TAOTOKEN_MODEL=claude-sonnet-4-5

Key 在控制台创建,地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API Keys 管理页在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。模型对话调试页在 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。

注意:Base URL 用https://taotoken.net/api,不要带末尾斜杠,也不要在代码里拼/v1之外的路径,具体以接入文档为准。

如果你用的是 Claude Code 这类客户端,配置片段(settings 风格)大致如下,路径按你本机实际位置改:

{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "sk-你的Key", "ANTHROPIC_MODEL": "claude-sonnet-4-5" } }

Cline / Roo 这类 VS Code 插件,则是在设置里填 Base URL、API Key、Model ID 三项,Base URL 同样是https://taotoken.net/api。Codex 的auth.json结构不同,但三件套逻辑一致:Base URL、Key、Model ID 缺一不可。这一步先把通道备好,下一节写工具时就能直接调用。

3. 用 Type Hints 注册工具:可复制的 server.py 配置

现在写核心的server.py。MCP Python SDK 提供FastMCP,用装饰器注册工具,SDK 会自动把类型注解和 Docstring 转成模型能读懂的 schema。先看一个最小可运行版本:

# server.py import sys import httpx from mcp.server.fastmcp import FastMCP mcp = FastMCP("TaoTokenTools") @mcp.tool() def add(a: int, b: int) -> int: """Add two integers and return the sum.""" return a + b @mcp.tool() def divide(a: float, b: float) -> float: """Divide a by b. b must not be zero.""" if b == 0: raise ValueError("b must not be zero") return a / b if __name__ == "__main__": mcp.run()

关键点在于@mcp.tool()下面的函数签名。a: int, b: int -> int会被 SDK 解析成参数类型和返回类型,Docstring 变成工具描述。模型就是靠这些信息判断「什么时候该调 add、什么时候该调 divide」。我试过把 Docstring 写成和函数实际行为不一致的描述,模型会按描述去调,结果拿到意料之外的返回值——所以 Docstring 必须和实现一致。

再写一个真正调用 TaoToken 通道的工具,演示怎么在 MCP 工具里发 HTTP 请求:

import os import httpx from mcp.server.fastmcp import FastMCP mcp = FastMCP("TaoTokenTools") BASE_URL = os.environ.get("TAOTOKEN_BASE_URL", "https://taotoken.net/api") API_KEY = os.environ.get("TAOTOKEN_API_KEY", "") MODEL = os.environ.get("TAOTOKEN_MODEL", "claude-sonnet-4-5") @mcp.tool() def polish_text(text: str, tone: str = "professional") -> str: """Polish the given text into the specified tone. Args: text: The raw text to polish. tone: Target tone, e.g. professional, casual, concise. """ if not API_KEY: raise RuntimeError("TAOTOKEN_API_KEY is not set") payload = { "model": MODEL, "messages": [ {"role": "system", "content": f"Rewrite the text in a {tone} tone."}, {"role": "user", "content": text}, ], } headers = { "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json", } with httpx.Client(timeout=60) as client: resp = client.post(f"{BASE_URL}/v1/messages", json=payload, headers=headers) resp.raise_for_status() data = resp.json() return data["content"][0]["text"]

注意tone: str = "professional"这种带默认值的参数,SDK 会把它标成可选参数,模型不传也能调。但默认值类型要和注解一致,否则 schema 生成会出问题。

如果你用 Cline MCP 或 Claude Desktop,配置里要写全三件套。以 Claude Desktop 的claude_desktop_config.json为例:

{ "mcpServers": { "taotoken-tools": { "command": "uv", "args": ["--directory", "/绝对路径/mcp-tool-server", "run", "server.py"], "env": { "TAOTOKEN_BASE_URL": "https://taotoken.net/api", "TAOTOKEN_API_KEY": "sk-你的Key", "TAOTOKEN_MODEL": "claude-sonnet-4-5" } } } }

command用uv,args里--directory指向项目绝对路径,这样客户端启动时会自动进虚拟环境跑server.py,不用你手动 activate。这套配置对 Cline MCP 也基本通用,只是配置文件位置不同。

4. 本地 stdio 联调与一次成功请求验证

写完代码别急着装进客户端,先用mcp dev起 inspector 本地调试。stdio 模式下,客户端和 server 通过标准输入输出通信,inspector 会给你一个网页界面手动触发工具调用。

uv run mcp dev server.py

正常输出类似:

Starting MCP inspector... Proxy server listening on port 3000 MCP Inspector is up and running at http://localhost:5173

浏览器打开http://localhost:5173,左侧能看到你注册的工具列表,点add,填a=1, b=2,点 Run,右侧返回3。这一步验证的是「工具注册 + 类型解析 + 调用链路」全通。

再验证polish_text。先在终端导出环境变量,或者确认.env已被加载:

export TAOTOKEN_BASE_URL=https://taotoken.net/api export TAOTOKEN_API_KEY=sk-你的Key export TAOTOKEN_MODEL=claude-sonnet-4-5 uv run mcp dev server.py

在 inspector 里调polish_text,text填「这个功能挺好用的」,tone填casual,返回应该是润色后的句子。如果返回 401,说明 Key 没读到或无效;如果返回连接错误,检查 Base URL 是否写成了https://taotoken.net/api。

验证通过后,装进 Claude Desktop:

uv run mcp install server.py

输出里会看到Added server 'TaoTokenTools' to Claude config。重启 Claude Desktop,点输入框旁的工具图标,就能看到你注册的工具。输入「帮我算一下 12 除以 4」,模型会调用divide并返回3.0。

这里有个细节:mcp install默认把 server 名写进配置,如果你改了FastMCP("TaoTokenTools")里的名字,配置里的 key 也会跟着变。装完最好去claude_desktop_config.json核对一遍,确认command、args、env三块都对。

5. 常见报错排查:401、local proxy failed、reading choices、OAuth

这一节按真实报错来对。MCP Server 开发最容易卡在四类错误上,我一个个拆。

第一类:401 Unauthorized。工具里调 TaoToken 通道返回 401,九成是 Key 没传对。检查三处:环境变量名是否和代码里os.environ.get("TAOTOKEN_API_KEY")一致;客户端配置的env块里有没有这个变量;Key 本身是否在控制台被禁用。注意Authorization头是Bearer sk-xxx,别漏了Bearer前缀。

第二类:local proxy failed / connection refused。这种通常出现在mcp dev启动阶段,端口 3000 或 5173 被占用。换个端口:

uv run mcp dev server.py --port 3001

如果是客户端启动 server 时报这个,检查args里的路径是不是绝对路径,uv --directory后面有没有拼错。相对路径在客户端环境下经常解析失败。

第三类:reading 'choices' of undefined。这个报错说明你拿到的响应结构不是预期的 OpenAI 风格。TaoToken 的/v1/messages返回的是 Anthropic 风格,取文本要用data["content"][0]["text"];如果你走的是/v1/chat/completions,才是data["choices"][0]["message"]["content"]。两种端点返回结构不同,取错字段就会报reading 'choices' of undefined或reading 'content' of undefined。先确认你调的是哪个端点,再对应取字段。

第四类:OAuth 相关报错。某些客户端在连接远程 MCP Server 时会走 OAuth 流程,本地 stdio 模式一般用不到。如果你看到OAuth字样,先确认是不是误配了远程 transport。本地开发用 stdio,配置里不要写url字段,只写command+args。

还有一个高频坑:RuntimeError: Type not yet supported: str | None。这是 typer 旧版本不支持X | None联合类型导致的,出现在mcp install或mcp dev启动时。解决办法是升级:

uv add --upgrade "mcp[cli]" typer

升级后重启终端再跑。这个坑我在旧环境里踩过,升级 typer 到 0.12 以上就好了。

排查顺序建议:先看终端完整 traceback,定位是启动阶段还是调用阶段;启动阶段多半是依赖/路径问题,调用阶段多半是 Key/字段问题。把print(..., file=sys.stderr)加在工具函数里,日志会打到客户端日志或终端,不影响 stdio 协议。

6. 把工具服务接进长期编码流:Coding Plan 与后续调试

工具跑通之后,下一步是让它进入日常编码流。如果你只是偶尔调一下,inspector 手动触发就够了;但如果你想让 MCP 工具长期挂在 Cline、Claude Code 里,配合 Agent 做多步任务,那就要考虑通道的稳定性和额度。TaoToken 的 Coding Plan 就是为这种长期编码场景准备的,入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。

接入方式还是三件套:Base URL 用https://taotoken.net/api,Key 用 Coding Plan 对应的 Key,Model ID 按文档填。Claude Code 的配置片段和前面 settings 那段一致,只是 Key 换成 Coding Plan 的。Cline MCP 里则是在 provider 设置里选自定义 Base URL,填同样的地址和 Key。

调试上我有个习惯:给每个工具函数加一行 stderr 日志,记录入参和耗时。stdio 模式下 stdout 被协议占用,日志必须走 stderr,否则会污染通信导致客户端解析失败。比如:

import sys import time @mcp.tool() def add(a: int, b: int) -> int: """Add two integers and return the sum.""" start = time.time() result = a + b print(f"[add] a={a} b={b} result={result} cost={time.time()-start:.3f}s", file=sys.stderr) return result

这样在客户端日志里能看到每次调用的真实入参,排查模型传参错误时特别有用。模型有时候会把字符串"1"传给int参数,SDK 会尝试转换,转不了就报错,日志里一看便知。

最后提醒一点:MCP Server 的 Docstring 是给模型看的,不是给人看的。写的时候用祈使句、说清楚边界条件,比如「b must not be zero」,模型在调用前就会自己判断要不要传零。这比在代码里抛异常再让模型重试要高效得多。工具描述写得好,模型调用准确率能明显提升,这是我在多个项目里验证过的经验。

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

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

立即咨询