Home Blog Contact
Home/Blog/How to Build an MCP Server in Python with Fas…
How toLLM EngineeringMCPPythonClaude

How to Build an MCP Server in Python with FastMCP

8 min readBy Miloš Mitrović

To build an MCP server in Python, install the official SDK with uv add "mcp[cli]", create a FastMCP instance, decorate ordinary Python functions with @mcp.tool() so their type hints and docstrings become the tool schema, and start it with mcp.run(transport="stdio"). That gives you a working server an MCP host like Claude for Desktop can launch and call. This guide walks the full path: a minimal server first, a real API-backed tool second, then testing and connecting it to a host.

Key takeaways

  • The short answer: install mcp[cli], wrap functions in @mcp.tool(), and run mcp.run(transport="stdio").
  • FastMCP reads your function's type hints and docstring to auto-generate the tool's input schema and description, so you write plain Python, not JSON schema by hand.
  • For stdio servers, never write to stdout with print(). It corrupts the JSON-RPC stream and breaks the server. Log to stderr instead.
  • Test before wiring anything up: npx @modelcontextprotocol/inspector runs an interactive UI against your server.
  • You need Python 3.10 or higher and MCP Python SDK 1.2.0 or higher.
  • To use the server in a host, add it under the mcpServers key of the host's config with an absolute path, then restart the host.

What you need

  • Python 3.10 or higher.
  • The uv package manager (recommended by the MCP docs). Pip works too if you prefer it.
  • The MCP Python SDK, version 1.2.0 or higher, published on PyPI as mcp.
  • An MCP host to consume the server, such as Claude for Desktop. The server itself runs anywhere Python does.

Build and run an MCP server in Python

Follow these steps in order. By the end you will have a server running over stdio that a host can call.

  1. Create the project and install the SDK. Using uv:
    uv init weather
    cd weather
    uv venv
    source .venv/bin/activate
    uv add "mcp[cli]" httpx
    touch weather.py
    If you use pip instead, the equivalent is python -m venv .venv && source .venv/bin/activate followed by pip install "mcp[cli]" httpx.
  2. Write a minimal server to confirm the wiring. Put this in weather.py. It exposes one tool and runs over stdio:
    from mcp.server.fastmcp import FastMCP
    
    mcp = FastMCP("weather")
    
    @mcp.tool()
    def add(a: int, b: int) -> int:
        """Add two integers and return the result."""
        return a + b
    
    if __name__ == "__main__":
        mcp.run(transport="stdio")
    The string passed to FastMCP names the server. The decorator turns add into a callable tool; its signature becomes the input schema and its docstring becomes the description the model sees.
  3. Run it. Start the server with:
    uv run weather.py
    A stdio server does not print a banner. It sits waiting for JSON-RPC messages on standard input, which is correct. To interact with it, use the Inspector in the next step rather than typing into the terminal.
  4. Test it with the MCP Inspector. In a second terminal, from the project folder, run:
    npx @modelcontextprotocol/inspector uv --directory . run weather.py
    This opens a browser UI. Open the Tools tab, select add, pass two numbers, and confirm you get the sum back. This proves the server speaks MCP correctly before you connect any host.
  5. Add a real, useful tool. Replace the add stub with a tool that calls an external API. This one queries the US National Weather Service for active alerts:
    from typing import Any
    import httpx
    from mcp.server.fastmcp import FastMCP
    
    mcp = FastMCP("weather")
    NWS_API_BASE = "https://api.weather.gov"
    USER_AGENT = "weather-app/1.0"
    
    async def make_nws_request(url: str) -> dict[str, Any] | None:
        headers = {"User-Agent": USER_AGENT, "Accept": "application/geo+json"}
        async with httpx.AsyncClient() as client:
            try:
                r = await client.get(url, headers=headers, timeout=30.0)
                r.raise_for_status()
                return r.json()
            except Exception:
                return None
    
    @mcp.tool()
    async def get_alerts(state: str) -> str:
        """Get active weather alerts for a US state.
    
        Args:
            state: Two-letter US state code, for example CA or NY.
        """
        url = f"{NWS_API_BASE}/alerts/active/area/{state}"
        data = await make_nws_request(url)
        if not data or "features" not in data:
            return "Unable to fetch alerts or no alerts found."
        if not data["features"]:
            return "No active alerts for this state."
        events = [f.get("properties", {}).get("event", "Unknown") for f in data["features"]]
        return "\n".join(events)
    
    if __name__ == "__main__":
        mcp.run(transport="stdio")
    Note that tools can be async. FastMCP handles both sync and async functions. The Args: block in the docstring documents each parameter for the model.
  6. Connect the server to a host. For Claude for Desktop, open its config file (create it if it does not exist) at ~/Library/Application Support/Claude/claude_desktop_config.json on macOS, or %AppData%\Claude\claude_desktop_config.json on Windows, and add your server under mcpServers:
    {
      "mcpServers": {
        "weather": {
          "command": "uv",
          "args": ["--directory", "/ABSOLUTE/PATH/TO/weather", "run", "weather.py"]
        }
      }
    }
    Use the absolute path to the project folder. Get it with pwd. Save the file and restart the host completely. The server's tools then appear in the host and the model can call them with your approval.

