ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

3步掌握MCP stdio模式:从零搭建你的首个AI服务!

3步掌握MCP stdio模式:从零搭建你的首个AI服务!

1. 为什么本地 AI 服务都爱用 MCP stdio 模式

如果你最近在折腾 AI 工具链,大概率听过 MCP 这个词。MCP 全称 Model Context Protocol,简单说就是一套让大模型能调用外部工具的通信协议。它解决的核心问题是:模型本身只会生成文本,但你想让它查数据库、读文件、调接口,就得有个标准化的“插槽”把工具接进去。MCP 就是这个插槽规范。

而 stdio 模式,是 MCP 里最适合本地开发的一种通信方式。它不走网络端口,不依赖 HTTP 服务,直接通过进程的标准输入(stdin)和标准输出(stdout)来收发数据。你可以把它想象成一条“魔法管道”:客户端把 JSON 请求塞进管道一头,服务端从另一头读出来执行,再把结果写回去。整个过程没有网络握手、没有端口占用、没有跨域问题,启动即用。

这套模式适合谁?三类人最该掌握。第一类是 Python/Java 开发者,想给自己的 AI 应用加工具能力,但不想先搭一套 HTTP 服务;第二类是本地调试党,需要快速验证一个工具逻辑对不对,stdio 模式改完代码重启进程就能测;第三类是 Agent 开发者,很多编码助手(比如 Claude Code、Cline)默认就用 stdio 方式拉起本地 MCP 服务,你不懂这个模式,配置都写不明白。

我实测下来,stdio 模式最大的好处是“零网络配置”。你不需要关心端口冲突、防火墙、内网穿透这些破事,一个命令拉起进程,客户端就能调用。代价是它只能在本机跑,跨机器就得换 SSE 或 HTTP 模式。但对“从零搭建首个 AI 服务”这个目标来说,stdio 是最短路径。

这篇文章会带你走完三步:先把服务端代码写出来,再配好客户端调用,最后通过 TaoToken 统一 Key 和 API 通道把模型接进来,跑通一个真正可用的 MCP stdio 服务。全程可复制,踩过的坑我也会标出来。

2. TaoToken 前置准备:统一 Key 与 API 通道

在写代码之前,先把“模型从哪来”这件事解决掉。MCP 服务本身只是工具的执行者,真正决定调用哪个工具、怎么调用的,是背后的大模型。所以你需要一个能稳定访问模型的 API 通道。TaoToken 在这里扮演的角色就是统一入口:一个 Key 打通多家模型,Base URL 固定,省得你在代码里到处改配置。

先注册并拿到 API Key。打开官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,注册后在控制台里创建 Key。控制台地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,进去之后左侧菜单找 API Keys,点新建,复制那串 sk- 开头的字符串。这个 Key 只显示一次,丢了就重新建一个。

拿到 Key 之后,记住两个固定值:Base URL 是 https://taotoken.net/api ,这个地址不加任何 UTM 参数,直接写进代码。模型 ID 根据你用的场景选,做工具调用建议用支持 function calling 的模型,比如 claude 系列或 gpt 系列,具体在模型对话页面能看到当前可用的列表:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 。

这里有个关键点:MCP stdio 服务端本身不直接调模型,它是被客户端(比如 Claude Code、Cline、或者你自己写的 Agent)拉起来的。所以 TaoToken 的 Key 和 Base URL 要配在客户端那一侧,而不是服务端代码里。服务端只负责暴露工具,客户端负责把工具描述发给模型、解析模型的调用意图、再把请求转发给 stdio 服务。这个分工一定要搞清楚,否则你会把 Key 写错地方。

如果你用的是 Claude Code 这类编码助手,它内部已经集成了 MCP 客户端能力,你只需要在它的配置文件里声明“我要启动哪个 stdio 服务”。而模型通道则通过环境变量注入。下面给一个通用的环境变量写法,Python 和 Java 都适用:

export TAOTOKEN_API_KEY="sk-你的Key" export TAOTOKEN_BASE_URL="https://taotoken.net/api" export TAOTOKEN_MODEL="claude-3-5-sonnet"

Windows 下用set或者直接在系统环境变量里加。配好之后,客户端启动时会读取这些变量,把模型请求发到 TaoToken 的通道上。这样你换模型只改一个环境变量,不用动服务端代码。

另外提醒一句:TaoToken 的 API 通道是标准 OpenAI 兼容格式,所以任何支持自定义 Base URL 的客户端都能接。如果你后面要写自己的 Agent 循环,直接用 openai 官方 SDK,把 base_url 指向 https://taotoken.net/api 就行。这一步做完,模型侧就通了,接下来专心搞 stdio 服务端。

3. 可复制配置:Python 与 Java 的 stdio 服务端

这一节是重头戏,直接给可复制的代码和配置。先讲 Python,因为 MCP 官方 SDK 对 Python 支持最完整,上手最快。再讲 Java,用 Solon-MCP 框架,适合已经在 JVM 生态里的团队。

