← Blog

July 19, 2026 · 9 min read

How to Build an MCP Server with Python

The Model Context Protocol has official SDKs for TypeScript and Python. If you want Claude, Cursor, or any MCP client to call your Python code — a database query, an internal API, a calculation — building an MCP server is the right move. This guide walks through it from scratch.

What you'll build

A working MCP server in Python that exposes a tool. You'll be able to connect it to Claude Desktop or any stdio MCP client. The same pattern extends to any tool you want to expose — databases, APIs, file systems, internal data.

Prerequisites

Python 3.10+. That's it. The MCP SDK is a pip install.

1. Install the MCP SDK

pip install mcp

If you're using uv (recommended for MCP servers since Claude Desktop uses it for subprocess management):

uv init my-mcp-server
cd my-mcp-server
uv add mcp

2. Write the server

Create server.py:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("my-server")

@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two numbers together."""
    return a + b

@mcp.tool()
def greet(name: str) -> str:
    """Return a greeting for the given name."""
    return f"Hello, {name}!"

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

That's a complete MCP server. FastMCP reads the function signatures and docstrings to generate the tool definitions automatically. The @mcp.tool() decorator registers each function as an MCP tool.

3. Test it locally

Run the server to confirm it starts without errors:

python server.py

You should see it start in stdio mode. Use MCP Inspector to send test requests:

npx @modelcontextprotocol/inspector python server.py

MCP Inspector opens a local UI where you can call your tools manually and see the results — the fastest way to confirm your server works before connecting it to a real client.

4. Connect to Claude Desktop

Add the server to ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):

{
  "mcpServers": {
    "my-server": {
      "command": "python",
      "args": ["/absolute/path/to/server.py"]
    }
  }
}

If you're using uv:

{
  "mcpServers": {
    "my-server": {
      "command": "uv",
      "args": ["run", "/absolute/path/to/server.py"]
    }
  }
}

Restart Claude Desktop. You should see your server appear in the tools panel (the hammer icon). Claude can now call your tools.

5. Add a real tool

Here's a more realistic example — a tool that queries a SQLite database:

import sqlite3
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("db-server")
DB_PATH = "/path/to/your/database.db"

@mcp.tool()
def query_db(sql: str) -> list[dict]:
    """Run a read-only SQL query and return results as a list of rows."""
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    cursor = conn.execute(sql)
    rows = [dict(row) for row in cursor.fetchall()]
    conn.close()
    return rows

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

Now Claude can query your SQLite database in natural language. You'd ask "how many users signed up last week?" and Claude calls query_db with the appropriate SQL.

Type annotations matter

FastMCP uses type annotations to generate the JSON Schema for each tool's input. If you omit annotations, the generated schema will be weak and the AI may not call the tool correctly. Always annotate parameters:

# Good — fully annotated
@mcp.tool()
def search(query: str, limit: int = 10) -> list[str]:
    """Search indexed content."""
    ...

# Bad — unannotated, schema will be incomplete
@mcp.tool()
def search(query, limit=10):
    ...

Exposing resources (not just tools)

MCP also has Resources — static or dynamic data the client can read, like file contents or configuration. Add a resource with @mcp.resource():

@mcp.resource("config://app")
def get_config() -> str:
    """Return the current application config."""
    return open("/path/to/config.yaml").read()

Resources are read when the client requests them, not when the AI decides to call them. Think of tools as actions and resources as data the client subscribes to.

Running over HTTP instead of stdio

stdio works great for local tools. For a remotely-hosted server, switch to streamable HTTP:

if __name__ == "__main__":
    mcp.run(transport="streamable-http", port=8080)

Then connect clients via the HTTP URL instead of a subprocess command. This is how AgentReady runs — a hosted MCP server that any client can reach without installing anything locally.

What to build next

Once you have a working server, the pattern for any tool is the same: write a Python function, annotate it, decorate with @mcp.tool(). Some useful directions:

— Wrap your internal REST APIs as tools so agents can call them without parsing JSON themselves.

— Index your documentation and expose a search tool (or use AgentReady for this so you don't have to build the RAG pipeline).

— Expose database queries with guardrails — only read operations, parameterized SQL to prevent injection.

The protocol is simple and the SDK handles the plumbing. The valuable part is deciding what tools your agents actually need.