25 min read
Intermediate
Building Servers

Build an MCP Server with Python

Build a production-ready MCP server in Python using the FastMCP framework with decorator-based tool registration

MCPgee Team

MCP Expert

Python 3.10+ installedBasic Python knowledge including decoratorsUnderstanding of MCP concepts from the introductory tutorialpip or uv package manager

Build an MCP Server with Python

Introduction

Python is one of the most popular languages for AI development, and building MCP servers in Python is straightforward thanks to the FastMCP framework. In this tutorial, you will learn how to create a fully functional MCP server using Python's decorator-based approach, which makes defining tools, resources, and prompts intuitive and clean.

If you have not already read the basics, start with our What is MCP tutorial first. For the TypeScript approach, see our first MCP server tutorial.

Prerequisites

Before starting, make sure you have:

  • Python 3.10+ installed (check with python --version)
  • pip or uv package manager
  • A code editor (VS Code recommended)
  • Basic familiarity with Python decorators

Setting Up Your Project

Step 1: Create the Project Structure

bash
mkdir my-python-mcp-server
cd my-python-mcp-server
python -m venv .venv
source .venv/bin/activate  # On Windows: .venv\Scripts\activate

Step 2: Install Dependencies

bash
pip install mcp

Or if you prefer uv:

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

Building Your First FastMCP Server

The FastMCP Framework

FastMCP is the high-level Python framework for building MCP servers. It uses decorators to register tools, resources, and prompts, making your code clean and declarative.

python
from mcp.server.fastmcp import FastMCP

# Create a FastMCP server instance
mcp = FastMCP("my-python-server")
Important: Always import from mcp.server.fastmcp, not from mcp.server. The FastMCP class is the recommended high-level API.

Defining Tools with Decorators

Tools are functions that AI clients can invoke. Use the @mcp.tool() decorator:

python
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("calculator-server")

@mcp.tool()
def add(a: float, b: float) -> str:
    """Add two numbers together.

    Args:
        a: The first number
        b: The second number
    """
    return str(a + b)

@mcp.tool()
def multiply(a: float, b: float) -> str:
    """Multiply two numbers together.

    Args:
        a: The first number
        b: The second number
    """
    return str(a * b)

FastMCP automatically extracts:

  • The function name as the tool name
  • The docstring as the tool description
  • Type hints and docstring Args as the input schema

Defining Resources

Resources represent data that AI clients can read. Use the @mcp.resource() decorator:

python
@mcp.resource("config://app")
def get_config() -> str:
    """Return the application configuration."""
    return json.dumps({
        "version": "1.0.0",
        "environment": "production",
        "features": ["auth", "logging", "cache"]
    })

@mcp.resource("users://{user_id}")
def get_user(user_id: str) -> str:
    """Retrieve user information by ID."""
    users = {
        "1": {"name": "Alice", "role": "admin"},
        "2": {"name": "Bob", "role": "user"},
    }
    user = users.get(user_id, {"error": "User not found"})
    return json.dumps(user)

Resource URIs can include template parameters (like {user_id}) for dynamic content.

Defining Prompts

Prompts are reusable templates that help AI clients structure their interactions:

python
@mcp.prompt()
def analyze_data(dataset: str) -> str:
    """Create a prompt to analyze a dataset."""
    return f"""Please analyze the following dataset and provide insights:

Dataset: {dataset}

Please include:
1. Summary statistics
2. Key trends
3. Anomalies or outliers
4. Recommendations"""

Building a Real-World Example: File Manager Server

Let us build a practical MCP server that provides file management capabilities:

python
import os
import json
from pathlib import Path
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("file-manager")

WORKSPACE = Path.home() / "mcp-workspace"
WORKSPACE.mkdir(exist_ok=True)

@mcp.tool()
def list_files(directory: str = "") -> str:
    """List files in the workspace directory.

    Args:
        directory: Subdirectory to list (relative to workspace root)
    """
    target = WORKSPACE / directory
    if not target.exists():
        return json.dumps({"error": f"Directory not found: {directory}"})

    files = []
    for item in target.iterdir():
        files.append({
            "name": item.name,
            "type": "directory" if item.is_dir() else "file",
            "size": item.stat().st_size if item.is_file() else None,
        })
    return json.dumps({"files": files, "count": len(files)})

@mcp.tool()
def read_file(filename: str) -> str:
    """Read the contents of a file in the workspace.

    Args:
        filename: Path to file relative to workspace root
    """
    filepath = WORKSPACE / filename
    if not filepath.exists():
        return json.dumps({"error": f"File not found: {filename}"})
    if not str(filepath.resolve()).startswith(str(WORKSPACE)):
        return json.dumps({"error": "Access denied: path traversal detected"})
    return filepath.read_text()

