1. 为什么我要用 Python + UVX 重写一遍 MCP 服务
MCP 是 Model Context Protocol 的缩写,简单说就是一套让大模型能调用外部工具的通信协议。你可以把它理解成给模型装了一个 USB 接口:模型本身只会聊天,但通过 MCP,它能读文件、查数据库、调接口、跑脚本。适合谁?适合所有想让自己的 Python 脚本被 Claude、Cursor、Cline 这类客户端直接调用的开发者。
我最早接触 MCP 时,用的是传统pip install+ 手动配python xxx.py的方式。问题很明显:换台机器就得重装依赖,虚拟环境路径一改就报错,团队里别人拿到你的代码还得问“你 Python 几点几”。后来我把服务改成基于 UVX 分发,情况完全变了——客户端只要写一行uvx your-package,它会自动拉取、自动隔离依赖、自动执行,不需要你提前装任何东西。
这篇要做的,是一个能跑通的 UVX 版 MCP 服务:从项目结构、pyproject.toml配置、server.py编写,到本地用 stdio 调用验证,再到接入 TaoToken 的模型做一次真实请求。全程可复制,你跟着敲就能得到一个属于自己的 MCP 服务。热词里的 MCP、Python、UVX 三个点,我会在每一步都落到具体文件和命令上,不空谈概念。
先说清楚 UVX 是什么。它是uv工具链里的执行器,类似npx之于 Node。uvx package-name会临时创建一个隔离环境,装好包再运行它的入口命令,跑完不留垃圾。对 MCP 来说这太合适了:MCP 客户端启动服务时就是执行一条命令,UVX 让这条命令变成“自包含”的,用户零配置。
我试过把同一个服务分别用 pip 和 uvx 分发,pip 版本在别人机器上因为 Python 版本差异挂了两次,uvx 版本一次过。这就是我坚持用 UVX 的原因。
2. 前置准备:TaoToken 接入与 uv 环境安装
在写代码之前,先把两件事办了:装 uv,以及拿到调用模型需要的 API Key。MCP 服务本身可以只做本地工具(比如读文件),但大多数实用场景都要调模型,所以这一步不能省。
2.1 安装 uv 与验证版本
uv 支持 macOS、Linux、Windows。macOS/Linux 一行命令:
curl -LsSf https://astral.sh/uv/install.sh | shWindows 用 PowerShell:
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"装完验证:
uv --version uvx --version正常会输出类似uv 0.5.x和uvx 0.5.x。如果提示 command not found,把~/.local/bin加进 PATH 再开一个新终端。这一步踩坑最多的是 Windows 用户忘了重开终端,环境变量没生效。
2.2 获取 TaoToken API Key
TaoToken 是一个模型调用聚合入口,兼容 OpenAI 风格的接口,MCP 服务里调模型时把 Base URL 指向它就行。注册和拿 Key 的入口在这里:
控制台与 API Key 管理:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite
登录后创建一个 Key,复制保存。注意 Key 只在创建时完整显示一次,丢了就重新建。拿到后先别急着写进代码,用环境变量管理,后面配置里我会写成TAOTOKEN_API_KEY。
Base URL 用https://taotoken.net/api,这个地址不加任何查询参数。模型 ID 按你账号里可用的填,比如gpt-4o-mini这类通用对话模型都行,具体以控制台模型列表为准。
2.3 初始化项目骨架
建目录并初始化:
mkdir mcp-uvx-demo && cd mcp-uvx-demo uv init --package mcp-uvx-demouv init --package会生成一个标准包结构,包含pyproject.toml和src/mcp_uvx_demo/__init__.py。我建议保留这个 src 布局,因为 UVX 打包时对入口点识别更稳。目录长这样:
mcp-uvx-demo/ ├── pyproject.toml ├── README.md └── src/ └── mcp_uvx_demo/ └── __init__.py接下来加依赖。MCP 官方 Python SDK 叫mcp,我们还需要httpx来调 TaoToken 的接口:
uv add mcp httpxuv add会自动写进pyproject.toml的 dependencies 并生成uv.lock。锁文件很重要,它保证别人 uvx 运行时装到的依赖版本和你一致,避免“我这能跑你那报错”。
3. 可复制配置:pyproject.toml 与 server.py 完整写法
这一节是核心,两个文件决定服务能不能被 UVX 正确拉起。我先把pyproject.toml的完整内容贴出来,再逐段解释。
3.1 pyproject.toml 入口点配置
[project] name = "mcp-uvx-demo" version = "0.1.0" description = "A demo MCP server built with Python and distributed via uvx" readme = "README.md" requires-python = ">=3.10" dependencies = [ "mcp>=1.2.0", "httpx>=0.27.0", ] [project.scripts] mcp-uvx-demo = "mcp_uvx_demo.server:main" [build-system] requires = ["hatchling"] build-backend = "hatchling.build"关键在[project.scripts]。这一行告诉 uvx:当用户执行uvx mcp-uvx-demo时,去调用mcp_uvx_demo.server模块里的main函数。名字mcp-uvx-demo就是将来客户端配置里写的命令名。requires-python = ">=3.10"是因为 mcp SDK 用到了较新的类型语法,低于 3.10 会报语法错误。
build-system用 hatchling,uv 默认支持,不用额外装。
3.2 server.py 实现工具与模型调用
在src/mcp_uvx_demo/下新建server.py:
import os import httpx from mcp.server.fastmcp import FastMCP mcp = FastMCP("mcp-uvx-demo") TAOTOKEN_BASE_URL = os.environ.get("TAOTOKEN_BASE_URL", "https://taotoken.net/api") TAOTOKEN_API_KEY = os.environ.get("TAOTOKEN_API_KEY", "") MODEL_ID = os.environ.get("TAOTOKEN_MODEL", "gpt-4o-mini") @mcp.tool() def add(a: int, b: int) -> int: """两数相加,用于验证 MCP 工具调用链路是否通畅。""" return a + b @mcp.tool() async def ask_model(prompt: str) -> str: """把 prompt 发给 TaoToken 上的模型并返回文本结果。""" if not TAOTOKEN_API_KEY: return "缺少 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, "messages": [{"role": "user", "content": prompt}], } async with httpx.AsyncClient(timeout=60) as client: resp = await client.post(url, headers=headers, json=payload) resp.raise_for_status() data = resp.json() return data["choices"][0]["message"]["content"] def main(): mcp.run(transport="stdio") if __name__ == "__main__": main()几个要点。FastMCP是官方 SDK 的高层封装,@mcp.tool()装饰器把普通函数注册成 MCP 工具,函数签名和 docstring 会自动变成工具的参数描述,模型靠这个决定怎么调。add是纯本地工具,用来验证协议链路;ask_model走网络,验证 TaoToken 接入。
main()里transport="stdio"表示用标准输入输出通信,这是 MCP 客户端最常用的方式,uvx 启动的进程天然支持。注意ask_model是 async 函数,FastMCP 支持异步工具,不用额外包一层。
环境变量三个:TAOTOKEN_API_KEY必填,TAOTOKEN_BASE_URL和TAOTOKEN_MODEL有默认值。这样设计是为了让服务在没配 Key 时也能启动,只是调模型会返回提示,方便你先验证本地工具。
3.3 本地安装与入口验证
在项目根目录执行:
uv sync uv run mcp-uvx-demouv sync按锁文件装依赖,uv run会执行[project.scripts]里定义的入口。如果服务正常,终端会停住等待 stdio 输入,没有报错就是成功。按 Ctrl+C 退出。
想模拟 uvx 的隔离执行效果,可以:
uvx --from . mcp-uvx-demo--from .表示从当前目录构建并运行,等价于用户从 PyPI 装你的包。这一步能过,说明打包配置没问题。
4. 验证请求:用 stdio 手动调一次 MCP 服务
服务能启动不代表工具能被调用。MCP 协议是 JSON-RPC 格式,我教你用最原始的方式发一条请求,确认add和ask_model都正常响应。
4.1 手动发送 initialize 与 tools/list
MCP 的 stdio 通信是每行一个 JSON。先发初始化请求:
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"manual-test","version":"1.0"}}}把服务跑起来后,把这行粘进终端回车,会收到包含serverInfo的响应。接着发:
{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}正常返回里能看到add和ask_model两个工具,以及它们的 inputSchema。如果这里 tools 是空的,八成是@mcp.tool()装饰器没生效或者模块没被正确导入。
4.2 调用 add 工具
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"add","arguments":{"a":3,"b":4}}}返回内容里content数组的 text 字段应该是7。这一步过了,说明 MCP 协议链路完全通。
4.3 调用 ask_model 验证 TaoToken
先确保环境变量已导出:
export TAOTOKEN_API_KEY="你的Key" export TAOTOKEN_MODEL="gpt-4o-mini" uv run mcp-uvx-demo然后发:
{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"ask_model","arguments":{"prompt":"用一句话解释什么是MCP"}}}如果返回一段模型生成的文本,说明 TaoToken 接入成功。如果返回“缺少 TAOTOKEN_API_KEY”,检查环境变量是否在启动服务的那个终端里生效。想更直观地看模型对话效果,也可以直接在网页端试:
模型对话体验:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite
4.4 接入真实客户端
手动测通后,把它配进 Cline 或 Claude Code。以 Cline 的 MCP 配置为例,在设置里加一段:
{ "mcpServers": { "mcp-uvx-demo": { "command": "uvx", "args": ["--from", "/绝对路径/mcp-uvx-demo", "mcp-uvx-demo"], "env": { "TAOTOKEN_API_KEY": "你的Key", "TAOTOKEN_MODEL": "gpt-4o-mini" } } } }三件套齐了:Base URL 走默认的https://taotoken.net/api,Key 在 env 里,Model ID 用TAOTOKEN_MODEL指定。发布到 PyPI 后,args可以简化成["mcp-uvx-demo"],连路径都不用写。
5. 常见报错排查:401、local proxy failed 与 reading choices
这一节按真实报错来,都是我或读者遇到过的。
401 Unauthorized。调ask_model时返回 401,基本是 Key 问题。三种可能:Key 没导出到启动服务的终端;Key 复制时带了空格或换行;Key 被删除或过期。排查方法是在同一个终端echo $TAOTOKEN_API_KEY看有没有值。注意 MCP 客户端配置里的 env 和终端环境变量是两套,客户端启动的服务读的是配置里的 env。
local proxy failed / connection refused。这个报错通常出现在客户端侧,意思是它连不上你配置的服务进程。原因多是command写错,比如写了uvx但系统 PATH 里没有,或者args里的路径不对。解决:在终端手动执行一遍配置里的完整命令,看能不能起来。能起来说明是客户端配置问题,起不来就是命令本身错。
reading 'choices' 报 KeyError。这个错来自data["choices"][0],说明返回的 JSON 里没有 choices 字段。常见原因是 Base URL 写错,比如多加了/v1导致路径变成/v1/v1/chat/completions,或者模型 ID 不存在返回了错误结构。排查时先把resp.text打出来看原始返回。正确路径是https://taotoken.net/api/v1/chat/completions,代码里我用f"{TAOTOKEN_BASE_URL}/v1/chat/completions"拼的,Base URL 不要带/v1。
OAuth 相关报错。如果你接的是需要 OAuth 的客户端(比如某些 Claude Code 场景),报错里出现OAuth或token exchange failed,说明客户端在走授权流程而不是 stdio。MCP 的 stdio 服务不需要 OAuth,检查客户端是不是把它当成了远程 HTTP 服务。stdio 类型配置里不应该有url字段。
uvx 找不到包。uvx mcp-uvx-demo报No solution found,说明包没发布到 PyPI 或者名字被占用。本地测试用--from .绕过。发布前先在pyproject.toml里确认 name 唯一。
工具列表为空。服务起来了但tools/list返回空数组。检查server.py里装饰器是不是@mcp.tool(),以及main()里mcp.run有没有被真正调用。还有一种情况是模块导入时抛了异常被吞掉,把uv run mcp-uvx-demo的输出完整看一遍。
6. 从本地到长期运行:把 MCP 服务用起来
服务跑通只是起点。真正要长期用,有几个方向可以走。
一是发布到 PyPI,让uvx mcp-uvx-demo在任何机器上一条命令可用。打包命令是uv build,产物在dist/下,用uv publish上传。发布前把版本号、README、license 补全,否则审核会卡。
二是把工具做厚。现在只有add和ask_model,你可以加文件读写、HTTP 请求、数据库查询。每个工具一个@mcp.tool()函数,docstring 写清楚用途和参数,模型靠它决定调用时机。工具粒度别太粗,一个函数干一件事,模型更容易选对。
三是环境变量管理。生产环境别把 Key 写死在配置里,用客户端的 env 字段或者系统级环境变量。TaoToken 的 Key 可以在控制台随时轮换,轮换后更新配置重启服务即可。
四是如果你要做的是长期编码或 Agent 类场景,单次调用模型不够,需要稳定的额度和并发。这种情况可以看下 Coding Plan:
Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite
接入文档里有各客户端的完整配置示例,包括 Claude Code、Cline、Codex 的写法:
接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite
最后说个实用技巧:调试 MCP 服务时,别一上来就配客户端,先用第 4 节的手动 JSON-RPC 方式把每个工具单独测通。客户端报错信息往往被包装过,直接看原始响应最快。等tools/list和tools/call都正常了,再往客户端里塞,能省掉大量来回折腾的时间。