VOL. 1NO. ----Sat24.10.26EDITION--:--

LOADING · TRADINGVIEW

AIAITutorial NO. ---

How to build an MCP server in Python: a step-by-step guide

A hands-on guide to building a Model Context Protocol server in Python: set up the official SDK, expose a tool and a resource, test with the MCP Inspector, connect it to Claude and apply the security rules the spec requires.

By Published 10 min read
Text editor showing Python source code next to a matplotlib plot window on a computer screen
MikeRun / Wikimedia Commons / CC BY-SA 4.0 · Source

Key takeaways

  • To build an MCP server in Python you need the official mcp package (version 2.0.0 or newer for the current quickstart), a recent Python release and, ideally, the uv project manager.
  • The SDK's MCPServer class turns ordinary Python functions into MCP tools and resources, reading type hints and docstrings to generate the schema.
  • Test every server with the MCP Inspector before connecting it to a client such as Claude Desktop or Claude Code.
  • For a stdio server, never write to standard output with print(): it corrupts the JSON-RPC stream. Log to stderr instead.
  • The specification says servers must validate tool inputs, apply access controls, rate-limit calls and sanitize outputs. This guide shows a small file-reading server that follows those rules.

How to build an MCP server in Python: what you are making

To build an MCP server in Python, install the official mcp package (version 2.0.0 or newer), use the SDK's MCPServer class to turn ordinary functions into tools and resources, and test it with the MCP Inspector before connecting a client such as Claude Desktop.

The protocol is back in the news this week. At Figma's Config conference in India, co-founder Dylan Field told TechCrunch, "I also think that MCP is not a perfect protocol." Figma announced 20 new MCP partners at the same event, so even critics are building on it. If you want an assistant to read your files, query your database or call your internal API, an MCP server is the standard way to connect them.

In this tutorial you will build a small server called notes in Python. It exposes one folder of text notes to an AI client and does three things:

  • a tool called search_notes that the model can call to find notes containing a word;
  • a tool called read_note that returns the text of a single note;
  • a resource at notes://index that lists the available notes as read-only context.

The server only reads files. It cannot write, delete or leave its folder. That restriction is the main security idea in the guide.

The three building blocks of an MCP server

The official documentation describes three kinds of capability a server can offer. Knowing which one fits your use case saves redesign later.

PrimitiveWhat it isWho decides when it is usedExample in this guide
ToolsFunctions the language model can call, with a JSON Schema for their inputsThe model, ideally with a human able to deny the callsearch_notes, read_note
ResourcesFile-like data the client can read, identified by a URIThe client applicationnotes://index
PromptsPre-written templates that help users complete specific tasksThe userNot used here

Source: Model Context Protocol documentation, "Build an MCP server" and specification version 2026-07-28 (server tools).

Step 1: Install Python, uv and the MCP SDK

The official Python quickstart asks for Python 3.10 or higher and the Python MCP SDK 2.0.0 or higher. The SDK's GitHub README currently lists Python 3.11 or newer, so 3.11 or later is the safe choice. The quickstart uses uv, a fast Python package and project manager. On macOS or Linux, install it with:

curl -LsSf https://astral.sh/uv/install.sh | sh

On Windows PowerShell:

powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

Restart your terminal so the uv command is found. Then create a project and add the SDK with its command-line extra:

uv init notes-server
cd notes-server
uv venv
source .venv/bin/activate   # Windows: .venv\Scripts\activate
uv add "mcp[cli]"

If you use pip, run pip install "mcp[cli]" instead. According to the README, the cli extra installs the mcp command-line tool, which you will use for testing. Plain mcp works if you do not need it.

One version note: many older blog posts import FastMCP from mcp.server.fastmcp. Those posts were written for the 1.x SDK. The current README and quickstart use the MCPServer class from mcp.server, and that is the class this guide uses.

Step 2: Write your first tool and resource

Create a folder called notes next to your project and add a couple of .md or .txt files to it. Then create server.py:

import logging
from pathlib import Path

from mcp.server import MCPServer

# Logs go to stderr, which is safe for stdio servers.
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

NOTES_DIR = (Path(__file__).parent / "notes").resolve()
ALLOWED_SUFFIXES = {".md", ".txt"}
MAX_CHARS = 20_000

mcp = MCPServer("notes")


def safe_path(name: str) -> Path:
    """Resolve a note name and refuse anything outside NOTES_DIR."""
    candidate = (NOTES_DIR / name).resolve()
    if not candidate.is_relative_to(NOTES_DIR):
        raise ValueError("Note name must stay inside the notes folder.")
    if candidate.suffix not in ALLOWED_SUFFIXES:
        raise ValueError("Only .md and .txt notes can be read.")
    if not candidate.is_file():
        raise ValueError(f"No note called {name!r}.")
    return candidate


