Build a todo manager (Python)

Same tutorial as the Node.js version, built on the official Python MCP SDK's FastMCP instead. Every code sample here is real — tested with the actual MCP Inspector CLI, using two separate tokens, before publishing.

1. Create a project

Sign in at getmcpauth.dev/dashboard and create a project. Copy the registration secret it shows you — it's shown once.

2. Install dependencies

pip install "mcp[cli]" getmcpauth

Requires Python 3.10+ (the same floor the official mcp package itself requires).

3. Build the server

Each tool calls get_access_token() — a context-local lookup the official SDK populates automatically once McpAuthTokenVerifier verifies the request — to scope the in-memory store to the current user:

import os
from mcp.server.fastmcp import FastMCP
from mcp.server.auth.middleware.auth_context import get_access_token
from getmcpauth import McpAuthTokenVerifier, build_auth_settings

# Keyed by the authenticated user's subject — this is the whole point:
# each user only ever sees their own todos.
todos_by_subject: dict[str, list[dict]] = {}

def get_todos(subject: str) -> list[dict]:
    return todos_by_subject.setdefault(subject, [])

mcp = FastMCP(
    "todo-manager",
    token_verifier=McpAuthTokenVerifier(
        "https://getmcpauth.dev/api/oauth/introspect",
        registration_secret=os.environ["MCPAUTH_SECRET"],
    ),
    auth=build_auth_settings(
        "https://getmcpauth.dev",
        resource_server_url="https://my-server.example.com",
    ),
)

@mcp.tool()
def add_todo(text: str) -> str:
    """Add a new todo item."""
    subject = get_access_token().subject
    todos = get_todos(subject)
    todo = {"id": len(todos) + 1, "text": text, "done": False}
    todos.append(todo)
    return f"Added #{todo['id']}: {text}"

@mcp.tool()
def list_todos() -> str:
    """List this user's todos."""
    subject = get_access_token().subject
    todos = get_todos(subject)
    if not todos:
        return "No todos yet."
    return "\n".join(f"#{t['id']} [{'x' if t['done'] else ' '}] {t['text']}" for t in todos)

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

4. Test it live

Run python server.py, then use MCP Inspector to add a todo:

npx @modelcontextprotocol/inspector --cli http://localhost:8000/mcp \
  --transport http --header "Authorization: Bearer <your token>" \
  --method tools/call --tool-name add_todo --tool-arg text="Buy milk"

List it back — you'll only ever see todos added under the same token's identity. Try it with a second token minted for a different subject and the list comes back empty — real per-user isolation, enforced by the token:

npx @modelcontextprotocol/inspector --cli http://localhost:8000/mcp \
  --transport http --header "Authorization: Bearer <your token>" \
  --method tools/call --tool-name list_todos

Closing notes

If your product already has its own login system, see server-to-server token minting — your backend can mint a token for an already-authenticated user directly, skipping mcpauth's own consent screen entirely.