Skip to main content

Class 3: Tools, Resources, and Prompts

Duration: ~25 minutes | Level: Beginner | Prerequisites: Class 2: Anatomy of MCP


Three Primitives, One Philosophy

An MCP server exposes three types of capabilities: Tools, Resources, and Prompts. Each one is a different contract about who decides when the capability runs and what side effects it may have. The wrong choice doesn't just violate the spec; it produces worse AI behaviour.

MCP is symmetrical: six primitives in all, three on each side.

The client calls the three on the left, and when the server needs something from the user it says so inside its answer. This class covers that box, the side you build first.

The client half
  • Roots tell the server which directories and files the client considers relevant. A root URI must be a file:// URI, and the protocol does not enforce that a server stays inside them, so a server validates its own paths.
  • Sampling lets the server ask the client to run a prompt through the user's chosen model.
  • Elicitation lets the server ask the user a question during a tool call. It returns a result of type input_required carrying an elicitation/create request, and the client retries the original call with the answers.

Roots and Sampling are deprecated as of 2026-07-28 under . Deprecated features stay in the specification for at least twelve months, and new implementations adopt them. Class 6: Capability Negotiation covers all three, and Six Primitives, Not Three is the blog post on them.


Tools: The AI Acts

A Tool is a function the AI can invoke. When the AI calls a tool, something happens in the real world: a database row is created, an API is hit, a file is written, an email is sent.

The defining characteristic

Tools are model-controlled. The AI decides when to call a tool, which tool to call, and what arguments to pass. Unlike a resource, a tool does not have to be invoked by a human.

That decision travels a fixed path:

All the model sees of a tool is the text of its name, description and schema.

Tools are the mechanism for agentic behaviour: an AI that takes actions, not just answers questions.

What a tool definition looks like

Every tool has three required parts and one optional one worth filling in:

FieldRequiredWhat it is
nameyesthe identifier code matches on: ASCII letters, digits, underscore, hyphen and dot, 1 to 128 characters. snake_case is a convention, not a spec rule
descriptionyeswhat the model reads to decide when to use the tool
inputSchemayesa for the parameters
titlenothe human-readable name a client shows the user, so that name can stay a programmatic identifier
{
"name": "search_products",
"title": "Search products",
"description": "Search the product catalogue by name, category, or price range. Use this when the user asks about available products or wants to find items matching specific criteria.",
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string", "description": "Search terms" },
"max_price": { "type": "number", "description": "Maximum price in USD" },
"category": { "type": "string", "enum": ["electronics", "clothing", "books"] }
},
"required": ["query"]
}
}

Good tool descriptions

The description is the most important part of a tool: a vague one leads to missed calls or inappropriate calls.

Bad: "search_products", searches products
Good: "search_products", Search the product catalogue by name, category, or price range. Use this when the user asks about available products or wants to find items matching specific criteria. Do not use for inventory or order status queries."

The description is also text the server writes and the host feeds straight into the model's context, so a server you do not control can put instructions there. That is the prompt injection surface: treat a description from an untrusted server the way you treat any other untrusted input. Your Tool Description Is a Prompt works through what a good one looks like.

Tool results

A tool call returns content. These five types are the whole list in 2026-07-28:

Content typetype valueWhat it carriesAdded in
Texttexta text string: JSON, or human-readable output2024-11-05
Imageimage data plus a 2024-11-05
Embedded resourceresourcea whole resource, text or binary, inline in the result2024-11-05
Audioaudiobase64 data plus a MIME type2025-03-26
Resource linkresource_linka uri and a name, so the client can read the resource later2025-06-18

For results code has to read, a tool can declare an outputSchema and return its data in structuredContent. Servers MUST conform to the schema they declared and clients SHOULD validate against it, and the specification asks for the same JSON in a text block as well, for clients written before the field existed.

