learn-mcp-with-phoebe / Builder session 3 of 8
Learn MCP with Phoebe · Builder track · Session 3 of 8

Resources and prompts

Tools act; resources inform; prompts guide. Last session your server did things. Tonight you give it context to read and playbooks users can pick, and by the end your daybreak-mcp matches the running project exactly: three tools, two resources, one prompt. The recurring lesson is control - who decides when each primitive fires. Get that triangle right and the rest is decoration.

🟡 Medium Practitioners: DE · DS · AI engineers · builders Python · spec 2025-11-25 45 min
0-3 · Welcome 3-18 · Resources 18-36 · Prompts 36-45 · Round out + Q&A
Part 0

Completing the server trio

A server that only exposes tools is a server missing two-thirds of its vocabulary. Resources are read-only content the app pulls into context - a catalog, a policy doc. Prompts are templates the user picks explicitly, slash-command style. Same decorators you already know, different control model. By the end of tonight server.py is complete and identical to the project reference.

Live - presented in session Self-study - read after class ▶ Live MCP wire - step through real messages Official sources covered
★ What you walk out with today Two working resources with clean URIs (one static, one computed from the db), a monthly_report prompt whose argument becomes a form field in the host, a firm grip on the model/app/user control triangle, and a daybreak-mcp server that matches the running project line for line.
Part 1 · covers official "Resources", DLAI "Adding Prompt and Resource Features", Academy "Defining resources"

Resources: context with a URI 7 min live

A resource is read-only content the application decides to attach to a conversation. You register one with @mcp.resource("uri"): the decorator argument is the URI, the function returns the content, and the return type hint sets the mime type. The URI scheme is yours to design - daybreak:// here - and it is how the app addresses the content.

One resource, four fields the app sees uri daybreak://catalog name catalog mimeType text/plain contents the string you choose the scheme from the fn name from the return type what the fn returns Two flows on the wire: resources/list gives the app the menu; resources/read fetches one uri's contents. App-controlled: the application decides which resources enter context, not the model and not the user.
🔍 Click to zoom - the anatomy of a resource
LiveThe catalog resource, line by line4 min▶

Daybreak's catalog is computed from the database every time it is read - not a static file. That is the point of a resource function: it can run code. Here it queries products and formats a markdown list.

python · a computed resource@mcp.resource("daybreak://catalog") def catalog() -> str: """Daybreak product catalog with prices.""" rows = _query("SELECT name, category, price, roast FROM products ORDER BY price DESC") lines = ["# Daybreak catalog"] for name, category, price, roast in rows: roast_note = f" ({roast})" if roast else "" lines.append(f"- {name} - {category} - ${price:.2f}{roast_note}") return "\n".join(lines)
  • @mcp.resource("daybreak://catalog") - the URI is the argument; the function name catalog becomes the resource name.
  • Returns str, so the mime type is text. Return bytes or set a type for binary content.
  • Computed live - add a product to the db and the next read reflects it. No caching to invalidate.
LiveURI design taste - nouns, hierarchies, no verbs3 min▶

The scheme is yours, so design it like an API surface, not a dumping ground. Daybreak uses daybreak://catalog and daybreak://policy/refunds - notice the second one nests under a policy/ namespace.

  • Nouns, not verbs. Resources are things you read. daybreak://catalog, not daybreak://get-catalog. Verbs are for tools.
  • Hierarchies group. policy/refunds, policy/shipping - a namespace that scales as you add docs.
  • Stable + addressable. The app stores the URI; renaming it breaks references. Choose once.
  • Your scheme, your rules. daybreak:// means nothing to anyone else - and that is fine. It only has to be consistent within your server.
Static vs computed A resource can return a constant (like the refund policy text) or run a query (like the catalog). Both are just functions - the caller cannot tell which, and should not have to.
Part 2 · covers official "Prompts", DLAI "Adding Prompt and Resource Features", Academy "Defining prompts"

Prompts: playbooks users pick 7 min live

A prompt is a server-authored template the user chooses on purpose - it shows up as a slash command or menu item in the host. Register it with @mcp.prompt(); the function's parameters become the form fields the host renders. Daybreak ships one: monthly_report(month), the founder's standing ops report.

Livemonthly_report, walked through4 min▶

The prompt returns the text that will be handed to the model when the user picks it. Its month parameter becomes a field the host asks the user to fill - which is why the wire message carries an arguments declaration.

python · the prompt@mcp.prompt() def monthly_report(month: str) -> str: """Standard monthly ops report for the founder.""" return ( f"Write the {month} ops report for Daybreak: orders and revenue vs the " f"previous month, refunds, subscription changes, and one concrete " f"action for next month. Use query_sales, top_products and " f"subscription_status for the numbers - do not estimate." )
  • month: str becomes a required argument the host renders as a field - the same wire mechanism as a tool's input schema.
  • The template tells the model to use the tools - a prompt orchestrates your own primitives. Pick monthly_report for 2026-03 and the model will call query_sales, see $150.00, and write the dip up.
  • "do not estimate" - the prompt is where you encode the house rules for a recurring task.
Self-studyWhen a prompt beats a system-prompt doc3 min read▶

You could paste "here is how we write the monthly report" into a system prompt and be done. Sometimes that is right. But a prompt primitive wins when:

  • The user should trigger it explicitly. A slash command is discoverable and intentional - the user knows they are running the report, versus hoping the model remembers a buried instruction.
  • It takes arguments. month is a parameter, not a copy-paste edit. The host collects it cleanly.
  • It lives with the server. Update the report format once, on the server, and every host that connects gets the new version - no chasing pasted system prompts across teammates.

