Skip to content

C4 · lesson 5 · 10 min read

MCP primitives: tools, resources and prompts

Choose between MCP tools, resources and prompts by who controls them, and know the client features and annotations in the current spec.

Ընտրեք տեքստը՝ հայերեն թարգմանությունը տեսնելու համար։Highlight text to see Armenian. Machine-assisted translation; English is the source of truth.

An MCP server can offer three kinds of things. They differ less in what they contain than in who decides to use them. That question is the fastest way to choose the right one.

toolsresourcespromptsmodel decidestools/listtools/callapplication decidesresources/listresources/readuser picksprompts/listprompts/getkubectl_get_podsrunbook://payments/mcp__k8s-ro__triage
Server primitives, by who is in control.

Tools: the model decides

Tools are functions the model calls when it thinks they help, for example kubectl_get_pods or pagerduty_ack_incident. Clients discover them with tools/list and run them with tools/call. A tool definition has a name, a description, an inputSchema and, optionally, a title, an outputSchema and annotations.

A tool result carries content, a list of text, image, audio, resource_link or embedded resource items. It can also include structuredContent: JSON that matches the outputSchema, if the tool declares one. isError: true marks a failed run (lesson 2).

Annotations are hints about behaviour:

Hint Meaning when true Default
readOnlyHint The tool doesn’t modify its environment false
destructiveHint It may make destructive changes (not only additive) true
idempotentHint Repeating it with the same arguments has no extra effect false
openWorldHint It talks to an open set of external entities true

The defaults are pessimistic on purpose. A tool that says nothing is treated as possibly destructive. Claude Code uses readOnlyHint and idempotentHint, for example to run read-only calls in parallel and to retry a safe call after a dropped connection.

Resources: the application decides

Resources are data identified by a URI, such as file:///etc/app.yaml, runbook://payments or postgres://schema/users. The host application decides when to load them and how much to show the model. Usually that happens because the user attached them. Clients use:

  • resources/list for fixed resources.
  • resources/templates/list for parameterised URIs, such as runbook://{service}.
  • resources/read to fetch the content.
  • subscriptions/listen to get change notifications. In 2026-07-28 this replaces the older resources/subscribe.

In Claude Code you attach a resource with an @ mention: @k8s-ro:runbook://payments.

Prompts: the user decides

Prompts are templates the user picks on purpose, usually from a slash menu. prompts/list shows them with their arguments, and prompts/get renders one into messages. Prompt arguments are simple named string values, not a JSON Schema. In Claude Code an MCP prompt appears as a command: /mcp__k8s-ro__triage payments.

# add to server.py from lesson 4
from pathlib import Path

RUNBOOKS = Path(__file__).parent / "runbooks"  # e.g. runbooks/payments.md

@mcp.resource("runbook://{service}")
def runbook(service: str) -> str:
    """On-call runbook for one service: owners, dashboards, known failure modes."""
    path = RUNBOOKS / f"{service}.md"
    if not path.is_file() or path.resolve().parent != RUNBOOKS.resolve():
        raise ValueError(f"No runbook for {service!r}")
    return path.read_text()

@mcp.prompt()
def triage(service: str) -> str:
    """Start a read-only triage of one service."""
    return (f"Triage {service}. First call kubectl_get_pods for its namespace, list pods that are "
            f"not Ready or have restarts, then read runbook://{service} and suggest next steps. "
            "Do not propose any change that writes to the cluster.")

Which primitive?

You want… Use
Claude to fetch or do something when it judges it useful Tool
A user or app to attach known context (schema, runbook, config) Resource
A repeatable workflow a person starts by name Prompt

A single server often offers all three. A database server might expose a query tool, a schema resource and a “write a safe migration” prompt.

Client features

Servers can also ask things of the client. In 2026-07-28 the active feature is elicitation (elicitation/create), which lets a server ask the user for input or confirmation. It is now carried in the result as an “input required” response, using the multi round-trip request pattern, rather than sent as a separate server-to-client request.

Sampling, roots and logging are deprecated as of 2026-07-28. They still work during the deprecation window, but new servers shouldn’t use them:

  • Instead of sampling, call an LLM API directly.
  • Instead of roots, pass paths as tool arguments or server configuration.
  • Instead of logging, write to stderr, or use OpenTelemetry.

Recap

  • Tools are model-controlled, resources are application-controlled (URIs and templates), and prompts are user-controlled (slash commands).
  • Tool results have content, optional structuredContent that matches outputSchema, and isError.
  • Annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) are hints with cautious defaults. Never trust them from an untrusted server.
  • In Claude Code, use @server:uri for resources and /mcp__server__prompt for prompts.
  • Elicitation is current. Sampling, roots and logging are deprecated in 2026-07-28.

See it in action

adding a remote MCP server to Claude Code · illustrative session
transcript
$ claude mcp add --transport http monitoring https://mcp.example.com/mcp
Added HTTP MCP server monitoring (local config)
$ claude
> /mcp
monitoring · connected · 3 tools
> what is the p95 latency of checkout in the last hour?
monitoring - query_metric(service: "checkout", stat: "p95", window: "1h")
p95 latency for checkout over the last hour is 412 ms (SLO 500 ms).

$ ./try-it.sh

Try it: add a runbook resource and a triage prompt

  1. Add the resource and prompt from this lesson to the k8s-ro server from lesson 4.
  2. Restart Claude Code, type @ and pick @k8s-ro:runbook://payments. Check that the runbook text is attached to your prompt.
  3. Type / and run /mcp__k8s-ro__triage payments. Check that Claude uses kubectl_get_pods while following the prompt.
  4. Mark kubectl_get_pods as read-only with annotations=ToolAnnotations(read_only_hint=True) (from mcp.types) and look at the annotations in the MCP Inspector.

Check yourself

3 questions. Original practice questions, not taken from the real exam.

  1. Your MCP server should offer each service's on-call runbook so that engineers can attach it to a conversation before asking Claude for help.

    1. Which MCP primitive fits best?

  2. A community MCP server you just installed marks its cleanup_namespace tool with readOnlyHint: true. A colleague suggests auto-approving every tool that has that hint.

    2. What does the MCP specification say?

  3. You're designing a new MCP server against spec 2026-07-28. It needs the user to confirm a target environment partway through a tool call.

    3. Which client feature should you use, given what this revision deprecates?

Answer every question to check.