Heidelberg AICurriculum
Track 13 · Advanced
13.4

Build your own MCP server

Stop consuming tools and start shipping one

7 lessons 2026-08-10 AI-generated

1Overview

A small server that publishes tools, resources and prompts over the Model Context Protocol, so any MCP-speaking assistant can use your systems without a bespoke integration.

MCP shows up in a dozen chapters of this course, always as something you install. This one is the other side: writing a server so an assistant can reach your systems — your database, your ticket tracker, your internal API — with auth, an allowlist, and a shape a model can actually use. → Built on the first-party Python SDK, whose v2 is stable against the 2026-07-28 spec, with FastMCP as the ergonomic layer and an honest note on how much of it the official SDK has absorbed. → The hard part is not the protocol. It is designing a tool surface small enough that the model picks the right one.

Writing the server, not installing one. A tool your assistant can call, wrapped around a system only you have — with authentication, an explicit allowlist, and error messages a model can act on. The protocol takes an afternoon; the tool design is what decides whether the model uses it correctly.

1.2After this chapter you can
Explain what MCP standardises and what it deliberately leaves to you
Expose a working tool from your own system and call it from an assistant
Design a tool surface a model can choose from without guessing
Authenticate a server and restrict it to an explicit allowlist
Return errors a model can recover from instead of ones that end the turn
Decide when a tool should be an MCP server and when a plain script is enough
1.3When to reach for it

When more than one assistant needs the same capability, or when the capability touches something that needs auth and an audit trail.

1.4Key parts

A transport, a set of tool definitions with typed inputs, an auth layer, an allowlist of what the tools may reach, and structured errors.

1.5Free vs paid

The SDKs and the protocol are free and open. The cost is maintenance: a tool surface is an API, and it gets used by something that will not read your changelog.

1.6Watch out

Twenty tools with overlapping names is worse than five with distinct ones. The model picks by description, so a vague description is a bug.

2Lessons 7

2.1 Identify MCP protocol guarantees and design responsibilities

MCP — the Model Context Protocol — defines a JSON‑RPC wire format plus three primitives (tools, resources, prompts) that any assistant and server can agree on.

Identify the protocol guarantees and the design decisions you must make.

  1. READ the description of the three primitives in the official specification
  2. OPEN the spec page at modelcontextprotocol.io/specification/latest in a browser
  3. REVIEW the fixed message shapes and discovery endpoints under tools/list and resources/list
  • You'll see A clear mental model of what the wire format mandates versus what you must implement yourself
  • Takeaway MCP standardises the wire format and three primitives while leaving tool design, authentication and error handling to you
  • Check Your assistant should decide mid‑answer whether to look up a customer record. Which primitive handles that – tool, resource, or prompt?
  • Cost Free. Both SDKs are open source; nothing in this lesson needs an account.

2.2 Run an MCP tool from the Inspector

mcp dev is the test harness that launches MCP Inspector, a local UI that speaks MCP to your server so you can call tools without an assistant.

Start a local MCP server, invoke its tool through the Inspector and make it reachable to Claude Code.

Tryclaude mcp add my-demo -- uv run python server.py

Run it in the folder that holds server.py. Everything after -- is the command Claude Code runs to start your server as a subprocess over stdio; no --transport flag is needed for a local server.

The MCP Inspector Tools tab for the Demo server. The tool list on the left holds one entry, add. The Results panel shows 3, and the protocol log on the right records a TOOLS/CALL to add that returned OK.
  1. 1 add Your Python function, discovered over the protocol. The name is the `def`, the description is the docstring.
  2. 2 Results: 3 Computed by your code. If the model had done the arithmetic there would be no TOOLS/CALL in the log.
  3. 3 TOOLS/CALL add Proof the request crossed the protocol boundary and came back OK.

Best viewed on desktop — tap Enlarge to read the numbered controls.