Tools report failure in two ways. A request the server cannot dispatch, an unknown tool name or malformed arguments, comes back as a JSON-RPC error with code -32602. A call that ran and failed, a timed-out API or a rejected date, comes back as a normal result carrying the error text, with isError set to true. Clients SHOULD hand the second kind to the model, which can read it and decide what to do next.

// a tool execution error, returned as a normal result
{
"content": [{ "type": "text", "text": "No product matched 'lapptop'. Try 'laptop'." }],
"isError": true
}

Resources: The AI Reads

A Resource is a piece of data the AI can access. Files, database records, API responses, live metrics, documentation pages, anything with a stable identifier and readable content.

The defining characteristic

Resources are application-controlled (or can be). The human or the host decides which resources are attached to a conversation, and the AI typically does not choose which ones to fetch the way it chooses to call tools.

The same exchange, drawn for a resource:

No arrow starts at the model, which is the difference from the tools diagram. "Attached" is worth taking literally: the protocol delivers the content to the application, which can pass all of it to the model, select the relevant part, or use it some other way. Whatever it passes travels as ordinary text in the request, because the model's API does not know what an MCP resource is. The content belongs to the request instead of the model's training, so the model has it only while the application keeps sending it. The resources/read result carries it like this:

// resources/read response
{
"contents": [
{
"uri": "file:///project/README.md",
"mimeType": "text/markdown",
"text": "# Project\n\nSetup instructions..."
}
]
}

Reading a resource is expected to leave the server unchanged. The protocol does not verify that, so it is a contract the server keeps and not a guarantee the client can check.

Everything a resource holds can reach the model, and from there the transcript and whatever tool the model calls next, so the server still decides who may read what. The specification asks for access controls on sensitive resources and URI validation on every read; OWASP files the failure as sensitive information disclosure.

Resource addressing

Every resource has a URI, a stable identifier with a scheme:

file:///home/user/project/README.md
postgres://mydb/public/users/42
github://repos/myorg/myrepo/issues/123
metrics://prometheus/cpu_usage?window=1h

MCP standardises https://, file:// and git://, and lets servers define custom schemes for everything else. A custom scheme must be a valid URI, and the convention is to choose something meaningful and stable.

Resource content types

ContentHow it travelsExample MIME types
Texta text stringtext/plain, text/markdown, application/json
Blobbinary as base64 in a blob stringimage/png, application/pdf

A resource can also be referenced instead of inlined, through a ResourceLink carrying the resource's URI and name, plus an optional MIME type and description. Tools that produce many or large resources return ResourceLinks, and the client decides whether to read them. A link a tool returns does not have to appear in resources/list.

Resource discovery

Clients list resources with resources/list. A resource whose URI has a variable part is listed instead by resources/templates/list, as a URI template the client fills in:

// resources/templates/list response
{
"resourceTemplates": [
{
"uriTemplate": "tasks://{task_id}",
"name": "task",
"description": "One task with full details",
"mimeType": "application/json"
}
]
}

Without templates, a server lists every task separately.

How change notifications work depends on which revision you target, and 2026-07-28 rewrote this one. It replaced resources/subscribe with a single notification stream: a client opens a long-lived subscriptions/listen request and, in params.notifications, names exactly the types it wants. resourcesListChanged brings notifications/resources/list_changed, and resourceSubscriptions, an array of resource URIs, brings notifications/resources/updated for those resources. toolsListChanged and promptsListChanged sit in the same filter, so all three primitives share one stream.

The acknowledgment is the step to watch. It MUST arrive before any notification on that subscription, and it reports the subset of the filter the server agreed to honour, so a client compares it against what it asked for. The server MUST NOT send a type the client did not ask for. Every notification carries io.modelcontextprotocol/subscriptionId in its _meta, the JSON-RPC id of the listen request, so several subscriptions over one transport can be told apart.

