> ## Content Index
> Fetch the complete content index at: https://bullquill.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# How to build an MCP server in Python: a step-by-step guide
- URL: https://bullquill.com/how-to-build-mcp-server-python/
- Published: 2026-10-22T07:14:44.000Z
- Updated: 2026-10-23T13:28:40.000Z
- Description: 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.
- Author: Rowan Ashby
- Tags: AI, Tutorial, How-To, MCP, Anthropic, Python, Claude, AI Agents, #lang-en, #evergreen, #tr-how-to-build-mcp-server-python

## 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.

| Primitive | What it is                                                                 | Who decides when it is used                           | Example in this guide     |
| --------- | -------------------------------------------------------------------------- | ----------------------------------------------------- | ------------------------- |
| Tools     | Functions the language model can call, with a JSON Schema for their inputs | The model, ideally with a human able to deny the call | search\_notes, read\_note |
| Resources | File-like data the client can read, identified by a URI                    | The client application                                | notes://index             |
| Prompts   | Pre-written templates that help users complete specific tasks              | The user                                              | Not 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.

| Transport       | Best for                                          | How to start it                                      | Logging to stdout                     |
| --------------- | ------------------------------------------------- | ---------------------------------------------------- | ------------------------------------- |
| stdio           | Local servers launched by a desktop client or IDE | mcp.run(transport="stdio")                           | Forbidden: breaks the JSON-RPC stream |
| Streamable HTTP | Remote or shared servers                          | uv run mcp run server.py --transport streamable-http | Allowed                               |

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](https://bullquill.com/openai-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](https://bullquill.com/how-to-run-local-llm-with-ollama/). For cloud models, our [Gemini 4 Argon API tutorial](https://bullquill.com/how-to-use-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](https://bullquill.com/openai-models-codex-come-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

- [Model Context Protocol: Build an MCP server (Python quickstart)](https://modelcontextprotocol.io/docs/develop/build-server?ref=bullquill.com)
- [Model Context Protocol Python SDK on GitHub (README)](https://github.com/modelcontextprotocol/python-sdk?ref=bullquill.com)
- [Model Context Protocol specification](https://modelcontextprotocol.io/specification?ref=bullquill.com)
- [MCP specification 2026-07-28: Tools (security considerations)](https://modelcontextprotocol.io/specification/2026-07-28/server/tools?ref=bullquill.com)
- [Model Context Protocol: MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector?ref=bullquill.com)
- [Anthropic: Donating the Model Context Protocol and establishing the Agentic AI Foundation](https://anthropic.com/news/donating-the-model-context-protocol-and-establishing-of-the-agentic-ai-foundation?ref=bullquill.com)
- [Linux Foundation: Formation of the Agentic AI Foundation](https://www.linuxfoundation.org/press/linux-foundation-announces-the-formation-of-the-agentic-ai-foundation?ref=bullquill.com)
- [TechCrunch: Figma co-founder Dylan Field thinks MCP is not a perfect protocol](https://techcrunch.com/2026/10/20/figma-co-founder-dylan-field-thinks-mcp-is-not-a-perfect-protocol/?ref=bullquill.com)