Your first tool, answering. uv run mcp dev server.py opens this at localhost:6274. The 3 in Results came from your Python function — not from the model doing arithmetic. Credit: MCP Inspector ↗
  1. OPEN a terminal, create a project folder and install the SDK with uv add "mcp[cli]"
  2. CREATE server.py in that folder and paste the provided example code
  3. RUN uv run mcp dev server.py, then toggle the Server card to Connected
  4. IN the Inspector, open the Tools tab, click the add entry, type 1 and 2, and press Execute Tool
  5. REGISTER the tool for Claude Code with claude mcp add my-demo -- uv run python server.py
  6. LIST the registered tools with claude mcp list to confirm a ✔ Connected status
  • You'll see The Tools tab shows the add tool, the Results panel displays 3, and the protocol log records a successful TOOLS/CALL entry
  • Takeaway A type‑hinted function decorated with @mcp.tool() becomes a full tool any local assistant can invoke via claude mcp …
  • Check You never wrote a JSON Schema. Where did the Inspector obtain the types for the parameters a and b?
  • Cost Free. Everything here runs on your own machine — no account, no key.

2.3 Add resources and prompts to your MCP server

A resource is addressable data a client can read or attach to context, while a prompt is a canned template a human selects; both are added with decorators on the server object.

Create reusable data endpoints and human‑chosen templates alongside your tools.

Tryuv run mcp dev server.py

Click through Tools, Resources and Prompts. In Claude Code the same three surface as a callable tool, an @-mentionable reference, and a / slash command.

  1. ADD a @mcp.resource() decorator to server.py exposing a greeting function
  2. ADD a @mcp.prompt() decorator below it defining a code‑review template
  3. RESTART the development server with uv run mcp dev server.py
  4. OPEN the Resources tab to see the new greeting endpoint listed
  5. OPEN the Prompts tab to see the new code‑review template displayed
  • You'll see Three populated tabs – Tools, Resources, Prompts – showing the newly added decorators
  • Takeaway Choose a resource when data is attached, a prompt when a human selects it, and a tool when the model decides
  • Check “List my open tickets” needs no judgement from the model. Should it be implemented as a tool or a resource?
  • Cost Free — same server, same process, two more decorators.

2.4 Create a concise ticket‑status tool

A tool surface is the list of names and one‑line descriptions the model sees when selecting a tool; it is the only view the model has of your server’s capabilities.

Reduce a noisy API surface to a single, well‑described function that the model can pick correctly.

Tryuv run mcp dev tickets.py

Open the Tools tab and read only the names. That is the model's whole view at selection time — your source code is not in it.

The MCP Inspector Tools tab listing four tools — search, find, lookup and jira_api_get — named after the API they wrap rather than what a caller wants. Nothing in the list distinguishes them.
  1. 1 search Named for the verb, not the answer. Nothing here says what it searches or what comes back.
  2. 2 jira_api_get Named after the endpoint it wraps. The request is about a ticket; the name is about your HTTP client.
  3. 3 TOOLS/LIST The single call the client makes before choosing. What it returns is the whole surface the model reasons over.

Best viewed on desktop — tap Enlarge to read the numbered controls.

This list is the model's entire view of your server. Four names, no way to tell which one answers "what is the status of PROJ-142". A vague tool surface is a bug, not a style choice. Credit: MCP Inspector ↗
  1. CREATE a file called tickets.py containing four example @mcp.tool() functions
  2. RUN uv run mcp dev tickets.py, open the Tools tab in the Inspector and note the list with the source file closed
  3. IDENTIFY which listed tool should answer “what is the status of PROJ‑142”
  4. REPLACE the four functions with a single get_ticket_status(ticket_id: str) tool and add a docstring that also states when not to use it
  5. RELOAD the Inspector and verify the Tools list now shows only the appropriate entry
  • You'll see Initially four indistinguishable tools, then one clearly named entry matching the query
  • Takeaway Clear naming and explicit docstrings let the model choose the right tool without ambiguity
  • Check Two of your tools could both plausibly answer the same request. Should you merge them or add a caveat to each docstring?
  • Cost Free — a design pass, not new infrastructure. It costs you the discipline to delete tools, which is the part people skip.

2.5 Require tokens and enforce an allowlist

TokenVerifier converts a bearer token into an AccessToken; AuthSettings declares the issuer, server URL and required scopes. An allowlist is a custom pattern you code into tool bodies.

Require a valid token before any tool runs and keep the set of reachable systems in an explicit, reviewable list.

Trycurl -i http://127.0.0.1:8000/mcp

