Lab 06 — Add a resource and a prompt to PKB
Lesson 06 · MCP Servers · ~25 min hands-on
By the end of this lab the PKB server exposes two new entry points beyond the search tool you built in
Lesson 05: a static resource at pkb://about, a dynamic resource template at
pkb://notes/{title}, and a welcome prompt. All three should be visible from
the MCP Inspector's Resources and Prompts tabs without any extra wiring beyond FastMCP decorators.
Prereqs
- Lesson 05 lab finished — your PKB server runs and
pkb.searchworks under Inspector. - Editable install still in place:
pip install -e .from the project root. - The
NOTESdict inpkb/tools.pystill contains at least one note (e.g.hello).
Step 1 — Create pkb/resources.py
Resources live in their own module so the wiring in server.py stays small. Create the file
alongside tools.py with the following content:
"""Resources exposed by PKB."""
from mcp.shared.exceptions import McpError
from mcp.types import ErrorData, METHOD_NOT_FOUND
from pkb.tools import NOTES
def get_note(title: str) -> str:
"""Return the body of one note by title. URI: pkb://notes/{title}."""
if title not in NOTES:
raise McpError(ErrorData(
code=METHOD_NOT_FOUND,
message=f"unknown note: {title}",
))
return NOTES[title]
def about() -> str:
"""Static resource describing the PKB server. URI: pkb://about."""
return (
"PKB — Personal Knowledge Base MCP server (course sample).\n"
"Exposes notes via pkb://notes/{title} and tools for searching them."
)
What this does, line by line:
- Imports —
McpError,ErrorData, and theMETHOD_NOT_FOUNDsentinel are the structured way to signal "this resource doesn't exist." The client receives a proper JSON-RPC error instead of a Python traceback. NOTESre-use — we import the same in-memory dict the search tool uses. One source of truth for the lab; in production this would be a database or filesystem.get_note(title)— the dynamic side. Thetitleargument becomes the path variable in the URI templatepkb://notes/{title}.about()— the static side. No arguments, fixed URIpkb://about, returns a string the client treats as the resource body.
Step 2 — Create pkb/prompts.py
Prompts are even simpler — a function that returns the rendered string. FastMCP handles the rest.
"""Prompts exposed by PKB."""
def welcome(name: str = "friend") -> str:
"""Render the welcome prompt."""
return (
f"Hi {name}! I'm your PKB assistant. "
"Want me to summarize today's notes or search for something specific?"
)
The default name="friend" means the prompt can be invoked with no arguments and still
render. In Inspector the argument will appear with an empty input box and a placeholder hint.
Step 3 — Wire server.py
Add the new imports near the top of pkb/server.py, then register the resource and prompt
handlers with FastMCP decorators. The additions look like this:
from pkb import resources, prompts
@mcp.resource("pkb://about")
def about_resource() -> str:
return resources.about()
@mcp.resource("pkb://notes/{title}")
def note_resource(title: str) -> str:
return resources.get_note(title)
@mcp.prompt()
def welcome(name: str = "friend") -> str:
return prompts.welcome(name)
The decorators do the registration: @mcp.resource("pkb://...") attaches the function as
a resource handler with that URI (static or template, depending on whether the URI contains
{placeholders}); @mcp.prompt() registers the function as a prompt named
after the function itself. FastMCP introspects the signature to figure out the argument schema.
Step 4 — Reinstall (only if you added new files outside the package)
Both resources.py and prompts.py sit inside the existing pkb/
package, so the editable install picks them up automatically — no reinstall needed. If you ever add a
new top-level module or change pyproject.toml, rerun:
pip install -e .
Step 5 — Run under Inspector
From the project root:
mcp dev pkb/server.py
The Inspector UI opens in your browser. Click the Resources tab. You should see two entries:
pkb://about— static, no input fields.pkb://notes/{title}— template, with atitleinput box.
Step 6 — Read the about resource
In the Resources tab, click pkb://about, then click Read. The response
pane shows the static text returned by pkb/resources.py:about:
PKB — Personal Knowledge Base MCP server (course sample).
Exposes notes via pkb://notes/{title} and tools for searching them.
Step 7 — Read a dynamic resource
Click pkb://notes/{title}. Fill in title=hello (or whatever key exists in
your NOTES dict). Click Read. The response pane shows the note body.
Now try title=does-not-exist. You should see a structured error response with code
METHOD_NOT_FOUND and the message unknown note: does-not-exist. This is the
McpError you raised in resources.py being serialised over JSON-RPC.
Step 8 — Invoke the prompt
Click the Prompts tab. You should see one entry, welcome. Click it,
fill in name=Tester (or leave it blank to use the default), and click
Get Prompt. The rendered message appears:
Hi Tester! I'm your PKB assistant. Want me to summarize today's notes or search for something specific?
Step 9 — Confirm in Claude Desktop
Restart Claude Desktop (the PKB config from Lesson 04 is still in place — no edits needed). In a new chat, type:
read the pkb://about resource
Claude routes that through the resource read API on the PKB server and quotes the response back.
Depending on the Claude Desktop build, the welcome prompt may also surface as a slash
command (/welcome) in the prompt menu — try it if your build supports it.
What you should leave with
- PKB now exposes 2 tools (
pkb.search, plus whatever else you had) + 2 resources (pkb://about,pkb://notes/{title}) + 1 prompt (welcome). - All five are discoverable through Inspector with zero extra config.
- Errors from the dynamic resource come back as structured JSON-RPC errors, not tracebacks.
Troubleshooting
Common stumbles
- "Resources tab is empty" — confirm the
@mcp.resourcedecorator is imported from the same FastMCP instance you use elsewhere inserver.py. Restartmcp devafter edits — it does not always pick up changes hot. - "Reading
pkb://notes/{title}returns a Pydantic-style error" — FastMCP expects the path variable name in the URI template to match the function argument name exactly. If your URI says{title}but your function takesnote_title, the lookup fails. Rename one or the other. - "Prompts tab is empty" — confirm
@mcp.prompt()(with the parentheses) is applied to a function that returns a string. A function returningNoneor a non-string type is silently dropped.