@mcp.tool()
def write_file(filename: str, content: str) -> str:
    """Write content to a file in the workspace.

    Args:
        filename: Path to file relative to workspace root
        content: Content to write to the file
    """
    filepath = WORKSPACE / filename
    if not str(filepath.resolve()).startswith(str(WORKSPACE)):
        return json.dumps({"error": "Access denied: path traversal detected"})
    filepath.parent.mkdir(parents=True, exist_ok=True)
    filepath.write_text(content)
    return json.dumps({"status": "success", "path": str(filepath)})

@mcp.tool()
def search_files(pattern: str, directory: str = "") -> str:
    """Search for files matching a glob pattern.

    Args:
        pattern: Glob pattern to match (e.g., '*.py', '**/*.json')
        directory: Subdirectory to search in
    """
    target = WORKSPACE / directory
    matches = [str(p.relative_to(WORKSPACE)) for p in target.glob(pattern)]
    return json.dumps({"matches": matches, "count": len(matches)})

@mcp.resource("workspace://structure")
def workspace_structure() -> str:
    """Return the workspace directory structure."""
    structure = []
    for root, dirs, files in os.walk(WORKSPACE):
        level = len(Path(root).relative_to(WORKSPACE).parts)
        indent = "  " * level
        structure.append(f"{indent}{Path(root).name}/")
        for f in files:
            structure.append(f"{indent}  {f}")
    return "\n".join(structure)

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

Async Tools and Context

FastMCP supports async functions and provides a context object for advanced features:

python
import asyncio
import httpx
from mcp.server.fastmcp import FastMCP, Context

mcp = FastMCP("async-server")

@mcp.tool()
async def fetch_url(url: str, ctx: Context) -> str:
    """Fetch content from a URL.

    Args:
        url: The URL to fetch
    """
    ctx.info(f"Fetching {url}...")
    async with httpx.AsyncClient() as client:
        response = await client.get(url)
        ctx.info(f"Received {len(response.text)} bytes")
        return response.text[:5000]

@mcp.tool()
async def parallel_fetch(urls: list[str], ctx: Context) -> str:
    """Fetch multiple URLs in parallel.

    Args:
        urls: List of URLs to fetch
    """
    async with httpx.AsyncClient() as client:
        tasks = [client.get(url) for url in urls]
        responses = await asyncio.gather(*tasks, return_exceptions=True)

    results = {}
    for url, resp in zip(urls, responses):
        if isinstance(resp, Exception):
            results[url] = f"Error: {str(resp)}"
        else:
            results[url] = resp.text[:1000]

    return json.dumps(results)

The Context parameter is automatically injected by FastMCP when included as a type hint. It provides:

  • ctx.info(), ctx.warning(), ctx.error() for logging
  • ctx.report_progress() for progress updates
  • Access to request metadata

Error Handling and Validation

Robust error handling is essential for production MCP servers:

python
from mcp.server.fastmcp import FastMCP
import json

mcp = FastMCP("robust-server")

@mcp.tool()
def divide(a: float, b: float) -> str:
    """Divide two numbers safely.

    Args:
        a: The dividend
        b: The divisor (must not be zero)
    """
    if b == 0:
        return json.dumps({"error": "Division by zero is not allowed"})
    result = a / b
    return json.dumps({"result": result})

@mcp.tool()
def process_json(data: str) -> str:
    """Parse and validate JSON data.

    Args:
        data: JSON string to process
    """
    try:
        parsed = json.loads(data)
        return json.dumps({
            "valid": True,
            "type": type(parsed).__name__,
            "keys": list(parsed.keys()) if isinstance(parsed, dict) else None,
            "length": len(parsed) if isinstance(parsed, (list, dict)) else None,
        })
    except json.JSONDecodeError as e:
        return json.dumps({"valid": False, "error": str(e)})

Running Your Server

stdio Transport (Default)

The simplest way to run your server for local use with clients like Claude Desktop:

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

This starts the server using stdio transport, which is ideal for local integrations.

Configuring Claude Desktop

Add your server to Claude Desktop's configuration file:

json
{
  "mcpServers": {
    "file-manager": {
      "command": "python",
      "args": ["/path/to/your/server.py"]
    }
  }
}

For more on connecting to different clients, see our Claude integration tutorial and our clients directory.

Streamable HTTP Transport

For remote deployments, use Streamable HTTP transport instead of the deprecated SSE:

python
if __name__ == "__main__":
    mcp.run(transport="streamable-http", host="0.0.0.0", port=8000)

Learn more about remote deployment and transports in our advanced MCP tutorial.

Testing Your Server

Manual Testing with MCP Inspector

The MCP Inspector is a helpful tool for testing servers:

bash
npx @modelcontextprotocol/inspector python server.py

This opens a web UI where you can:

  • Browse available tools and resources
  • Execute tools with sample inputs
  • View responses and debug issues

Unit Testing

Write unit tests for your tool functions:

python
import pytest
from server import list_files, read_file, write_file

def test_write_and_read():
    result = write_file("test.txt", "Hello MCP")
    data = json.loads(result)
    assert data["status"] == "success"

    content = read_file("test.txt")
    assert content == "Hello MCP"