Run it against the auth-protected server with no token. The 401 is the lesson: rejection happens at the boundary, not inside your code.

  1. CREATE a verifier class implementing TokenVerifier.verify_token that returns an AccessToken or None
  2. INSTANTIATE the server with both token_verifier and auth arguments; omitting either should raise a startup error
  3. CALL get_access_token() inside a tool to obtain caller token details for per‑tool scope checks
  4. START the MCP server over HTTP and issue a request without a token (e.g. curl -i http://127.0.0.1:8000/mcp) to see the 401 response
  5. WRITE an allowlist as plain text enumerating exactly which tables, paths or repositories each tool may access
  • You'll see HTTP/1.1 401 Unauthorized with a WWW-Authenticate: Bearer error="invalid_token" header and a JSON body indicating the request was refused before any tool executed
  • Takeaway token_verifier + AuthSettings gate who can call at all; an allowlist provides a separate, reviewable list of what each tool may touch
  • Check Your server has exactly one trusted caller and you trust them completely. Do you still need an allowlist?
  • Cost Free at the SDK level. The token issuer — your own or a third-party OAuth provider — is the piece that may carry infrastructure cost, separate from MCP itself.

2.6 Select appropriate error type for each failure

Plain Python exceptions are returned as tool content the model can read; MCPError aborts the JSON‑RPC request itself, ending the exchange.

Decide whether to raise a plain exception or an MCPError for each individual error condition.

Tryget_author(title="Nonexistent Book")

Call it once against each version of the tool in the Inspector and read what comes back in the Results panel.

  1. FOR each raise statement, ASK whether a smarter model could have avoided the error and note the answer in a comment
  2. RAISE a plain exception for errors another call could fix, such as an invalid title or malformed date
  3. RAISE MCPError for protocol‑level failures the model cannot influence, like authentication rejection or structurally invalid parameters
  4. INVOKE both functions from the Inspector with identical bad input and observe the differing outcomes
  5. INCLUDE the problematic value in the error message and, when possible, suggest a corrective action
  • You'll see The same bad title is processed twice – once the model reads the exception and retries, once the request ends with a hard failure
  • Takeaway Plain exceptions let the model recover while MCPError terminates the turn
  • Check The caller's token expired halfway through a session. Should you raise a plain exception or an MCPError?
  • Cost Free — this is a decision made per raise statement, not new infrastructure.

2.7 Choose a script over a server when appropriate

A script is a one‑off piece of code the assistant runs directly in its own environment, requiring no protocol, auth or persistent tool surface.

Decide whether to implement a one‑off task as a script instead of building a server.

  1. ASSESS the three conditions: multiple callers, authentication/audit requirements, repeated cross‑session use
  2. DETERMINE that only a single person will run the task in one session and no auth boundary is needed
  3. WRITE a plain script that performs the required work
  4. EVALUATE maintenance implications before committing to an API or server
  • You'll see A decision matrix showing when a script suffices and when a server is required
  • Takeaway Match infrastructure level to number of callers, auth needs and maintenance impact
  • Check Three teams need the same capability against a system that logs every access. Should you use a script or a server?
  • Cost The script costs nothing to build or retire. The server costs ongoing maintenance the moment a second caller depends on it — which is exactly the condition that justifies building it.

3You’ll know it worked 13 checkable outcomes in this chapter

  • Calling the registered "get_pokemon" tool from the client returns JSON data for the requested Pokémon
  • In the MCP UI, the tool appears in the list and can be executed manually, returning expected results
  • Invalid payloads cause a 400 response with clear error messages; valid payloads pass through to the handler
  • Attempting to run `INSERT`, `UPDATE`, or a function with side effects inside the wrapper throws an error and no data is changed
  • The IDE shows a green status for the server and lists its tools/resources in the UI
  • The browser lists your defined tools/resources and returns correct responses when you click ‘Invoke’
  • Running `uv run python -m fastmcp --help` shows the server CLI without errors
  • After running the command, opening Claude Desktop shows a new MCP server named “leave manager” with its three tools listed

13 outcomes in all — one per recipe below.

4FAQ, Tips & How-to 13

one problem, one solution, one action
How-to Everyone

Want an LLM to fetch current Pokémon info

The video shows how to create a lightweight Model Context Protocol (MCP) server in Python that queries the public PokéAPI and exposes the query as an MCP tool. By registering the function with the server, any LLM client can call it via the standard MCP transport.

2MinutesPy ↗ Lesson → AI-generated
How-to Everyone

Need the model to call your own code

The tutorial demonstrates that MCP tools are just Python callables wrapped with metadata (name, description). Registering them lets any compatible LLM discover and invoke the function without additional glue code.

2MinutesPy ↗ Lesson → AI-generated
How-to Everyone

Need to run SQL from TypeScript but prevent any writes

By wrapping the official Python SDK with FastMCP and using tsx you can run TypeScript directly, exposing only read‑only queries. The server validates incoming tool calls, opens a BEGIN READ ONLY transaction, runs the query, then rolls back to guarantee no writes.

YouTube ↗ Lesson → AI-generated
How-to Everyone

Bad command‑line or JSON‑RPC arguments cause errors

Using Zod to parse and validate command‑line or JSON‑RPC arguments prevents malformed requests from reaching the database, making the MCP surface minimal and safe.

YouTube ↗ Lesson → AI-generated
How-to Everyone

User‑provided SELECTs must never change data

Wrapping every query in `BEGIN READ ONLY` followed by `ROLLBACK` leverages PostgreSQL’s native protection: even if a malicious SELECT contains a write via dblink or side‑effecting functions, the transaction aborts on any attempted modification.

YouTube ↗ Lesson → AI-generated
How-to Everyone

Need a JSON‑RPC API for my Python helpers

The Model Context Protocol (MCP) server is built by decorating ordinary Python functions with FastMCP decorators. Resources expose static data, tools perform actions, and prompts guide the LLM on how to call them. Using these decorators lets the server automatically publish a JSON‑RPC interface that any MCP client can discover.

KodeKloud ↗ Lesson → AI-generated
How-to Everyone

Can't get my IDE to talk to a local server

IDE extensions like Cursor, Cloud Code, or Root Code read an MCP.json file that describes how to launch and communicate with an MCP server. By specifying the command, arguments, working directory, and transport mode, the client can automatically start the server and discover its capabilities.

KodeKloud ↗ Lesson → AI-generated
How-to Everyone

Want to view the catalog and search flights

A client uses the FastMCP client library to connect via HTTP or stdio, query the server’s catalog, and call a tool with JSON‑RPC parameters. The response is returned as plain JSON, making it easy to integrate into any Python automation.

KodeKloud ↗ Lesson → AI-generated
How-to Everyone

The MCP inspector is a lightweight web UI that connects to any MCP server URL and displays its resources, tools, and prompts. It lets you manually invoke methods without writing code, confirming the server behaves as expected before integration.

KodeKloud ↗ Lesson → AI-generated
How-to Everyone

UV is a fast, cross‑platform Python package manager that can create a new project with all required files (pyproject.toml, main.py, README). Initializing with UV gives you a clean skeleton ready for the MCP SDK and FastMCP.

codebasics ↗ Lesson → AI-generated
How-to Everyone

Want LLMs to manage employee leave

FastMCP provides an ergonomic wrapper around the MCP protocol. By creating a FastMCP object and registering Python functions as tools, you expose callable actions (e.g., get_balance, apply_leave) that LLMs can invoke via function calls.

codebasics ↗ Lesson → AI-generated
How-to Everyone

Claude Desktop can’t see my MCP server

The `ubmcp install` command writes a JSON entry into Claude Desktop’s configuration, telling the client how to launch your server (the UV run command). Once registered, the desktop app automatically starts the server and makes its tools available.

codebasics ↗ Lesson → AI-generated
How-to Everyone

LLM calls your function with bad arguments

MCP relies on the function’s docstring to generate a schema for the language model. A clear description of each parameter, its type, and example values enables Claude (or any MCP client) to map natural‑language requests to the correct tool and format arguments properly.

codebasics ↗ Lesson → AI-generated

The same set on /recipes, filtered by tool and role.

5See also

💬 Discuss this chapter

Ask, share, or report — over on the Heidelberg AI community forum.