Your first server with MCPServer

Model Context Protocol (MCP) · Essentials · 12 min

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 default

Remember

  • 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.

Check yourself

  1. Where does the model get the description of a tool defined with @mcp.tool()?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: py.sdk.modelcontextprotocol.io/get-started/first-steps/