3.1 Python 版:用 uv 初始化并写一个搜索工具

先建项目。我习惯用 uv,比 pip 快很多,而且虚拟环境管理干净:

uv init mcp_stdio_demo cd mcp_stdio_demo uv venv .venv source .venv/bin/activate uv add "mcp[cli]" httpx

Windows 激活命令换成.venv\Scripts\activate.bat。装完之后项目结构大概是:

mcp_stdio_demo/ ├── .venv/ ├── pyproject.toml └── mcp_stdio_demo/

在mcp_stdio_demo/目录下新建web_search.py,内容如下:

import httpx from mcp.server import FastMCP app = FastMCP("web-search") @app.tool(description="执行网络搜索并返回结果") def search_web(query: str) -> str: """通过搜索引擎获取实时信息""" with httpx.Client(timeout=10.0) as client: response = client.get( "https://api.example.com/search", params={"q": query} ) response.raise_for_status() return response.text if __name__ == "__main__": app.run(transport="stdio")

注意最后一行app.run(transport="stdio"),这是显式声明用 stdio 模式启动。如果你不写,某些版本默认可能是 SSE,客户端就连不上。FastMCP("web-search")里的名字是服务标识,客户端配置里要对应。

再写一个计算器工具,方便测试:

@app.tool(description="将两个数字相加") def add(a: float, b: float) -> float: return a + b @app.tool(description="安全除法,除数为零时抛异常") def safe_divide(a: float, b: float) -> float: if b == 0: raise ValueError("除数不能为零") return a / b

启动命令就是python web_search.py。控制台会打印类似MCP Server "web-search" started in stdio mode的日志。这时候进程会挂起等待 stdin 输入,这是正常的,别以为卡死了。

3.2 Java 版:Solon-MCP 的 stdio 注解

Java 这边用 Solon-MCP,Maven 依赖:

<dependency> <groupId>org.noear</groupId> <artifactId>solon-mcp-server</artifactId> <version>2.5.0</version> </dependency>

写一个计算器工具类:

import org.noear.solon.annotation.McpServerEndpoint; import org.noear.solon.mcp.annotation.ToolMapping; import org.noear.solon.mcp.annotation.Param; import org.noear.solon.mcp.McpChannel; @McpServerEndpoint(channel = McpChannel.STDIO) public class CalculatorTools { @ToolMapping(description = "将两个数字相加") public int add(@Param int a, @Param int b) { return a + b; } @ToolMapping(description = "从第一个数中减去第二个数") public int subtract(@Param int a, @Param int b) { return a - b; } }

关键注解是@McpServerEndpoint(channel = McpChannel.STDIO),它告诉框架用标准输入输出通信。@ToolMapping定义工具,description 会被发给模型,模型靠它判断该不该调这个工具。打包用mvn clean package,启动用java -jar target/calculator.jar。

3.3 客户端配置片段:把 stdio 服务挂上去

服务端写好了,得让客户端知道怎么拉起它。以 Claude Code 的配置为例,在~/.claude/claude_desktop_config.json或者项目级.mcp.json里写:

{ "mcpServers": { "web-search": { "command": "python", "args": ["/absolute/path/to/web_search.py"], "env": { "TAOTOKEN_API_KEY": "sk-你的Key", "TAOTOKEN_BASE_URL": "https://taotoken.net/api" } } } }

三个要素必须齐全:command 是可执行程序,args 是参数,env 是环境变量。路径一定用绝对路径,相对路径在客户端拉起子进程时经常找不到文件。如果你用 Cline 或者别的支持 MCP 的编辑器,配置结构类似,字段名可能叫mcpServers或servers,对照文档改一下就行。

Java 版的配置把 command 换成java,args 换成["-jar", "/absolute/path/to/calculator.jar"]。Model ID 通过环境变量TAOTOKEN_MODEL注入,客户端会用它去 TaoToken 通道请求模型。

4. 验证请求:从终端到客户端的完整跑通

配置写完,怎么确认真的通了?分两层验证:先验证 stdio 服务端本身能正确响应,再验证客户端能拉起服务并让模型调用工具。

4.1 终端直连测试

最原始的办法是手动往 stdin 写 JSON。启动 Python 服务后,在另一个终端用管道喂数据:

echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | python web_search.py

正常的话你会看到 stdout 返回工具列表,包含search_web、add、safe_divide的描述。如果没有任何输出,检查两点:一是app.run(transport="stdio")有没有写,二是日志是不是打到了 stderr 而不是 stdout。MCP 协议规定 stdout 只能放协议数据,日志必须走 stderr,否则客户端解析会报reading choices之类的错。

再测一次工具调用:

echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"add","arguments":{"a":3,"b":5}}}' | python web_search.py

返回里应该能看到"result": 8。这一步通了,说明服务端逻辑没问题。

4.2 客户端侧验证

