Home / Articles

Build Your First MCP Server in Python: Tools, Resources, Prompts and Inspector

Build an MCP server in Python with FastMCP: a standup helper with tools, resources and prompts, stdio vs HTTP, Inspector debugging and one config for 3 clients.

Publisher: REACTAPP.TOP9 min read2056 words

This article is published in English.

Sources: What Is MCP? Build a Custom MCP Server in Python

Picture three nearly identical Python files, named after Claude, Cursor and ChatGPT, each calling the Slack API, parsing the response and reshaping it for one specific AI client. Now picture replacing all three with a single file that every one of those clients can read from directly. When Slack changes a field, you fix it once. That single file is an MCP server, and building one takes about an afternoon.

MCP in a few sentences

The Model Context Protocol is small enough to describe in three messages. A client connects to a server and asks what it can do. The server answers with the tools a model can call, the data a host can fetch and the prompts a user can pick. The client then calls a tool with arguments, and the server runs it and replies. On the wire, that's all of it.

What makes MCP worth using is not the message format but the shared agreement. Claude Code, Cursor, VS Code's Copilot and more all perform the same handshake, so a Python file written now can be attached to any of them later.

A server may offer any of three kinds of capability:

  • Tools are callable functions the model picks by name, for instance one that searches a database, ships a branch or condenses a log.
  • Resources are read-only material, for example a config file, a schema or recent commits, that the host fetches when it needs background.
  • Prompts are reusable templates the user selects, which show up as a slash command in Claude Code or a menu entry in Claude Desktop.

In practice most servers ship tools, and resources and prompts come second. If you learn only one of the three, learn tools. How messages travel and how authentication works matter later. For now, remember this: one server, many clients, three things to expose.

Why integrations used to multiply

Before MCP, connecting an AI tool to a backend meant writing an adapter between your backend's shape and whatever tool-calling format that client wanted. One client expected Anthropic's tools schema, another used its own format, and a third wanted OpenAI's function-calling shape. The Slack search underneath stayed identical while three wrapper layers grew around it.

The arithmetic gets ugly fast. Three clients and two backends mean six adapters to maintain. A fourth client makes eight, and a third backend makes twelve, each with its own authentication, retries and quirks. MCP breaks that product apart. Clients and backends no longer talk to each other. Both talk to the protocol, so adopting the next AI client costs a config entry instead of a new integration.

The design is deliberately lopsided. The client decides everything: which servers to load, when to call tools and how to display results. The server's duty is much smaller. It declares its tools and answers when called. That asymmetry kept the protocol compact enough for every major client to implement quickly.

A standup helper in one file

A standup helper is a classic internal tool that engineers keep rewriting. It reads the local git log and summarizes what a developer did yesterday. Here it will be exposed as a tool, a resource and a prompt in one Python file.

You need the FastMCP package, the high-level Python layer that many MCP servers sit on.

pip install fastmcp

Create standup_server.py. It begins by importing the library and defining the tool.

# standup_server.py
import subprocess
from typing import TypedDict

from fastmcp import FastMCP

mcp = FastMCP("standup-helper")


class StandupSummary(TypedDict):
    branch: str
    since: str
    commit_count: int
    commits: list[str]


@mcp.tool()
def summarize_standup(
    branch: str = "main",
    since: str = "yesterday",
) -> StandupSummary:
    """Summarize recent git activity for a standup.

    Reads the local git log on the given branch since the
    given time window. Returns commit count and one-line
    subjects for each commit. Used by AI clients via MCP.
    """
    try:
        result = subprocess.run(
            [
                "git", "log",
                f"--since={since}",
                "--pretty=format:%h %s",
                branch,
            ],
            capture_output=True,
            text=True,
            timeout=5,
            check=True,
        )
    except (subprocess.CalledProcessError,
            subprocess.TimeoutExpired) as exc:
        return {
            "branch": branch,
            "since": since,
            "commit_count": 0,
            "commits": [f"git error: {exc}"],
        }

    lines = [
        line for line in result.stdout.splitlines() if line
    ]
    return {
        "branch": branch,
        "since": since,
        "commit_count": len(lines),
        "commits": lines,
    }


# resources and prompts come next

Behind the scenes, the decorator does four jobs. It reads the type hints to produce a JSON schema, uses the docstring as the description shown to the model, lists the tool in the server's capabilities during the handshake, and routes incoming JSON-RPC calls to the function.

Note the subprocess call with a strict five-second timeout. If a git command hangs, the host application waiting for the tool response hangs with it, so every external call needs a timeout. The error branch also returns the same dictionary shape as the success branch, so the model always gets a predictable structure. A fuller tool might also call the GitHub CLI for open pull requests, by adding another subprocess call and appending the output to the dictionary.

Next come the resource and the prompt, which complete the file.

# standup_server.py (continued)

@mcp.resource("recent_commits://main")
def recent_commits_main() -> str:
    """Last 10 commits on the main branch, plain text.

    Resources are pulled by the host opportunistically.
    They are not invoked by the model the way tools are.
    """
    result = subprocess.run(
        [
            "git", "log",
            "-n", "10",
            "--pretty=format:%h %ad %s",
            "--date=short",
            "main",
        ],
        capture_output=True,
        text=True,
        timeout=5,
    )
    return result.stdout or "(no commits found)"