Rule of thumb: reusable, parameterized, user-triggered workflows are prompts. One-off context is a system prompt. Recurring reference material is a resource.

Part 3 · covers official completion + pagination utilities

Round out the server 5 min live

The server trio is complete. Two utilities exist that you should know by name even though Daybreak does not need them yet, and then we confirm your file matches the reference exactly.

TOOLS - model-controlled query_sales(month) top_products(from, to) subscription_status() RESOURCES - app-controlled daybreak://catalog daybreak://policy/refunds PROMPTS - user-controlled monthly_report(month) the user picks it; it drives the three tools above Three primitives, three controllers. The whole server is 3 tools + 2 resources + 1 prompt over daybreak.db. Utilities like completion and pagination ride on top when a list grows - Daybreak is small enough to skip them.
🔍 Click to zoom - the complete Daybreak capability map, grouped by control
Self-studyCompletion and pagination, briefly and honestly2 min read▶

Two protocol utilities you will meet in bigger servers, named here so they are not a surprise:

  • Completion - the server can offer autocomplete suggestions for a prompt or resource argument (like a month picker suggesting valid months). Nice UX, optional, skipped here.
  • Pagination - when a list response is long, the server returns a cursor and the client asks for the next page. Daybreak lists three tools and two resources, so there is nothing to paginate. You will care about this when a server exposes hundreds of resources.

Neither changes how you write tools, resources, or prompts - they are protocol plumbing the SDK handles when you need them.

LiveThe finished b3 server matches the project exactly3 min▶

At this point your server.py is the running-project reference. Bottom of the file, unchanged since b2:

python · the two resources + the run block@mcp.resource("daybreak://policy/refunds") def refund_policy() -> str: """Refund and cancellation policy.""" return ( "# Daybreak refund + cancellation policy\n" "- Full refund within 14 days of any order, no questions asked.\n" "- Subscriptions cancel any time; the current month still ships.\n" "- Refunded orders keep their row in the orders table with status " "'refunded' - revenue reports must exclude them." ) if __name__ == "__main__": mcp.run() # stdio transport - b6 switches this to streamable-http

Restart the Inspector and confirm all six capabilities appear: three tools, two resources, one prompt. That is a complete MCP server, and it is yours.

Demo 1 of 2

Ship the catalog resource ★ 10 min · everyone builds

Add the first resource and read it through the Inspector's Resources tab.

Paste the catalog resource from Part 1 into server.py, below your tools.

Restart the Inspector: uv run mcp dev server.py.

Open the Resources tab. You should see daybreak://catalog listed - that is resources/list.

Read it. Click the URI; the Inspector fires resources/read and shows the markdown catalog, priced high to low.

Demo 2 of 2

Ship monthly_report + the refund policy ★ 10 min · build

Add the second resource and the prompt, and test the prompt in the Inspector - watch it ask you for the month.

Paste refund_policy (Part 3) and monthly_report (Part 2) into server.py.

Restart the Inspector and open the Prompts tab. monthly_report appears with a month field.

Fill month=2026-03 and get the prompt. Read the returned template - it instructs the model to use your three tools and not estimate.

Confirm the full set: 3 tools, 2 resources, 1 prompt. Your server now matches the project reference exactly.

Homework

Try it yourself - this week ◐ 25-35 min total

Source material

Official sources covered

This session teaches the working core of the resource and prompt material across the official docs and the major courses, on the running project. Certificates and videos stay official. This page covers:

✓modelcontextprotocol.io - "Resources" + "Prompts" server concepts@mcp.resource URIs, @mcp.prompt arguments, control model - taught on Daybreak
✓DeepLearning.AI "MCP" - "Adding Prompt and Resource Features"Both primitives, full working content, on our own server
✓Anthropic Academy "Introduction to MCP" - defining resources + promptsDefining and accessing resources, defining prompts
◐modelcontextprotocol.io - completion + pagination utilitiesNamed and explained; not built (Daybreak is small enough to skip them)
Check yourself

Three questions before you go 🎯 ◐ 90 seconds

1 · In @mcp.resource("daybreak://catalog"), who defines the daybreak:// scheme?

The scheme is yours. daybreak:// means nothing outside your server, and that is fine - it just has to be a stable, addressable name the app can store and read from.

2 · What do a prompt function's parameters become?

A prompt's parameters (like monthly_report's month) travel as an arguments declaration on the wire, which is exactly what lets the host render a form and collect the user's input.

3 · Who controls when a resource enters the conversation?

The control triangle: model controls tools, user controls prompts, app controls resources. The application decides which read-only content to attach; the server only offers it.

Builder session 3 cheat sheet · pin this

Resource@mcp.resource("daybreak://catalog") over a function. URI is yours; return type sets the mime type.
Static or computedA resource can return a constant or run a query. Caller cannot tell; both are just functions.
URI tasteNouns not verbs, hierarchies (policy/refunds), stable + addressable. Verbs belong to tools.
Prompt@mcp.prompt() over a function. Parameters become the form fields the host renders.
The control triangleModel controls tools · app controls resources · user controls prompts. The server only offers.
Prompt vs system docReusable + parameterized + user-triggered -> prompt. One-off -> system prompt. Reference -> resource.
Wire flowsresources/list then resources/read · prompts/list then prompts/get. Same shape as tools.
Complete server3 tools + 2 resources + 1 prompt over daybreak.db - matches project/server.py exactly.