What is the smallest useful MCP server you can understand completely? A read-only catalog lookup lets us examine the entire path without granting a model file-system or database write access.
Before you begin: Read MCP connections. Know Python dictionaries, functions, and virtual environments.
Design the result before the decorator
The tool accepts a workshop ID. For a known ID, it returns a public fixture and a source version. For an unknown ID, it returns found: false. For an oversized ID, it rejects the argument. Missing data and invalid input are different conditions, so the contract should preserve them.
We deliberately do not accept an arbitrary file path. A tool that opens whatever path a model supplies can expose unrelated files; adding a friendly description does not constrain it. Exact record IDs give this example a smaller, inspectable boundary.
Match the current SDK
Reviewed September 10, 2026: the official MCP Python SDK identifies v2 as the stable line. It uses MCPServer from mcp.server. Older v1 FastMCP examples need a version-specific migration; do not combine a v1 tutorial with an unbounded latest installation.
Use Python 3.10 or later in a fresh virtual environment and install "mcp[cli]>=2,<3". Record the resolved version in your project lockfile. This is a documented integration example; interaction with an external MCP host is not claimed as tested here.
Save this complete module as catalog_server.py:
from mcp.server import MCPServer
mcp = MCPServer("Fictional workshop catalog")
CATALOG = {"W1": {"capacity": 12, "start_time": None}}
@mcp.tool()
def read_workshop(workshop_id: str) -> dict:
"""Read a public fixture by exact ID; does not read booking data."""
if not workshop_id or len(workshop_id) > 40:
raise ValueError("Use a workshop ID of 1 to 40 characters")
record = CATALOG.get(workshop_id)
if record is None:
return {"found": False, "workshop_id": workshop_id}
return {
"found": True,
"workshop_id": workshop_id,
"record": dict(record),
"source_version": "fixture-1",
}
Run mcp dev catalog_server.py to inspect the tool with the SDK's development tooling. Defining the module alone does not start a server. The CLI loads it and manages the development connection.
Call the tool with W1, W99, an empty string, and a 41-character string. W1 should preserve the missing start time as null. W99 should be a normal not-found result. The last two inputs should fail validation. These are expected contract outcomes for you to verify, not recorded host test results.
Observe what the schema cannot know
Type hints help the SDK describe and validate the tool interface. Our handler adds a length rule because “string” alone is too broad. A real private catalog also needs authenticated identity, record access checks, timeouts, and limits on returned data. Those concerns are absent from this public fixture, not solved by the decorator.
With stdio, the connection reserves streams for protocol messages. The current v2 SDK protects that channel by redirecting ordinary stdout to stderr while serving. Still use deliberate logging and avoid writing custom bytes into protocol streams. Do not assume another SDK or an older release has the same stream protection.
Change one field and predict the effect
Set W1's capacity to zero. A developer uses if not record["capacity"] to decide that the record is missing. What breaks?
Inspect the boundary case
Zero is a present value, even if it means the workshop cannot accommodate anyone. Use record existence to determine found, and preserve zero in the record. Null, zero, missing record, and backend failure carry different information. Collapsing them makes an assistant's answer less reliable before generation begins.
Extend the fixture with one more workshop and write down its expected results. Only after this contract is clear should you replace the dictionary with a real data source. The next lesson connects multiple such operations into a workflow with explicit state.
Sources
Official Python SDK supplies the v2 API and CLI. SDK migration guide covers version changes; use the guide linked from the repository if its structure changes.