Build an MCP Server in Python 2026: Tools, Resources, and Prompts from Scratch

Introduction

MCP (Model Context Protocol) is Anthropic's open protocol for connecting AI models to external tools and data sources. It defines a standard way for a host application — such as Claude Desktop or Claude Code — to discover and invoke capabilities provided by a separate server process. Think of it as a plugin system for AI agents: your server declares what it can do, and the AI calls into it when needed.

Before MCP, integrating an AI assistant with a database, a filesystem, or a third-party API required bespoke glue code for every combination of model and tool. MCP standardises that boundary so any compliant client can talk to any compliant server without custom wiring.

Practical use cases include:

  • Filesystem access — let the AI read and write local files
  • Database queries — run SQL and return results to the model
  • API calls — wrap REST or GraphQL endpoints as callable tools
  • Web search — give the model real-time access to search results

This tutorial builds a working MCP server from scratch using FastMCP, the recommended Python library. You will implement tools, resources, and prompts, then connect the server to Claude Desktop and Claude Code.

Prerequisites

  • Python 3.10 or newer
  • uv or pip for package management
  • Claude Desktop or Claude Code installed (to test the finished server)
  • Basic familiarity with Python functions and type hints

Install FastMCP

FastMCP is a high-level Python framework that handles the MCP wire protocol and lets you focus on your server's logic.

pip install fastmcp
# or with uv
uv add fastmcp

Verify the installation:

python -c "import fastmcp; print(fastmcp.__version__)"

Your First MCP Server

The minimal server requires three things: a FastMCP instance, at least one decorated function, and a call to mcp.run().

from fastmcp import FastMCP

mcp = FastMCP("My First Server")

@mcp.tool()
def greet(name: str) -> str:
    """Greet a user by name."""
    return f"Hello, {name}!"

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

Save this as server.py and run it:

python server.py

The server starts and listens on standard input/output by default, which is the transport Claude Desktop uses. The greet tool is now discoverable and callable by any connected MCP client.

Adding Tools

Tools are the core feature of MCP. A tool is any Python function decorated with @mcp.tool(). The function's docstring becomes the tool description that the AI reads when deciding whether to call it. Type hints define the input schema, and FastMCP validates incoming arguments automatically.

The following example adds two file-system tools:

from fastmcp import FastMCP
from pathlib import Path

mcp = FastMCP("File Reader")

@mcp.tool()
def read_file(path: str) -> str:
    """Read a file and return its contents."""
    try:
        return Path(path).read_text()
    except FileNotFoundError:
        return f"Error: file not found at {path}"

@mcp.tool()
def list_directory(path: str = ".") -> list[str]:
    """List files in a directory."""
    return [str(p) for p in Path(path).iterdir()]

A few notes on writing good tools:

  • Keep tool names short and action-oriented (read_file, not perform_file_reading_operation).
  • Write docstrings as if explaining the tool to a colleague who cannot see the code. The AI uses this text verbatim.
  • Return plain strings or JSON-serialisable values. FastMCP serialises return values automatically.
  • Handle errors gracefully and return a descriptive message rather than raising an exception, so the model can relay the problem back to the user.

Adding Resources

Resources are URIs that the AI can read, similar to files in a filesystem or endpoints in a REST API. They are read-only by convention and are suited for exposing configuration, reference data, or database records.

@mcp.resource("config://app")
def get_config() -> str:
    """Return application configuration."""
    return '{"debug": false, "version": "1.0.0"}'

@mcp.resource("data://users/{user_id}")
def get_user(user_id: str) -> str:
    """Return user data by ID."""
    # In real use, query a database here
    return f'{{"id": "{user_id}", "name": "Example User"}}'

The URI template data://users/{user_id} captures a path segment and passes it to the function as a keyword argument. When a client requests data://users/42, FastMCP calls get_user(user_id="42") and returns the result.

Resources complement tools: tools perform actions, while resources expose data. A well-designed server uses resources for stable, readable data and tools for operations that change state or trigger side effects.

Adding Prompts

Prompts are reusable message templates. They let you package a complex instruction into a callable that the AI — or the user via the client UI — can invoke by name. This is useful for standardising recurring tasks such as code review, document summarisation, or incident analysis.

@mcp.prompt()
def code_review(code: str, language: str = "python") -> str:
    """Generate a code review prompt."""
    return (
        f"Review this {language} code for bugs and improvements:\n\n"
        f"```{language}\n{code}\n```"
    )

Calling this prompt with a block of Python code returns a fully formed review request that the model can act on immediately.

HTTP Transport for Remote Use

The default stdio transport works well for local desktop clients, but if you want multiple clients to connect simultaneously, or if you are running the server on a remote host, switch to the streamable HTTP transport:

