Project 12 — Build an MCP Server
Enforces: the three MCP primitives (tool, resource, prompt), stdio transport, and
.mcp.jsonwiring (from introduction-to-mcp) Surface: code (Python, FastMCP) · Time: ~75 min · Difficulty: 🟡 intermediate
Why this project
MCP is how you give Claude new capabilities that aren't built in. The three primitives — tools (model-invoked actions), resources (app-controlled data), prompts (user-invoked templates) — are the entire vocabulary, and the exam tests whether you know which is which. The fastest way to internalize them is to ship one of each and call it from Claude Code.
What you'll build
A small FastMCP server exposing exactly one of each primitive — one tool, one resource, one prompt — running over stdio, wired into Claude Code via .mcp.json, with the tool successfully called from a session.
Steps
-
Set up —
uv init mcp-notes && cd mcp-notes && uv add "mcp[cli]". (Orpip install mcp.) -
Write the server —
server.py, one of each primitive. The function name + docstring become the tool name + description; type hints become the JSON schema:from mcp.server.fastmcp import FastMCP mcp = FastMCP("notes") NOTES: dict[str, str] = {"welcome": "This is your first MCP note."} @mcp.tool() def add_note(title: str, body: str) -> str: """Save a note by title. Use when the user wants to persist a short note.""" NOTES[title] = body return f"Saved note '{title}'." @mcp.resource("note://{title}") def read_note(title: str) -> str: """Return the body of a saved note. App-controlled context, not an action.""" return NOTES.get(title, f"(no note titled '{title}')") @mcp.prompt() def summarize_notes() -> str: """A user-invoked prompt template to summarize all saved notes.""" joined = "\n\n".join(f"# {t}\n{b}" for t, b in NOTES.items()) return f"Summarize these notes into 3 bullets:\n\n{joined}" if __name__ == "__main__": mcp.run() # stdio transport by defaultRemember: log to stderr, never stdout — stdout is the wire on stdio transport.
-
Inspect it standalone — Before wiring to Claude, sanity-check with the inspector:
npx @modelcontextprotocol/inspector uv run server.py. List tools, calladd_note, readnote://welcome, render the prompt. -
Wire into Claude Code — Create
.mcp.jsonat the repo root so the server loads on session start:{ "mcpServers": { "notes": { "command": "uv", "args": ["run", "server.py"] } } }Launch
claude, then run/mcpto confirm thenotesserver connected and lists three primitives. -
Call the tool — Ask Claude: "Add a note titled 'standup' with body 'shipped MCP server'." Confirm it invokes
add_note(you'll see thetool_use/tool_resultexchange), then ask it to read the note back via the resource. -
Map primitives to roles — Write one sentence each: who invokes the tool (the model), who controls the resource (the app/host), who triggers the prompt (the user).
Acceptance criteria — you're done when
-
server.pydefines exactly one tool, one resource (with a URI template), and one prompt. - The server passes a manual check in
@modelcontextprotocol/inspector. -
.mcp.jsonis committed and/mcpshowsnotesconnected with all three primitives. - Claude successfully calls
add_noteand reads it back via thenote://resource. - You can correctly assign each primitive to its invoker (model / app / user).
- You journaled the difference between a tool and a resource in your own words in learning-journal-template.
Stretch goals
- Add input validation to
add_noteand return a clear error string the model can recover from. - Add a second tool and write descriptions specific enough that Claude never confuses the two.
- Run the same server over an HTTP transport instead of stdio and note what changes in
.mcp.json.
Self-assessment rubric
| Level | Signal |
|---|---|
| 🟢 Got it | You can scaffold an MCP server from memory and explain tool vs resource vs prompt without hesitation. |
| 🟡 Almost | The server runs, but you had to look up which decorator maps to which primitive. |
| 🔴 Revisit | Server wouldn't connect, or you logged to stdout and broke the wire. Re-watch introduction-to-mcp and re-read mcp-server-python. |
See also
- Course: introduction-to-mcp
- Previous project: p11-custom-command-and-hook
- Next project: p13-agent-skill
- Deeper: mcp-server-python, mcp-transports