@mcp.tool()
def search_notes(query: str) -> list[str]:
    """Return the names of notes that contain the query text.

    Args:
        query: Word or phrase to look for (2 to 100 characters).
    """
    query = query.strip()
    if not 2 <= len(query) <= 100:
        raise ValueError("Query must be 2 to 100 characters long.")
    logger.info("search_notes called")
    hits = []
    for path in sorted(NOTES_DIR.iterdir()):
        if path.suffix in ALLOWED_SUFFIXES:
            text = path.read_text(encoding="utf-8", errors="ignore")
            if query.lower() in text.lower():
                hits.append(path.name)
    return hits[:50]


@mcp.tool()
def read_note(name: str) -> str:
    """Return the text of one note.

    Args:
        name: File name of the note, for example "ideas.md".
    """
    path = safe_path(name)
    logger.info("read_note called for %s", path.name)
    return path.read_text(encoding="utf-8", errors="ignore")[:MAX_CHARS]


@mcp.resource("notes://index")
def notes_index() -> str:
    """List the notes this server can read."""
    names = [p.name for p in sorted(NOTES_DIR.iterdir())
             if p.suffix in ALLOWED_SUFFIXES]
    return "\n".join(names) or "No notes yet."


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

A few things are worth noticing:

  • The decorators do the protocol work. @mcp.tool() registers a function as a tool, and @mcp.resource("notes://index") registers a readable resource. The quickstart says MCPServer uses type hints and docstrings to build the tool definitions automatically, so write clear docstrings. The model reads them when it decides which tool to call.
  • Resources can take parameters. The SDK README shows a template such as @mcp.resource("greeting://{name}"), where the {name} part becomes a function argument.
  • The transport is stdio. The client launches your script as a subprocess and exchanges JSON-RPC messages over its standard input and output. That is why print() must stay out of the file.

Run it once to check for syntax errors with uv run server.py. It will start and wait silently for a client. Stop it with Ctrl+C.

Step 3: Test the server with the MCP Inspector

The MCP Inspector is the project's reference tool for testing and debugging servers. The quickest route comes with the SDK's CLI extra:

uv run mcp dev server.py

This opens the Inspector in your browser, connected to your server. You can also run the Inspector package directly. It needs Node.js 22.19.0 or newer and runs through npx without installation:

npx @modelcontextprotocol/inspector uv run server.py

In the web interface, list the tools and resources, call search_notes with a word from one of your notes, and read notes://index. Then try to break it. Call read_note with ../server.py or /etc/passwd and confirm the server returns an error instead of the file. That test is the most important one in this guide.

For automated checks, the Inspector also has a command-line mode. According to its documentation, npx @modelcontextprotocol/inspector --cli followed by your server command and --method tools/list lists the tools and exits, which is useful in CI pipelines. Note one warning from the docs: on machines without an OS keychain, such as headless Linux servers, the Inspector may save secrets to an unencrypted file at ~/.mcp-inspector/secrets.json.

Step 4: Connect the server to Claude Desktop or Claude Code

Any MCP client can launch your server. The official quickstart uses Claude for Desktop. Open its configuration file. Create it if it does not exist:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %AppData%\Claude\claude_desktop_config.json
  • Linux: ~/.config/Claude/claude_desktop_config.json

Add your server under the mcpServers key, using the absolute path to your project folder:

{
  "mcpServers": {
    "notes": {
      "command": "uv",
      "args": [
        "--directory",
        "/ABSOLUTE/PATH/TO/notes-server",
        "run",
        "server.py"
      ]
    }
  }
}

Save the file and restart Claude for Desktop. The documentation warns that you may need the full path to the uv executable in command. Find it with which uv on macOS or Linux, or where uv on Windows. On Windows, use double backslashes in JSON paths.

In Claude Code, the terminal coding tool, you can register the same server from the command line:

claude mcp add --transport stdio notes -- uv --directory /ABSOLUTE/PATH/TO/notes-server run server.py

Ask the assistant something like "Which of my notes mention invoices?" and it should ask permission to call search_notes.

Step 5: Choose stdio or Streamable HTTP

According to the SDK README, it supports every standard transport: stdio, Streamable HTTP and SSE. Streamable HTTP is the one to deploy as a network service.

TransportBest forHow to start itLogging to stdout
stdioLocal servers launched by a desktop client or IDEmcp.run(transport="stdio")Forbidden: breaks the JSON-RPC stream
Streamable HTTPRemote or shared serversuv run mcp run server.py --transport streamable-httpAllowed

Source: MCP Python SDK README; "Build an MCP server" quickstart.

Start with stdio. A local process that only your own client can reach is much easier to secure. Move to HTTP only when other people or machines need the server, and add authentication when you do.

Step 6: Security practices that matter

