The decorator API handles all protocol details, so a server is just ordinary Python functions.
The official Python SDK is the mcp package. Version 2 (current) is installed with pip install "mcp[cli]" or uv add "mcp[cli]"; the cli extra adds the mcp command-line tool.
A server is an MCPServer object plus decorated functions. A decorator is the @something line above a function; here it registers the function with the server.
@mcp.tool()turns a function into a tool. Its name becomes the tool name and its docstring becomes the description the model reads.@mcp.resource("notes://index")exposes a function's return value as a resource at that URI.@mcp.prompt()turns a function returning text into a prompt template.
mcp.run() starts the server. With no arguments it uses stdio, so it just waits silently for a host to talk to it. Keep it under if __name__ == "__main__": because tools like mcp dev and your tests import the file, and the guard stops an import from starting a server.
If you used version 1 of the SDK, the class was called FastMCP and lived in mcp.server.fastmcp. In v2 it is MCPServer from mcp.server; the decorators work the same. (FastMCP also continues as a separate, larger third-party framework with its own package, fastmcp.)
Notice what you did not write: no JSON, no message parsing, no schema. The SDK builds all of it from the function, as the next lesson shows.
Example
# notes_server.py (pip install "mcp[cli]")
from mcp.server import MCPServer
mcp = MCPServer("Notes")
NOTES = {"groceries": "milk, eggs", "ideas": "MCP talk"}
@mcp.tool()
def list_notes() -> list[str]:
"""List the titles of all notes."""
return sorted(NOTES)
@mcp.resource("notes://{title}")
def read_note(title: str) -> str:
"""The text of one note."""
return NOTES[title]
@mcp.prompt()
def summarize(title: str) -> str:
"""Summarize one note."""
return f"Summarize this note in one sentence:\n\n{NOTES[title]}"
if __name__ == "__main__":
mcp.run() # stdio by defaultRemember
- from mcp.server import MCPServer (v1 called it FastMCP)
- @mcp.tool(), @mcp.resource(uri), @mcp.prompt() register primitives
- Function name, docstring and type hints become the protocol metadata
- mcp.run() defaults to stdio; guard it with if __name__ == '__main__'
Watch out for
- Following an old tutorial with
from mcp.server.fastmcp import FastMCP: that import path no longer exists in SDK v2.