All posts
mcppython

MCP With Python: A Practical Guide for Full-Stack Developers

A practical guide to building Model Context Protocol servers in Python using the official SDK, tool definitions, and async patterns.

SR

Suhail Roushan

August 6, 2026

·
5 min read
·
0 views

Python's MCP SDK leans heavily on decorators and type hints, which means writing a well-typed Python function is most of the work — the SDK handles turning that function into a properly schema-described tool a model can discover and call correctly.

The official Python MCP SDK provides a FastMCP server class that turns decorated Python functions into MCP tools, automatically generating parameter schemas from type hints. It's built on Python's async ecosystem, integrating naturally with existing async codebases (data pipelines, ML services, FastAPI applications) that want to expose capabilities to AI clients.

Why MCP With Python Matters (and When to Skip It)

For Python-heavy domains — data science, ML model serving, scientific computing — MCP lets you expose existing Python functions and pipelines directly to AI clients without rewriting them in another language or building a separate API layer specifically for AI consumption, taking advantage of Python's strength in exactly the domains where AI-assisted workflows are often most valuable.

Skip building a Python MCP server if your actual application logic lives in a different language/service — bridging through Python unnecessarily adds a translation layer; expose tools from whichever service actually owns the relevant logic and data.

Getting Started with MCP and Python

A minimal server using FastMCP, with type hints driving automatic schema generation:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("data-analysis")

@mcp.tool()
def get_summary_stats(dataset_name: str, column: str) -> dict:
    """Return summary statistics for a column in a named dataset."""
    df = load_dataset(dataset_name)
    return {
        "mean": float(df[column].mean()),
        "std": float(df[column].std()),
        "count": int(df[column].count()),
    }

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

An async tool, for I/O-bound operations:

@mcp.tool()
async def fetch_latest_metrics(service_name: str) -> dict:
    """Fetch the latest metrics for a named service from the monitoring API."""
    async with httpx.AsyncClient() as client:
        response = await client.get(f"https://metrics.internal/{service_name}")
        return response.json()

Core MCP With Python Concepts Every Developer Should Know

Type hints drive automatic parameter schema generation, meaning the accuracy of your tool's schema (and therefore how reliably a model calls it correctly) depends directly on how precisely you type your function signature — using specific types (Literal["asc", "desc"] instead of a bare str) produces a more constrained, more reliable schema than loose typing.

Docstrings become the tool's description, which the model uses to decide when and how to invoke the tool — a vague or missing docstring leads to unreliable tool selection, the same correctness issue that applies in any MCP SDK, but especially easy to overlook in Python where docstrings are sometimes treated as optional.

Both sync and async functions are supported as tools, and using async def for I/O-bound operations (API calls, database queries) lets the server handle concurrent tool calls efficiently, consistent with how you'd write any async Python I/O code — sync functions are fine for CPU-bound or trivial operations but block the event loop if used for slow I/O.

Pydantic models can define more complex structured input/output, giving you validation and richer schema generation beyond simple type hints for tools with non-trivial parameter shapes:

from pydantic import BaseModel

class QueryParams(BaseModel):
    dataset: str
    filters: dict[str, str] = {}

@mcp.tool()
def query_dataset(params: QueryParams) -> list[dict]:
    """Query a dataset with optional filters."""
    return run_query(params.dataset, params.filters)

Common Mistakes Building MCP Servers in Python and How to Fix Them

Mistake 1: loose or missing type hints, producing a vague parameter schema that leads to unreliable model tool calls. Fix: type function signatures precisely, using Literal, Enum, or Pydantic models to constrain valid inputs where appropriate.

Mistake 2: using synchronous functions for slow I/O operations, blocking the server's event loop and degrading responsiveness for concurrent tool calls. Fix: use async def and an async HTTP/database client for any I/O-bound tool.

Mistake 3: missing or vague docstrings, leaving the model without enough information to reliably decide when to use a tool or how to fill its parameters. Fix: write clear, specific docstrings describing exactly what the tool does and any important constraints on its use.

When Should You Use FastMCP Instead of Implementing the Protocol Manually?

Use FastMCP (the standard approach) for virtually all Python MCP server development — it handles protocol mechanics, schema generation, and transport correctly, and there's little reason to implement the raw protocol yourself unless you have a genuinely unusual requirement the SDK doesn't support. Implement lower-level protocol handling only for edge cases requiring behavior the high-level SDK doesn't expose.

MCP With Python in Production

Type function signatures precisely and write specific docstrings from the start, since both directly affect tool-calling reliability and are easy to under-invest in during initial development. Also use async functions consistently for I/O-bound tools, since a single blocking sync call in a Python MCP server can degrade responsiveness for all concurrent tool invocations, not just the slow one.

If you have existing Python data or ML functions that would benefit from AI-assistant access, wrapping them with @mcp.tool() decorators is close to the lowest-friction path to exposing them correctly.

Related posts

Written by Suhail Roushan — Full-stack developer. More posts on AI, Next.js, and building products at suhailroushan.com/blog.

Get in touch