An MCP server gives a language model the ability to act. The tools section of the specification sets four duties for servers: they must validate all tool inputs, implement proper access controls, rate-limit tool invocations and sanitize tool outputs. It also says there should always be a human in the loop who can deny tool invocations. In practice that means:

  1. Validate every argument. Treat tool arguments like form input from an untrusted website. The safe_path() helper resolves the path and rejects anything outside the notes folder. That blocks the classic ../ path-traversal trick. Length limits on query do the same job for text.
  2. Use least privilege. Give the server only the access its job needs. This one reads files and has no write or delete tool. If you connect a database, use a read-only account. If you call an API, use a token with the narrowest scope available.
  3. Keep secrets out of tool output. Load API keys from environment variables and never return them, along with stack traces or configuration files, in a tool result. Anything a tool returns ends up in the model's context and may be shown to the user or sent elsewhere. The output cap (MAX_CHARS) also limits how much data one call can pull out.
  4. Return clear errors. The specification separates protocol errors from tool execution errors. Execution errors are returned with isError: true so the model can correct itself and retry. A helpful message such as "Query must be 2 to 100 characters long" works better than a crash.
  5. Add rate limits and logs. A simple per-minute counter in front of expensive tools prevents runaway loops. Log which tool was called and when, to stderr or a file. Do not log the full content returned.
  6. Treat retrieved text as data. Notes, web pages and tickets can contain hidden instructions aimed at the model. That is known as prompt injection. Your server cannot fully prevent it, which is why destructive actions should always require a human confirmation in the client.

Recent incidents show why this matters. Bullquill has reported on OpenAI's disclosure that its coding agents weakened internal monitoring. Agents with broad permissions can do things their operators did not intend. Narrow tools are the cheapest protection.

Common errors and how to fix them

  • The client shows no tools. Check that the path in the configuration is absolute and that uv is on the client's PATH. If it is not, use the full path. Restart the client after every configuration change.
  • "Unexpected token" or JSON parse errors. Something wrote to stdout. Remove print() calls and any library output sent to stdout.
  • ImportError for MCPServer. You probably have a 1.x SDK installed. Upgrade with uv add "mcp[cli]" or update your pin, or follow the SDK's migration guide.
  • Tool arguments arrive with the wrong type. Add precise type hints and describe each argument in the docstring. The generated JSON Schema depends on them.

What it means

Building an MCP server in Python now takes one package, one class and a few decorators. The hard part is choosing what to expose. Protocol and SDK versions change quickly: the current documentation is versioned 2026-07-28, and the Python SDK has moved to a 2.x API. Pin your SDK version and recheck the official docs when you upgrade.

The design rule that ages best is to keep each server small, give it the fewest permissions that work, and test it with the Inspector the way an attacker would. A server that can only read one folder is easy to approve. A server that can run shell commands on your laptop is not.

If you want to keep the whole stack on your own machine, pair this server with a local model; see how to run a local LLM with Ollama. For cloud models, our Gemini 4 Argon API tutorial covers keys and requests. For the wider shift in how AI providers distribute their models and agents, read OpenAI bringing its models and Codex to Google Cloud.

Frequently asked questions

What is an MCP server?

An MCP server is a program that exposes tools, resources or prompts to AI applications through the Model Context Protocol, an open standard Anthropic introduced in 2024 and contributed to the Linux Foundation's Agentic AI Foundation in December 2025.

Which Python version do I need to build an MCP server?

The official quickstart asks for Python 3.10 or higher with MCP SDK 2.0.0 or newer, while the SDK README lists Python 3.11 or newer. Using Python 3.11 or later satisfies both.

How do I test an MCP server without Claude?

Use the MCP Inspector. Run 'uv run mcp dev server.py' from the SDK's CLI extra, or 'npx @modelcontextprotocol/inspector uv run server.py' with Node.js 22.19.0 or newer, then list and call your tools in the browser.

Why does print() break my MCP server?

A stdio server exchanges JSON-RPC messages over standard output, so anything printed to stdout corrupts the stream. Use Python's logging module, which writes to stderr by default.

Sources

AIMore in AI

See all

Latest elsewhereDate · Title · Reading time

  1. Kering Q3 2026 results: Gucci sales fall 6%, revenue flat at €3.3bn

    Kering's third-quarter revenue held at €3.345 billion, but Gucci's 9% drop in its own stores, at least 125 planned store closures and a missing 2026 growth goal sent the shares down about 16% in Paris.

    BIZ · 7 MIN
  2. Strategy STRC daily dividend vote on Oct 28 decides $1.00 payout

    Strategy's board has pre-declared conditional daily STRC dividends for November and raised its preferred buyback authorization to $3.0 billion from $2.0 billion, with $1.16 billion available as of October 18.

    CRYP · 6 MIN
  3. Aston Villa vs Man City live: Jackson puts Villa 1-0 up

    Jackson pounced after Marc Guéhi lost the ball near his own box, giving Villa an early lead over Enzo Maresca's league leaders in Saturday's lunchtime game.

    CULT · 5 MIN
  4. Microsoft, Alphabet, Amazon earnings dates: Oct 28 and 29

    Microsoft and Alphabet report on the same afternoon as the Fed decision, with Amazon a day later and Apple on November 2. This tracker lists confirmed call times, last quarter's capex and cloud numbers, and why AI spending is the week's market focus.

    MKT · 8 MIN