# Build a Custom MCP Server in Python (MCPServer, formerly FastMCP)

> Expose your own functions as MCP tools and resources using the official Python SDK in about 20 lines.

Category: MCP & Tool Use  
Tags: mcp, python, server, sdk  
Setup time (estimate): 20 minutes  
Estimated cost: Free (open-source SDK).  
Last verified: 2026-10-14

## Install

```bash
uv init my-mcp && cd my-mcp
uv add "mcp[cli]>=2"
```

## server.py

```python
from mcp.server.mcpserver import MCPServer  # mcp>=2; on mcp<2: from mcp.server.fastmcp import FastMCP

mcp = MCPServer("notes")
NOTES: dict[str, str] = {}

@mcp.tool()
def add_note(title: str, body: str) -> str:
    """Save a note under a title."""
    NOTES[title] = body
    return f"Saved '{title}'"

@mcp.tool()
def search_notes(query: str) -> list[str]:
    """Return titles of notes containing the query."""
    return [t for t, b in NOTES.items() if query.lower() in (t + b).lower()]

@mcp.resource("notes://{title}")
def read_note(title: str) -> str:
    return NOTES.get(title, "Not found")

if __name__ == "__main__":
    mcp.run()  # stdio transport by default
```

## Test and connect

```bash
uv run mcp dev server.py      # opens the MCP Inspector
```

```json
{"mcpServers": {"notes": {"command": "uv", "args": ["--directory", "/abs/path/my-mcp", "run", "server.py"]}}}
```

Docstrings and type hints become the tool description and input schema, so write them carefully.

## Failure modes

- Code written for mcp 1.x (FastMCP import) fails on mcp 2.x; pin your major version.
- Printing to stdout corrupts the stdio protocol stream.
- Weak docstrings and type hints produce vague tool schemas the model misuses.