打开 Claude Code 或者你用的 MCP 客户端,它会自动读取配置文件并拉起 stdio 进程。你可以在对话里直接说“帮我算一下 3 加 5”,模型会识别出该调用add工具,客户端把请求转发给 stdio 服务,拿到结果再回给模型。整个过程你能在客户端的工具调用日志里看到。

如果客户端没反应,先看它的 MCP 日志。Claude Code 的日志一般在~/.claude/logs/下,找 mcp 相关的文件。常见现象是进程启动了但工具列表为空,多半是路径写错或者依赖没装全。另一个坑是 Python 虚拟环境:客户端拉起子进程时用的是系统 Python,不是你激活的 venv,所以要么在 command 里写 venv 里的 python 绝对路径,要么把依赖装到系统环境。

4.3 通过 TaoToken 通道验证模型调用

服务端和客户端都通了之后,最后确认模型请求真的走了 TaoToken。在客户端里问一个需要工具的问题,比如“搜索一下今天的天气”,然后看 TaoToken 控制台的用量记录。控制台地址 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,进去看 API 调用日志,应该能看到对应的请求记录,模型 ID 和你配的一致。

如果控制台没有记录,说明客户端根本没发模型请求,问题出在客户端的模型配置上,不是 MCP 服务的问题。这时候检查TAOTOKEN_BASE_URL有没有写对,Key 有没有过期。TaoToken 的 Base URL 是 https://taotoken.net/api ,注意结尾没有斜杠,有些客户端会自动补/v1,你按它的文档来。

5. 本篇常见错排查:401、local proxy failed 与 reading choices

这一节把最容易撞上的报错列出来,对照着改。

401 Unauthorized:这个最直接,Key 不对或者没传。检查三处:环境变量TAOTOKEN_API_KEY有没有拼错;客户端配置里的 env 字段有没有生效;Key 是不是在控制台被删了。TaoToken 的 Key 是 sk- 开头,复制的时候别带空格。如果用的是 Claude Code,它可能读的是系统环境变量而不是配置文件里的 env,那就把 Key 加到 shell 的 profile 里。

local proxy failed / connection refused:这个报错通常出现在客户端试图连接一个 HTTP 代理或者 SSE 端点时。stdio 模式根本不该有网络连接,出现这个说明客户端配置写成了 SSE 模式,或者 command 指向的程序启动失败。检查配置文件里有没有url字段,stdio 模式只认command和args。另外确认 Python 或 Java 的路径是绝对路径,子进程启动失败时客户端会报代理错误。

reading choices 相关报错:完整报错可能是error reading choices: unexpected end of JSON input之类。这是客户端在解析模型返回时失败了。原因通常是模型通道返回了非标准格式,或者 stdout 里混入了日志。先确认 TaoToken 的 Base URL 写的是 https://taotoken.net/api ,没有多余路径。再检查服务端代码,把所有print()改成sys.stderr.write(),保证 stdout 干净。

OAuth 相关报错:如果你用的是需要 OAuth 的客户端,可能会看到 token 过期或 scope 不足。MCP stdio 本身不涉及 OAuth,这个报错一般来自客户端连接模型通道时的认证。TaoToken 用的是 API Key 认证,不需要 OAuth 流程,所以检查客户端是不是错误地启用了 OAuth 模式。把认证方式改成 API Key 即可。

工具列表为空:客户端连上了但看不到工具。检查@app.tool装饰器有没有漏写,description 是不是空字符串。有些客户端要求 description 非空才展示。Java 版检查@ToolMapping的 description 属性。另外确认服务端启动时没有抛异常,异常信息会打到 stderr,客户端日志里能看到。

Codex auth.json 配置问题:如果你用 Codex 类工具,认证信息在auth.json里。确保里面的 base_url 指向 https://taotoken.net/api ,api_key 字段填你的 Key。这个文件权限要设好,别让其他用户读到。改完重启客户端生效。

6. 把 stdio 服务接进你的日常工作流

跑通第一个服务之后,下一步是把它用起来。几个实用方向:把数据库查询封装成工具,让模型直接读你的私有数据;把文件操作封装进去,实现“帮我找一下上周的日志”;把内部 API 封装成工具,让 Agent 自动调。每个工具就是一个@app.tool函数,加完重启进程,客户端刷新工具列表就能用。

如果你要长期跑编码类 Agent,建议了解一下 Coding Plan,它把模型通道和常用工具链打包好了,省得自己一个个配:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有各种客户端的配置示例,照着抄就行。

最后说个我踩过的坑:stdio 服务进程的生命周期由客户端管理,客户端退出时子进程可能变成僵尸进程。在服务端代码里加个信号处理,收到 SIGTERM 时干净退出。Python 里用signal.signal(signal.SIGTERM, handler),Java 里用 shutdown hook。这样反复重启客户端不会攒一堆孤儿进程。

现在你手里有一个能跑的 stdio 服务、一套客户端配置、一个统一的模型通道。接下来就是往里加工具,加一个测一个,慢慢就攒出自己的 AI 工具箱了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进