What changed, and why
What the client does2025-11-252026-07-28
Open a subscriptionresources/subscribesubscriptions/listen, with a notifications filter
Choose which notifications arriveall of them, once subscribedonly the types named in the filter
Where notifications travel on HTTPa separate channelthe response stream of the listen request
Stop a subscriptionresources/unsubscribeclose the stream, or notifications/cancelled on stdio
Where the subscription state livesa registry on the connectionthe request

2026-07-28 removed resources/subscribe, resources/unsubscribe and the HTTP GET channel (SEP-2575). The reason is statelessness: a per-connection subscription registry is connection state, and this revision's whole thrust is removing that. The Java MCP SDK still implements 2025-11-25, so the subscribe and unsubscribe shape is what the Java code on this site uses.

// resources/list response
{
"resources": [
{
"uri": "file:///project/README.md",
"name": "README.md",
"description": "Project overview and setup instructions",
"mimeType": "text/markdown"
}
]
}

Prompts: Reusable AI Workflows

A Prompt is a parameterised instruction template that surfaces in the client UI as something the user can invoke. Think of them as with arguments, structured, repeatable workflows.

The defining characteristic

Prompts are user-controlled. A user invokes one from the host application's UI, typically a dropdown or slash command, and the server returns a pre-built conversation: user and assistant messages carrying the instructions and context for one task. MCP prompt messages do not use a system role, and the host decides how to present them to the model.

The host lists the prompts, and the user picks one:

The message text comes from the server, and the user only chooses which prompt runs and when. That is what "user-controlled" means, and it is why the specification requires implementations to validate prompt inputs and outputs to prevent injection attacks.

Why prompts exist

Without prompts, every user writes the same "review my code carefully and check for security issues and edge cases" instruction from scratch, in slightly different ways, with slightly different results. A prompt server standardises that instruction across your team.

// prompts/list response
{
"prompts": [
{
"name": "code_review",
"description": "Perform a thorough code review focusing on security, correctness, and style",
"arguments": [
{ "name": "language", "description": "Programming language", "required": false },
{ "name": "focus", "description": "Optional specific area: security | performance | style", "required": false }
]
}
]
}

prompts/get returns a GetPromptResult with an array of messages, the assembled conversation starter:

{
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "Please review the following code for security vulnerabilities, correctness issues, and style problems. Be specific about line numbers when possible.\n\n[code here]"
}
}
]
}

The Decision Matrix: Which Primitive Do I Use?

QuestionAnswerUse
Does the AI need to do something with a side effect?YesTool
Does the AI need to read data without side effects?YesResource
Do you want users to invoke a pre-built, reusable workflow?YesPrompt
Should the AI autonomously decide when to use this?YesTool
Is this a stable, addressable piece of data?YesResource
Is this a structured, parameterised instruction template?YesPrompt

Common misclassifications

