DEV Community

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)

Overview

This guide walks through building a complete MCP (Model Context Protocol) loop in Python that transitions from local stdio-based tool discovery to remote HTTP-based invocation, with an LLM orchestrating the tool selection. The pattern bridges "SDK demo" and "service-owned tool sessions," providing a reliable discover → bind → call → feedback loop before choosing the underlying LLM.

Environment Setup

The author used Python 3.13.5 (3.11+ is acceptable). For dependency installation, prefer either uv or pip:

# uv
uv add mcp fastmcp

# pip
python -m pip install mcp fastmcp

Two versions of the FastMCP library exist: the official shipment includes FastMCP v1, while the community version has moved to v2. Both are viable during learning phases.

Write type hints, return types, and docstrings carefully. These become the model-facing tool descriptions later.

Step 1 - Minimal FastMCP Server (stdio)

Concept: Prompts, resources, and tools on one server using transport="stdio".

Illustrative code structure:

from mcp import FastMCP

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

if __name__ == "__main__":
    mcp.run(transport="stdio")

Key components defined in the server:

  • greet_user(name: str, style: str = "formal") -> str - Greets a user with a specified style.
    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}!"
    
  • greeting_resource(name: str) -> str - A simple greeting resource returning "Hello, {name}".
  • get_config() -> str - Returns static configuration data ("App configuration here").
  • add(a: int, b: int) -> int - Adds two numbers.
  • get_date() -> str - Returns today's date formatted as %Y-%m-%d.
  • get_weather(city: str) -> str - Returns a fixed response like "It's always sunny in {city}!".

Step 2 - Stdio Client with ClientSession

The client launches the server as a subprocess via StdioServerParameters, which specifies the absolute interpreter, script path, and working directory.

import asyncio
from pathlib import Path
from mcp import ClientSession, StdioServerParameters, types
from mcp.client.stdio import stdio_client

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)

Key steps:

  1. Initialize the client with stdio_client(server_params).
  2. Within the context manager, open a ClientSession.
  3. Call session.initialize() to ready the session.
  4. List available prompts and tools using list_prompts() and list_tools().
  5. Read a resource via read_resource() and extract its text content.
  6. Invoke a tool using call_tool("add", arguments={...}).

Step 3 - Same Server over Streamable-HTTP

The server remains unchanged but runs on port 8001 instead of 8000. To communicate with it, swap the transport layer:

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

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

Conceptually, replace the stdio client with a streamable HTTP client:

from streamablehttp_client import streamablehttp_client

client = streamablehttp_client("http://localhost:8001/mcp")

Then proceed with the same ClientSession.initialize(), list_prompts(), list_tools(), and call_tool() workflow. The key distinction is that the server now exposes itself over HTTP rather than being accessed via stdin/stdout.

Step 4 - Let the LLM Choose Tools

The server logic stays identical regardless of transport. The client connects (via HTTP in this case) and follows this flow:

  1. Connect to the tool server.
  2. Call list_tools() to retrieve available tools.
  3. Map the LLM's output to tool names and construct arguments accordingly.
  4. Invoke the selected tool via call_tool(name, arguments=...).
  5. Feed results back to the model until no further tool calls are requested.

Example interaction outcomes from the original session:

  • "What is today's date?"get_date
  • "Weather in Hefei?"get_weather with {city: "合肥"}
  • Numeric comparison → custom comparison tool

Logging Pitfalls

When integrating an LLM, avoid mixing debug prints into the stdio server's stdout. Doing so breaks JSON-RPC communication. Keep protocol traffic confined to the MCP pipes and direct human logs elsewhere. This ensures the tool session remains stable even under verbose logging conditions.

Why This Matters for Agents / MCP / RAG Products

Shipping an LLM feature involves more than a single chat completion-it requires a reliable tool session. The core principles are:

  • Spawn or connect to servers (local stdio vs. remote HTTP).
  • Refresh schemas dynamically.
  • Bind the agent loop around these capabilities.
  • Keep transports swappable so adding new tools doesn't require rewriting the host.

Once this loop is solidified, every new tool becomes a schema change rather than a full host rewrite.

Compilation

This compilation was performed by YongBo Yu (English translation). See the original Chinese source at [MCP][02]快速入门MCP开发](https://github.com/Cnblogs/MCP-tutorial) for the original tutorial.

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.