MCP (Model Context Protocol) Lesson 3 of 6 ⏱️ 4 min read

MCP Tools, Resources & Prompts

The three MCP primitives — when to expose something as a tool, a resource or a prompt — with Python examples of each.

Updated Oct 30, 2026

The three MCP primitives

Every MCP server exposes some combination of tools, resources and prompts. Choosing the right one for each capability is the single most important design decision in an MCP server.

Tools Resources Prompts
Who triggers it The model The host application The user
What it is An action or computation Read-only data A reusable message template
Side effects Allowed (with care) None None
Example create_ticket, run_sql schema://orders, file:///notes.md "Summarise this lecture"

Tools: things the model can do

Use a tool when the model needs to act or compute something it decides on during a conversation.

Python
from mcp.server.mcpserver import MCPServer
from mcp.server.mcpserver.exceptions import ToolError

mcp = MCPServer("course-helper")

COURSES = {
    "cs101": {"title": "Intro to Programming", "credits": 3},
    "cs201": {"title": "Data Structures", "credits": 4},
}

@mcp.tool()
def course_credits(course_code: str) -> int:
    """Return the number of credit hours for a course code such as 'cs101'."""
    course = COURSES.get(course_code.lower())
    if course is None:
        raise ToolError(f"Unknown course '{course_code}'. Known codes: {', '.join(COURSES)}")
    return course["credits"]

Good tools are narrow, well described, and safe even when called with unexpected input.

Resources: data the app can load

Use a resource for read-only content identified by a URI. The host decides when to attach it to the model's context — for example, when the user picks it from a menu.

Python
@mcp.resource("courses://catalog")
def course_catalog() -> str:
    """The full course catalogue as plain text."""
    return "\n".join(f"{code}: {c['title']} ({c['credits']} credits)" for code, c in COURSES.items())


@mcp.resource("courses://{course_code}/syllabus")
def course_syllabus(course_code: str) -> str:
    """Syllabus for one course. The {course_code} part of the URI becomes the argument."""
    course = COURSES.get(course_code.lower())
    if course is None:
        return f"No syllabus found for {course_code}."
    return f"Syllabus for {course['title']}: weekly lectures, labs and a final project."

The second resource is a resource template — the {course_code} placeholder lets one function serve many URIs like courses://cs101/syllabus.

Rule of thumb: if reading it changes nothing and it's identified by what it is rather than what to do, make it a resource.

Prompts: templates the user chooses

Use a prompt for a reusable instruction a user picks explicitly — many hosts show them as slash commands or menu items.

Python
@mcp.prompt()
def explain_error(error_message: str, language: str = "python") -> str:
    """Explain a programming error to a beginner and suggest a fix."""
    return (
        f"I'm a student learning {language}. Explain this error in simple terms, "
        f"show the most likely cause, and give a corrected example:\n\n{error_message}"
    )

Prompts are a great way to ship best-practice instructions alongside your server, so every user gets the same high-quality request.

Choosing the right primitive

Ask these questions in order:

  1. Does it change something (create, update, send, delete)? → Tool, with validation and, if risky, approval in the host.
  2. Is it data the model should read as context? → Resource.
  3. Is it an instruction template a person will choose? → Prompt.
  4. Does the model need to fetch data dynamically, mid-task, based on its own reasoning? → A read-only tool (e.g. search_courses(query)) is fine — not every host loads resources automatically.

Returning results well

  • Return structured data (dict, list) when the model needs to reason over fields; the SDK serialises it for you.
  • Keep payloads small — return the top results and offer pagination arguments (page, limit).
  • Raise ToolError with a clear message that tells the model how to fix the call ("Known codes: cs101, cs201"). Other exceptions reach the model only as a generic "Error executing tool".

Testing all three

Run mcp dev server.py and use the Inspector's Tools, Resources and Prompts tabs. Check that:

  • each tool's schema shows the right types and required fields,
  • resource URIs and templates resolve correctly,
  • prompt arguments appear with their defaults.

Common mistakes

  • Making everything a tool. Static reference data as a tool wastes model calls; as a resource it's loaded once.
  • Side effects in a resource. Resources must be read-only — never send email or write a file when one is read.
  • Prompts that hard-code data. Use arguments so one prompt serves many cases.
  • No error guidance. "Invalid input" helps nobody; say what valid input looks like.

Key takeaways

  • Tools = model-triggered actions; resources = app-loaded data; prompts = user-chosen templates.
  • Resource templates ({param} in the URI) serve families of related data.
  • Choose by side effects and by who should trigger the capability.
Finished this lesson?
Tick it off to track your MCP (Model Context Protocol) progress.
Next lesson