def test_list_files():
    result = list_files("")
    data = json.loads(result)
    assert "files" in data
    assert isinstance(data["count"], int)

Project Structure for Larger Servers

For more complex servers, organize your code into modules:

plaintext
my-mcp-server/
  pyproject.toml
  src/
    server/
      __init__.py
      main.py          # FastMCP instance and entry point
      tools/
        __init__.py
        file_tools.py  # File management tools
        data_tools.py  # Data processing tools
      resources/
        __init__.py
        config.py      # Configuration resources
      utils/
        __init__.py
        validation.py  # Input validation helpers
python
# src/server/main.py
from mcp.server.fastmcp import FastMCP
from .tools.file_tools import register_file_tools
from .tools.data_tools import register_data_tools

mcp = FastMCP("organized-server")

register_file_tools(mcp)
register_data_tools(mcp)

if __name__ == "__main__":
    mcp.run()
python
# src/server/tools/file_tools.py
def register_file_tools(mcp):
    @mcp.tool()
    def list_files(directory: str = "") -> str:
        """List files in a directory."""
        # implementation...
        pass

Deployment Options

Once your server is working locally, you can deploy it in several ways:

For security best practices when deploying, read our security fundamentals tutorial.

Conclusion

You have built a fully functional MCP server in Python using FastMCP. The decorator-based approach makes it easy to define tools, resources, and prompts with minimal boilerplate. FastMCP handles protocol serialization, transport management, and schema generation automatically.

For more detailed guidance, check out our blog post on building Python MCP servers and explore the advanced MCP patterns tutorial for streaming, authentication, and more.

Code Examples

Basic FastMCP Server with Toolspython
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("my-server")

@mcp.tool()
def greet(name: str) -> str:
    """Greet someone by name.

    Args:
        name: The person's name
    """
    return f"Hello, {name}! Welcome to MCP."

@mcp.tool()
def calculate(expression: str) -> str:
    """Evaluate a mathematical expression safely.

    Args:
        expression: Math expression to evaluate
    """
    allowed = set("0123456789+-*/.(). ")
    if not all(c in allowed for c in expression):
        return "Error: Invalid characters in expression"
    try:
        result = eval(expression)
        return str(result)
    except Exception as e:
        return f"Error: {str(e)}"

if __name__ == "__main__":
    mcp.run()
FastMCP Resources and Promptspython
import json
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("resource-server")

@mcp.resource("status://health")
def health_check() -> str:
    """Return server health status."""
    return json.dumps({"status": "healthy", "uptime": "99.9%"})

@mcp.resource("data://users/{user_id}")
def get_user(user_id: str) -> str:
    """Retrieve user data by ID."""
    return json.dumps({"id": user_id, "name": "Example User"})

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

if __name__ == "__main__":
    mcp.run()
Claude Desktop Configurationjson
{
  "mcpServers": {
    "python-file-manager": {
      "command": "python",
      "args": ["/path/to/file_manager_server.py"],
      "env": {
        "WORKSPACE_DIR": "/home/user/documents"
      }
    }
  }
}

Key Takeaways

  • Use FastMCP from mcp.server.fastmcp for the recommended Python MCP development experience
  • The @mcp.tool() decorator automatically generates schemas from type hints and docstrings
  • Resources use URI templates for dynamic content with @mcp.resource()
  • The Context parameter provides logging, progress reporting, and request metadata
  • Always validate inputs and handle errors gracefully in tool implementations

Troubleshooting

ImportError: cannot import FastMCP from mcp.server

Make sure you import from mcp.server.fastmcp, not mcp.server. The correct import is: from mcp.server.fastmcp import FastMCP. Also ensure you have the latest mcp package installed with pip install --upgrade mcp.

Tools not appearing in Claude Desktop

Verify your Claude Desktop configuration file path is correct and the Python path points to the right interpreter. Restart Claude Desktop after making configuration changes. Check the logs for connection errors.

Server crashes with async tools

Ensure you are using Python 3.10+ which has better async support. Make sure all async dependencies (like httpx) are installed. Use try/except blocks in async tools to handle network errors gracefully.

Next Steps

  • Explore advanced patterns in the advanced MCP tutorial
  • Deploy your server with Docker for production use
  • Add authentication and security to your server
  • Connect your server to Claude Desktop or VS Code

Was this helpful?

Share tutorial:

Stay Updated with MCP Insights

Join 5,000+ developers and get weekly insights on MCP development, new server releases, and implementation strategies delivered to your inbox.

We respect your privacy. Unsubscribe at any time.

MCPgee Team

We write in-depth guides, tutorials, and reviews to help developers get the most out of the Model Context Protocol ecosystem.

Frequently Asked Questions

Explore MCP Servers

Browse our directory of 33,000+ MCP servers. Find the perfect tools for your AI-powered workflows.