How FastMCP turns functions into tools

The reason this is so short is that FastMCP does the schema work for you. When you decorate a function with @mcp.tool(), it inspects the signature and builds the tool's JSON schema from your type hints, uses the docstring as the tool description, and marks parameters without defaults as required. This is why clear type hints and a precise docstring matter more than usual: they are the interface the model reasons over when it decides whether and how to call your tool.

Tools are one of three capabilities a server can expose. Resources are read-only, file-like data the client can load into context. Prompts are reusable templates a user can invoke. Most servers start with tools because tools are the actions a model takes. The server concepts reference covers resources and prompts in depth once you need them.

Sync versus async, and when to reach for HTTP transport

Use a plain def for fast, in-process work and async def for anything that waits on a network or disk, so a slow call does not block the server. The examples above use stdio transport, which is right when the host launches the server as a local subprocess. If you want to host the server as a standalone service that multiple clients reach over the network, run it with an HTTP transport instead. Unlike stdio, HTTP transports allow normal stdout logging because output does not share a channel with the protocol.

Troubleshooting

The failures below account for most first-run problems.

  • The server connects but immediately disconnects, or the host shows a JSON parse error. Something wrote to stdout. In a stdio server, a stray print(), a library banner, or a debug statement corrupts the JSON-RPC stream. Route all logging to stderr, for example print("msg", file=sys.stderr) or the standard logging module, which writes to stderr by default.
  • The host cannot start the server. The command in the config is usually the culprit. The host does not inherit your shell PATH, so uv may not be found. Put the absolute path to the executable in command (find it with which uv), and make sure the --directory path is absolute, not relative.
  • Tools do not appear in the host. Confirm the config is valid JSON, that the server is nested under the mcpServers key, and that you fully restarted the host after saving. A running host does not pick up config changes live.
  • The model calls the tool with the wrong arguments. Tighten the type hints and rewrite the docstring. The model only knows what the generated schema and description tell it, so vague names and missing Args: lines lead to bad calls.
  • Import error on mcp.server.fastmcp. Your SDK is too old. Upgrade to 1.2.0 or higher with uv add "mcp[cli]" or pip install --upgrade "mcp[cli]".

What to do next

  • Read the official Build an MCP server tutorial for the full weather example, including a second forecast tool.
  • Add resources and prompts using the server concepts reference once tools alone are not enough.
  • Keep the MCP Inspector in your loop: change code, rebuild, reconnect, retest. It is faster than debugging through a host.
  • Study the Python SDK repository for lower-level control when FastMCP's defaults are not enough.
  • Review the host-side setup in connect local servers if the config step gives you trouble.

If you are building servers to feed an agent loop, see my notes on agentic orchestration with the Anthropic APIs and on what MCP means for enterprise AI. If your tools wrap a local model rather than a remote API, running LLMs locally with Ollama pairs naturally with an MCP server.

Sources

M
Miloš Mitrović
Email Marketing for Ecommerce

Have a question or a project?

Whether it is about this post or a system you want built, I'm happy to talk.

Get in touch

404

Post not found. It may have been moved or the link is incorrect.

← Back to the blog
Summarize with AI
ChatGPT, Perplexity, and Grok open with the prompt ready to run. Claude, Gemini, and Copilot open a chat with the prompt copied; press Ctrl+V (Cmd+V on Mac) to paste. The full text is included, so it works even without web access.