Build a Runnable MCP Loop in Python (stdio streamable-http LLM tool choice)

Build a Runnable MCP Loop in Python (stdio → streamable-http → LLM tool choice)

Attributed Chinese → English compile Source: [MCP][02]快速入门MCP开发 Original author: 花酒锄作田 (Cnblogs) · Posted: 2025-09-15 This is an English rewrite of the original tutorial’s ideas and code patterns. It is not original work by the compiler. Always link the Chinese source; do not present this compile as the original.

中文翻译: 来源归属: 原文:[MCP][02]快速入门MCP开发 原作者:花酒锄作田 (博客园) · 发布日期:2025-09-15 本文是对原教程思路和代码模式的英文重写,并非编译者的原创作品。请务必引用中文原文链接;请勿将此编译版本作为原创发布。


Many MCP write-ups only show how to register a Server and paste it into Cursor. The Cnblogs post by 花酒锄作田 is useful for product engineers because it also builds the Client / Host side: list prompts, resources, and tools; call them over stdio; switch to streamable HTTP; then let an LLM decide which tool to invoke. If you are shipping agents into a backend, that Client loop is the missing middle between “SDK demo” and “our service owns the tool session.” You need a reliable discover → bind → call → feed-back loop before you care which model sits on top.

中文翻译: 许多关于 MCP 的文章只展示了如何注册一个服务器并将其粘贴到 Cursor 中。花酒锄作田在博客园发布的文章对产品工程师非常有用,因为它还构建了客户端/宿主端:列出提示词、资源和工具;通过 stdio 调用它们;切换到可流式传输的 HTTP;然后让大模型(LLM)决定调用哪个工具。如果你正在将智能体(Agent)部署到后端,那么这个客户端循环就是“SDK 演示”与“我们的服务掌控工具会话”之间缺失的一环。在关心使用哪种模型之前,你需要一个可靠的“发现 → 绑定 → 调用 → 反馈”循环。


Environment

The author used Python 3.13.5 (3.11+ is fine). Prefer uv or pip:

# uv
uv add mcp fastmcp
# or pip
python -m pip install mcp fastmcp

Notes from the source: The official mcp package ships FastMCP v1; community FastMCP has moved to v2—trying both while learning is fine. Write type hints, return types, and docstrings carefully. Those become the model-facing tool descriptions later.

中文翻译:

环境准备

作者使用了 Python 3.13.5(3.11+ 均可)。建议使用 uv 或 pip:

# uv
uv add mcp fastmcp
# 或者 pip
python -m pip install mcp fastmcp

来源说明:官方 mcp 包提供的是 FastMCP v1 版本;社区版 FastMCP 已升级至 v2——学习时尝试两者均可。请仔细编写类型提示、返回类型和文档字符串(docstrings),因为它们稍后会成为模型可见的工具描述。


Step 1 — Minimal FastMCP Server (stdio)

Concept: prompts, resources, and tools on one server with transport="stdio".

中文翻译:

第一步 — 最小化 FastMCP 服务器 (stdio)

概念:在同一个服务器上使用 transport="stdio" 定义提示词、资源和工具。


(Code snippet omitted for brevity, focusing on the logic)

Preflight: run the server script alone once. If imports fail, the Client will fail in a confusing way when it tries to spawn the child process.

Production caution — SSH / god-mode shell: the original also demonstrates a remote SSH tool. Treat that as high-risk. Do not expose arbitrary remote command execution to a model without allowlists, authentication, and human confirmation. Prefer narrow, typed tools over “run anything on this host.”

中文翻译: 预检: 先单独运行一次服务器脚本。如果导入失败,客户端在尝试生成子进程时会以一种令人困惑的方式报错。

生产环境警告 — SSH / 超级权限 shell: 原文还演示了一个远程 SSH 工具。请将其视为高风险操作。在没有白名单、身份验证和人工确认的情况下,不要向模型暴露任意远程命令执行权限。相比于“在主机上运行任何命令”,请优先使用范围受限、类型明确的工具。


