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.
- 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_requiredcarrying anelicitation/createrequest, and the client retries the original call with the answers.
Roots and Sampling are deprecated as of 2026-07-28 under SEP-2577. Deprecated features stay in the specification for at least twelve months, and new implementations SHOULD NOT 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:
| Field | Required | What it is |
|---|---|---|
name | yes | the identifier code matches on: ASCII letters, digits, underscore, hyphen and dot, 1 to 128 characters. snake_case is a convention, not a spec rule |
description | yes | what the model reads to decide when to use the tool |
inputSchema | yes | a JSON Schema for the parameters |
title | no | the 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 type | type value | What it carries | Added in |
|---|---|---|---|
| Text | text | a text string: JSON, or human-readable output | 2024-11-05 |
| Image | image | base64 data plus a MIME type | 2024-11-05 |
| Embedded resource | resource | a whole resource, text or binary, inline in the result | 2024-11-05 |
| Audio | audio | base64 data plus a MIME type | 2025-03-26 |
| Resource link | resource_link | a uri and a name, so the client can read the resource later | 2025-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 RFC 3986 URI, and the convention is to choose something meaningful and stable.
Resource content types
| Content | How it travels | Example MIME types |
|---|---|---|
| Text | a text string | text/plain, text/markdown, application/json |
| Blob | binary as base64 in a blob string | image/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 the client does | 2025-11-25 | 2026-07-28 |
|---|---|---|
| Open a subscription | resources/subscribe | subscriptions/listen, with a notifications filter |
| Choose which notifications arrive | all of them, once subscribed | only the types named in the filter |
| Where notifications travel on HTTP | a separate GET/SSE channel | the response stream of the listen request |
| Stop a subscription | resources/unsubscribe | close the stream, or notifications/cancelled on stdio |
| Where the subscription state lives | a registry on the connection | the 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 slash commands 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?
| Question | Answer | Use |
|---|---|---|
| Does the AI need to do something with a side effect? | Yes | Tool |
| Does the AI need to read data without side effects? | Yes | Resource |
| Do you want users to invoke a pre-built, reusable workflow? | Yes | Prompt |
| Should the AI autonomously decide when to use this? | Yes | Tool |
| Is this a stable, addressable piece of data? | Yes | Resource |
| Is this a structured, parameterised instruction template? | Yes | Prompt |
Common misclassifications
| The idea | The right primitive | Why |
|---|---|---|
a get_user tool | either | If 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 resource | Tool | Search is dynamic and model-controlled: the model decides when to search and what to search for. |
a write_code tool | either | Tool 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 annotation, 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:
| Primitive | Example | Who starts it |
|---|---|---|
| Tool | create_task(title, description, assignee, due_date) | the model, while reasoning |
| Tool | update_task_status(task_id, status) | the model |
| Tool | post_comment(task_id, comment) | the model |
| Resource | tasks://project-123, all tasks in a project | the user or the host, picking context |
| Resource | tasks://task-456, one task with full details | the user or the host |
| Resource | members://team-789, the team member list | the user or the host |
| Prompt | daily_standup, a standup summary from recent task activity | the user, from the menu |
| Prompt | sprint_review, a retrospective built from completed tasks | the user, from the menu |
Key Takeaways
| Primitive | What it is for | Who decides when it runs |
|---|---|---|
| Tools | the AI acts, and side effects are allowed | the model |
| Resources | the AI reads, and a read is expected to leave the server unchanged | the application or the user |
| Prompts | a pre-built workflow template with parameters | the 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,
outputSchemaandstructuredContent, 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/listandprompts/getshapes, and the rule that a PromptMessage carries only a user or an assistant role. - Subscriptions: the full
subscriptions/listenmechanics 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,Promptand 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/readandprompts/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
typevalues. It also carries the split betweenisErrorfor tool execution errors and JSON-RPC-32602for 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://andgit://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/listenreplacesresources/subscribeand the HTTP GET endpoint; the notification filter fields; the server MUST NOT send unrequested types; the acknowledgment and theio.modelcontextprotocol/subscriptionIdkey. - Deprecated Features: Roots and Sampling are deprecated as of
2026-07-28under 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_requiredthat the client answers on a retry. - Schema Reference:
readOnlyHintand 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
titlefield 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.