cd /news/ai-agents/build-a-runnable-mcp-loop-in-python-… · home topics ai-agents article
[ARTICLE · art-132073] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=· neutral

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

A developer has published a Python walkthrough for building a runnable MCP client-host loop, covering stdio transport, streamable HTTP, and letting an LLM choose which tool to invoke. The tutorial, adapted from a Chinese-language Cnblogs post by 花酒锄作田, demonstrates listing prompts, resources, and tools via ClientSession, calling them over stdio, and warns against exposing arbitrary remote SSH command execution to a model without allowlists, authentication, and human confirmation.

by read5 min views4 publishedSep 16, 2026

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.

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.

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

uv add mcp fastmcp

python -m pip install mcp fastmcp

Notes from the source:

mcp package ships FastMCP v1; community FastMCP has moved to v2—trying both while learning is fine. Concept: prompts, resources, and tools on one server with transport="stdio".

Illustrative shape (adapted fr@mcp.prompt()def greet_user(name: str, style: str = "formal") -> str:

"""Greet a user with a specified style."""

if style == "friendly":

    return f"Hey {name}! What's up?"

return f"Hello, {name}!"

@mcp.resource("greeting://{name}")def greeting_resource(name: str) -> str:

"""A simple greeting resource."""

return f"Hello, {name}!"

@mcp.resource("config://app")def get_config() -> str:

"""Static configuration data"""

return "App configuration here"

@mcp.tool()def add(a: int, b: int) -> int:

"""Add two numbers"""

return a + b

@mcp.tool()async def get_date() -> str:

"""Get today's date."""

return datetime.now().strftime("%Y-%m-%d")

@mcp.tool()async def get_weather(city: str) -> str:

"""Get weather for a given city."""

return f"It's always sunny in {city}!"

if name == "main":

mcp.run(transport="stdio")
**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.”

## Step 2 — Stdio Client with `ClientSession`

The Client launches the Server as a subprocess via `StdioServerParameters` (absolute interpreter, script path, and cwd). Pattern from the source:
server_params = StdioServerParameters(
    command=str(Path(__file__).parent / ".venv" / "bin" / "python"),
    args=[str(Path(__file__).parent / "demo1-server.py")],
    cwd=str(Path(__file__).parent),
)

async def run():
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()

            prompts = await session.list_prompts()
            print([p.name for p in prompts.prompts])

            tools = await session.list_tools()
            print([t.name for t in tools.tools])

            resource_content = await session.read_resource(AnyUrl("greeting://World"))
            block = resource_content.contents[0]
            if isinstance(block, types.TextResourceContents):
                print(block.text)

            result = await session.call_tool("add", arguments={"a": 5, "b": 3})
            print(result.content[0].text if result.content else result)
            print(result.structuredContent)

if __name__ == "__main__":
    asyncio.run(run())

Server change:

mcp = FastMCP("custom", host="localhost", port=8001)

if __name__ == "__main__":
    mcp.run(transport="streamable-http")

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.

Docs and ecosystem starting points:

Server stays the same. Client:

list_tools() and mapThe original uses an OpenAI-compatible client pointed at qwen-plus, compatible-mode/v1). Any OpenAI-tools-compatible endpoint works the same way (DeepSeek, OpenAI, and similar). Config sketch from the source’s supplementary modules:

{
  "llm": {
    "model": "qwen-plus",
    "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
    "api_key": "your token"
  },
  "server": {
    "host": "127.0.0.1",
    "port": 8000
  }
}

Example interactive outcomes from the original session:

get_date get_weather with {city: "合肥"} That is the whole product loop in miniature: discover → bind schemas → model proposes → your code executes → feed results back.

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.

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). The same Client you use for MCP tools is where you later hang RAG retrieval as a resource or tool—without rewriting the host when you add the next capability. Get this loop solid once, and every new tool becomes a schema change instead of a host rewrite.

English compile by YongBo Yu.

https://yongbo-yu.vercel.app · https://github.com/YongBoYu1

Original Chinese article © 花酒锄作田 / Cnblogs. Always link the source; do not present this compile as the original. each tool to an OpenAI-compatible function schema (name, description, parameters from inputSchema).

tools=.... session.call_tool(name, args), append assistant and tool messages, call the model again. Expected behavior (as reported in the original run): prompts listed, resource text returned, add yields 8 plus structured content.

Failure mode to remember: starting the Client also starts the Server. Server syntax or import errors look like Client connection failures—debug the Server first.

import asyncio
from pathlib import Path
from pydantic import AnyUrl

from mcp import ClientSession, StdioServerParameters, types
from mcp.client.stdio import stdio_client

om the original). **Trim any SSH / remote-shell tools** before you run this locally unless you harden them first:

python

from datetime import datetime

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("custom")

── more in #ai-agents 4 stories · sorted by recency
── more on @model context protocol 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/build-a-runnable-mcp…] indexed:0 read:5min 2026-09-16 ·