mcp = FastMCP("My Server", transport="streamable-http", port=8015)

The server now listens on http://localhost:8015/mcp. You can run it as a long-lived process or behind a process manager such as systemd or supervisord.

Connect to Claude Desktop

Claude Desktop reads server definitions from a JSON configuration file. On Linux and macOS the path is ~/.config/claude/claude_desktop_config.json. On Windows it is %APPDATA%\Claude\claude_desktop_config.json.

Add an entry for your server:

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

Replace /path/to/server.py with the absolute path to your file. Restart Claude Desktop after saving the configuration. The server's tools, resources, and prompts will appear in the context menu and will be available to Claude automatically.

If your server uses a virtual environment, point command at the environment's Python interpreter:

{
  "mcpServers": {
    "my-server": {
      "command": "/home/user/.venvs/myenv/bin/python",
      "args": ["/path/to/server.py"]
    }
  }
}

Connect to Claude Code

Claude Code can connect to an MCP server running over HTTP. Create a JSON file describing the server under ~/.claude/mcpjson/:

echo '{"type":"http","url":"http://localhost:8015/mcp"}' > ~/.claude/mcpjson/myserver.json

Start your server with the HTTP transport enabled, then reload Claude Code. The server's tools will be available in your coding sessions.

Full Working Example: Web Fetch and Summarise

The following complete server combines tools and prompts to fetch a web page and prepare it for summarisation. Real web search requires an API key, so the search_web tool is shown as a stub you can replace with your preferred search provider.

from fastmcp import FastMCP
from pathlib import Path

try:
    import httpx
except ImportError:
    httpx = None  # install with: pip install httpx

mcp = FastMCP("Web Tools")


@mcp.tool()
def search_web(query: str) -> str:
    """Search the web for a query and return result snippets.

    Replace the body of this function with a call to your preferred
    search API (Brave Search, SerpAPI, Tavily, etc.).
    """
    # Stub: return a placeholder until a real API key is configured
    return (
        f"[search_web stub] Results for '{query}': "
        "configure a search API key to enable real results."
    )


@mcp.tool()
def fetch_url(url: str) -> str:
    """Fetch the text content of a URL.

    Returns the raw response body. For HTML pages, consider passing
    the result through a parser such as BeautifulSoup to strip tags.
    """
    if httpx is None:
        return "Error: httpx is not installed. Run: pip install httpx"
    try:
        response = httpx.get(url, follow_redirects=True, timeout=10)
        response.raise_for_status()
        return response.text[:8000]  # cap to avoid oversized responses
    except httpx.HTTPError as exc:
        return f"Error fetching {url}: {exc}"


@mcp.prompt()
def summarize_page(url: str) -> str:
    """Build a prompt that instructs the model to fetch and summarise a page."""
    return (
        f"Please fetch the content of {url} using the fetch_url tool, "
        "then write a concise summary (3-5 sentences) covering the main points."
    )


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

With this server connected, you can ask Claude: "Summarise the page at https://example.com" and it will call fetch_url, receive the HTML, and produce a summary — all driven by the summarize_page prompt template.

Testing Your Server

Run the server directly to verify it starts without errors:

python server.py

For interactive testing without a full client, use the MCP inspector. It is a browser-based tool that lets you browse tools and call them manually:

npx @modelcontextprotocol/inspector python server.py

The inspector opens in your browser and shows all registered tools, resources, and prompts. You can supply arguments and see the raw JSON responses, which is useful for debugging before connecting to Claude.

Next Steps

Once your server is working end-to-end, consider the following improvements:

Add authentication. For servers exposed over HTTP, require an API key or OAuth token. FastMCP supports middleware, so you can intercept every request and validate credentials before any tool runs.

Deploy to a remote server. A server running on a VPS or cloud instance can be shared across teams. Use a reverse proxy such as nginx in front of the FastMCP HTTP transport, add TLS, and point your ~/.claude/mcpjson/ file at the public URL.

Build a database MCP server. Wrap a PostgreSQL or SQLite connection in a set of tools — run_query, list_tables, describe_table — so Claude can inspect and query your schema in natural language. Use parameterised queries and a read-only database role to keep the integration safe.

Write tests. FastMCP servers are plain Python, so you can test tool functions directly with pytest. For integration tests, instantiate the server in-process and call tools via the FastMCP test client.

The MCP ecosystem is growing quickly. The official registry at https://registry.modelcontextprotocol.io lists community servers for databases, cloud providers, and developer tools that you can use as reference implementations or connect to directly.

Leonardo Lazzaro

Software engineer and technical writer. 10+ years experience in DevOps, Python, and Linux systems.

More articles by Leonardo Lazzaro