The ideaThe right primitiveWhy
a get_user tooleitherIf the application or the user selects whose data to attach as context, expose it as a Resource (users://42). If the model must decide at runtime which user to look up, use a read-only Tool.
a search resourceToolSearch is dynamic and model-controlled: the model decides when to search and what to search for.
a write_code tooleitherTool if the AI calls it as part of its own reasoning. Prompt if the user invokes a code-writing workflow from a menu.

The get_user case falls to a Tool because Resources are application-controlled and most hosts do not let the model fetch them on its own. Mark such a tool with the readOnlyHint , which declares to the client that the tool does not change anything. That is the server describing its own tool, and the schema reference tells clients to treat annotations from a server they do not trust as untrusted, so a client should not drop a confirmation on the strength of one.


All Three Together: A Full Example

An MCP server for a project management system:

PrimitiveExampleWho starts it
Toolcreate_task(title, description, assignee, due_date)the model, while reasoning
Toolupdate_task_status(task_id, status)the model
Toolpost_comment(task_id, comment)the model
Resourcetasks://project-123, all tasks in a projectthe user or the host, picking context
Resourcetasks://task-456, one task with full detailsthe user or the host
Resourcemembers://team-789, the team member listthe user or the host
Promptdaily_standup, a standup summary from recent task activitythe user, from the menu
Promptsprint_review, a retrospective built from completed tasksthe user, from the menu

Key Takeaways

PrimitiveWhat it is forWho decides when it runs
Toolsthe AI acts, and side effects are allowedthe model
Resourcesthe AI reads, and a read is expected to leave the server unchangedthe application or the user
Promptsa pre-built workflow template with parametersthe user
  • Prefer Resources for browsable, user-selected context; use a read-only Tool (with readOnlyHint) when the model must choose what to fetch at runtime
  • The distinction encodes safety and control semantics, not just API style

In the next class, we look at how all three primitives are communicated over the wire: the JSON-RPC protocol layer.


Further Reading

  • Tools: the normative rules behind this class's tool section, including the name character rules, the five content types, outputSchema and structuredContent, and the two error mechanisms.
  • Resources: the application-driven interaction model, the common URI schemes, resource templates, and the security considerations for exposing data.
  • Prompts: the user-controlled model, the prompts/list and prompts/get shapes, and the rule that a PromptMessage carries only a user or an assistant role.
  • Subscriptions: the full subscriptions/listen mechanics this class summarises, including the filter fields, the acknowledgment, the subscription id and how a client cancels.
  • Deprecated Features: the registry that lists Roots and Sampling as deprecated, with the migration path and the earliest revision that may remove them.
  • Elicitation: how a server asks a user a question in this revision, in form mode or url mode, and what a form is forbidden to ask for.
  • Schema Reference: the generated type reference, where you can look up the exact fields of Tool, Resource, Prompt and every result shape in this revision.
  • Understanding MCP servers: the same three primitives with a worked scenario, and the method tables for tools/list, resources/read and prompts/get.

Sources

  • Tools: tools are model-controlled, the name character set and the 1 to 128 character length, and the five content types with their type values. It also carries the split between isError for tool execution errors and JSON-RPC -32602 for protocol errors, and the rule that clients MUST treat tool annotations as untrusted.
  • Resources: resources are application-driven; text and blob content; the https://, file:// and git:// schemes; custom schemes MUST follow RFC 3986; access controls SHOULD be implemented for sensitive resources.
  • Prompts: prompts are user-controlled and typically appear as slash commands; the server writes the content; PromptMessage roles are limited to user and assistant.
  • Subscriptions: subscriptions/listen replaces resources/subscribe and the HTTP GET endpoint; the notification filter fields; the server MUST NOT send unrequested types; the acknowledgment and the io.modelcontextprotocol/subscriptionId key.
  • Deprecated Features: Roots and Sampling are deprecated as of 2026-07-28 under SEP-2577, and may be removed in the first revision released on or after 2027-07-28.
  • Roots: roots are informational guidance, the protocol does not enforce that a server stays within them, and a root URI must be a file:// URI.
  • Elicitation: elicitation travels as a result of type input_required that the client answers on a retry.
  • Schema Reference: readOnlyHint and the other tool annotations are hints that are not guaranteed to describe a tool faithfully.
  • Key Changes in 2025-06-18: resource links, structured tool output and the title field were added in this revision.
  • Key Changes in 2025-03-26: audio content was added in this revision, joining the existing text and image content types.
  • SEP-2575: Make MCP Stateless: the proposal that removed the per-connection subscription registry.
  • modelcontextprotocol/java-sdk releases: the 2.x line of the Java MCP SDK still implements 2025-11-25.
  • RFC 3986: Uniform Resource Identifier (URI): Generic Syntax: the URI syntax that a custom MCP resource scheme must follow.
  • LLM01:2025 Prompt Injection: the attack behind the warning that tool descriptions and prompt templates are server-authored text arriving in the model's context.
  • LLM02:2025 Sensitive Information Disclosure: the confidentiality risk in deciding which data a model may read.