Step 2 — Stdio Client with ClientSession

The Client launches the Server as a subprocess via StdioServerParameters (absolute interpreter, script path, and cwd).

中文翻译:

第二步 — 使用 ClientSession 的 Stdio 客户端

客户端通过 StdioServerParameters(绝对路径解释器、脚本路径和工作目录)将服务器作为子进程启动。


Step 3 — Same Server over streamable-http

Server change: mcp = FastMCP("custom", host="localhost", port=8001) Client change (conceptually): use streamablehttp_client("http://localhost:8001/mcp"), then the same ClientSession.initialize() / list_* / call_tool flow. The source notes a third return value, get_session_id, for session management—usually unused in hello-worlds. This is the fork most product backends care about: stdio for desktop or host-local tools, HTTP for remotely deployed tool servers.

中文翻译:

第三步 — 通过 streamable-http 使用相同的服务器

服务器端修改:mcp = FastMCP("custom", host="localhost", port=8001) 客户端修改(概念上):使用 streamablehttp_client("http://localhost:8001/mcp"),然后执行相同的 ClientSession.initialize() / list_* / call_tool 流程。原文提到了一个用于会话管理的第三返回值 get_session_id——这在入门示例中通常用不到。这是大多数产品后端所关注的分支:stdio 用于桌面或主机本地工具,HTTP 用于远程部署的工具服务器。


Step 4 — Let the LLM choose tools

Server stays the same. Client: Connect (HTTP example in the post). Call list_tools() and map. The original uses an OpenAI-compatible client pointed at Qwen / DashScope. Any OpenAI-tools-compatible endpoint works the same way (DeepSeek, OpenAI, and similar).

中文翻译:

第四步 — 让大模型选择工具

服务器保持不变。客户端:连接(参考文中的 HTTP 示例)。调用 list_tools() 并进行映射。原文使用的是指向通义千问/DashScope 的 OpenAI 兼容客户端。任何兼容 OpenAI 工具调用的端点(如 DeepSeek、OpenAI 等)工作方式相同。


Example interactive outcomes from the original session: “What is today’s date?” → get_date “Weather in Hefei?” → get_weather with {city: "合肥"} Numeric compare → custom comparison tool That is the whole product loop in miniature: discover → bind schemas → model proposes → your code executes → feed results back.

中文翻译: 原文会话中的交互结果示例: “今天几号?” → get_date “合肥天气如何?” → get_weather,参数为 {city: "合肥"} 数值比较 → 自定义比较工具 这就是整个产品循环的缩影:发现 → 绑定模式 → 模型建议 → 你的代码执行 → 反馈结果。


Logging pitfall while integrating the LLM The author’s sample logger can write to a file; if you stream-log, keep protocol traffic on the MCP pipes and human logs elsewhere. Mixing debug prints into a stdio Server’s stdout will break JSON-RPC—the same lesson every serious MCP guide repeats.

中文翻译: 集成大模型时的日志陷阱 作者的示例日志记录器可以写入文件;如果你使用流式日志,请将协议流量保留在 MCP 管道上,将人类可读的日志放在其他地方。将调试打印信息混入 stdio 服务器的 stdout 会破坏 JSON-RPC——这是每一份严肃的 MCP 指南都会重复强调的教训。


Why this matters for agents / MCP / RAG products Shipping an LLM feature is less about a single chat completion and more about a reliable tool session: spawn or connect to servers, refresh schemas, bound the agent loop, and keep transports swappable (local stdio versus remote HTTP).

中文翻译: 这对智能体/MCP/RAG 产品为何重要 发布一个大模型功能,重点不在于单次的聊天补全,而在于一个可靠的工具会话:生成或连接到服务器、刷新模式、绑定智能体循环,并保持传输方式的可切换性(本地 stdio 与远程 HTTP)。