@mcp.prompt("standup_template")
def standup_template(focus: str = "shipping work") -> str:
    """Reusable standup question exposed as a prompt
    template. Surfaces as a slash command in clients that
    expose prompts (e.g. /standup_template in Claude Code).
    """
    return (
        f"Summarize what I worked on yesterday, focusing on "
        f"{focus}. Use the summarize_standup tool to get the "
        f"git log, then write a one-paragraph standup note."
    )


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

A resource URI is just a string. No strict schema governs its shape, though the convention is a scheme followed by a path, and FastMCP supports URI templates when parameters are needed. The prompt is simply a function returning formatted text. Clients present it as a template the user invokes, which Claude Code exposes as a slash command. Prompts are never injected automatically, so the user always chooses them.

The last line starts the server. There's no port, no web framework and no event loop to manage, because FastMCP handles the stdio plumbing.

Transports and authentication

The transport decides how JSON-RPC messages travel between client and server.

Use stdio for local servers. The host launches your script as a child process. Stdio sidesteps the entire network stack: no port, no TLS and no authentication, because the child process inherits the host's trust boundary.

Use Streamable HTTP when the server must keep state across requests or be reached from several machines. A remote server faces the network, so it needs strict authentication.

# bottom of standup_server.py

if __name__ == "__main__":
    # Default transport is stdio. The host (Claude Code,
    # Cursor, Claude Desktop, etc.) launches this script
    # as a subprocess and talks to it over stdin/stdout.
    # No port, no TLS, no auth. The trust boundary is
    # whoever launched the host.
    mcp.run()

    # To expose the same server over the network instead,
    # use Streamable HTTP. SSE was deprecated in the
    # March 2025 spec update. Do not use it for new code.
    #
    # Production HTTP also needs an auth layer in front.
    # OAuth 2.1 with Dynamic Client Registration is the
    # current pattern. See Week 22 for the full flow.
    #
    # mcp.run(
    #     transport="streamable-http",
    #     host="0.0.0.0",
    #     port=8000,
    # )

Calling run with no arguments means stdio, and the commented block shows what the HTTP upgrade looks like. This build stays on stdio, which is the right choice for most internal developer tools. A tool that only reads local files or a local repository shouldn't sit behind a network interface at all.

The local development loop

Don't test a new server by plugging it straight into an AI client. Clients are built to be resilient. They silently swallow schema mismatches and ignore missing capabilities to keep the chat working, and when a tool fails, the model simply apologizes.

Use the official MCP Inspector instead, a browser-based development UI that talks to your server over stdio.

npx @modelcontextprotocol/inspector python standup_server.py

The command launches your script as a subprocess and opens a local dashboard. You can read the raw handshake, see exactly which capabilities your server advertised, and run tools and resources live.

Three problems tend to appear in this loop:

  1. Schema mismatch. The function's return value disagrees with its type hint, and the Inspector points it out right away.
  2. Missing capability. You call something the server never advertised, and the dashboard shows what was actually registered during negotiation.
  3. Output pollution. Any stray print() damages the JSON-RPC stream, since stdio carries the messages. Route logs to standard error with Python's logging module, which the protocol reserves for readable diagnostics.

FastMCP also offers a development mode that restarts the server when the file changes, which helps while you iterate in the Inspector.

One server, several clients

Now the design pays off. There's a single Python file, verified in the Inspector, and it can be attached to different AI clients without any client-specific code. Each client reads a config file naming the local servers to launch, and the JSON shape is the same across Claude Code, Cursor and Claude Desktop: an mcpServers object holding the command to run and its arguments.

{
  "mcpServers": {
    "standup-helper": {
      "command": "python",
      "args": ["/Users/you/code/standup_server.py"]
    }
  }
}
{
  "mcpServers": {
    "standup-helper": {
      "command": "python",
      "args": ["/Users/you/code/standup_server.py"]
    }
  }
}
{
  "mcpServers": {
    "standup-helper": {
      "command": "python",
      "args": ["/Users/you/code/standup_server.py"]
    }
  }
}

The key, the command and the arguments are identical in all three. Only the location of the config file differs per client.

Open any of them and ask for a standup. The client spawns the Python process, negotiates capabilities over stdio, shows the summary tool to the model and routes the model's call back to your script. Git runs, the dictionary returns and the model writes the standup, along the same code path every time.

ChatGPT isn't in this list because its connectors work with remote servers over Streamable HTTP. Wiring this helper into it would require a public URL and an authentication layer, a very different operational profile from this strictly local build.

When not to reach for MCP

The protocol isn't a universal hammer. Some situations are a poor fit:

  • Very latency-sensitive paths. Every tool call adds a JSON-RPC round trip, so a tool inside a tight inner loop should use a direct SDK integration.
  • Custom interactive output. If you need a specific chart or a custom form, you're building a host integration, because the protocol separates data from presentation.
  • Setups you fully control. If a single team builds both the AI client and the backend, wiring them together directly can be easier to maintain than inserting MCP in between.

Small protocol, big shift

Three primitives, two transports and one JSON-RPC handshake make up the whole protocol. Being that small is the strength: it separates people who build the intelligence from people who supply context. Writing a first server takes an afternoon and pays off every time another AI tool appears.