Explainer

MCP resources vs tools vs prompts: who decides, and when to use each

MCP servers expose three primitives with three different owners: the model, the app and the user. Here's how each works on the wire in the 2026-07-28 spec, plus what happened to sampling, roots and elicitation.

One glass hub wired to three cards on a cobalt gradient, titled Three primitives

In 20 seconds

  1. Tools are model-controlled actions, resources are application-driven context, and prompts are user-selected templates. Pick by asking who should decide when the thing is used.
  2. In the current 2026-07-28 spec, every request carries its own version and capabilities, servers ask for user input with an input_required result instead of calling the client, and sampling, roots and logging are deprecated.
  3. Clients differ: almost every host supports tools, but resource and prompt handling, elicitation and extensions vary by app.
On this page

An MCP server can offer three kinds of things to an AI app: tools, resources and prompts. They look similar on the wire, and many servers expose everything as tools by default. But the spec draws a clear line between them, and it isn't about data shape. It's about who decides when each one is used: the model, the application, or the user. Get that right and your server behaves predictably in every host. Get it wrong and you end up with a model calling things it shouldn't, or useful context that no client ever surfaces.

This explainer covers the three server primitives, the client-side features that sit next to them (elicitation, sampling, roots), and the newer Tasks extension, using the current 2026-07-28 revision of the spec. If you're new to MCP, start with our MCP explainer. If you want working code, our TypeScript server guide builds one of each.

Last verified: October 8, 2026

The one-line rule

PrimitiveSpec wordingWho decidesGood forBad for
Tools"model-controlled"The model, with the host's approvalActions, searches, computationsStatic context the user should choose
Resources"application-driven"The host app (often the user via a picker)Files, schemas, docs, records to readAnything with side effects
Prompts"user-controlled"The user, explicitlyReusable workflows, slash commandsThings the model should trigger by itself

The spec adds a caveat to each: implementations are "free to expose" these "through any interface pattern that suits their needs." So these are design intents, not enforcement. Hosts can, and do, let the model read resources automatically.

A note on the 2026-07-28 wire format

The current spec changed every request. There is no longer an initialize handshake; instead each request carries _meta fields with the protocol version and the client's capabilities, for example:

JSON
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/list",
  "params": {
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientCapabilities": {},
      "io.modelcontextprotocol/clientInfo": { "name": "raw-test", "version": "0.0.1" }
    }
  }
}

Results now carry a required resultType ("complete" or "input_required"), and list or read results carry cache hints (ttlMs, cacheScope). The spec's own examples omit _meta for brevity, and so do most examples below. The response bodies shown here are wire captures from the example server in our TypeScript server guide, which we compiled and ran with @modelcontextprotocol/server 2.3.1 on Node.js 22 on October 8, 2026 to check that the code works. They are protocol output, not model output.

A client can ask what a server offers with server/discover. The example server answered:

JSON
{
  "supportedVersions": ["2026-07-28"],
  "capabilities": {
    "tools": { "listChanged": true },
    "resources": { "listChanged": true },
    "prompts": { "listChanged": true }
  },
  "resultType": "complete",
  "ttlMs": 0,
  "cacheScope": "private",
  "_meta": { "io.modelcontextprotocol/serverInfo": { "name": "style-helper", "version": "1.0.0" } }
}

Tools: things the model can do

A tool is a named function with a JSON Schema for its input and, optionally, its output. The model sees the list, decides to call one, and the host sends tools/call. Here is the example server's tools/list result:

JSON
{
  "tools": [
    {
      "name": "word_count",
      "title": "Word counter",
      "description": "Count words, sentences and characters in a piece of text.",
      "inputSchema": {
        "type": "object",
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "properties": {
          "text": { "type": "string", "maxLength": 100000, "description": "The text to analyze" }
        },
        "required": ["text"]
      },
      "annotations": { "readOnlyHint": true, "openWorldHint": false },
      "outputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "words": { "type": "number" },
          "sentences": { "type": "number" },
          "characters": { "type": "number" }
        },
        "required": ["words", "sentences", "characters"],
        "additionalProperties": false
      }
    }
  ],
  "resultType": "complete",
  "ttlMs": 0,
  "cacheScope": "private"
}

And a call result. Note both content (text for older clients) and structuredContent (machine-readable), which the spec recommends sending together:

JSON
{
  "content": [{ "type": "text", "text": "{\"words\":3,\"sentences\":1,\"characters\":16}" }],
  "structuredContent": { "words": 3, "sentences": 1, "characters": 16 },
  "resultType": "complete"
}

Things to know:

  • Two kinds of errors. Unknown tools or malformed requests are JSON-RPC errors. Business failures ("date must be in the future") go back as a normal result with isError: true, so the model can read the message and retry.
  • Annotations are hints. readOnlyHint, destructiveHint and friends help hosts decide when to ask for confirmation, but the spec says clients "MUST consider tool annotations to be untrusted unless they come from trusted servers."
  • No sessions. Because the 2026 spec is stateless, a tool that needs state across calls (a shopping cart, a browser tab) should return an explicit handle and take it as an argument later. The spec suggests unguessable IDs and checking authorization on every call.
  • Human in the loop. The spec says there "SHOULD always be a human in the loop with the ability to deny tool invocations." Our case against unattended agents explains why that matters.

Use a tool when the model should decide on its own that it needs to act or look something up.

Resources: context the app attaches

A resource is a piece of readable content identified by a URI (file:///, https://, git:// or your own scheme like docs://). The host lists resources and decides which to include, typically through a picker or an @ mention. The example server's resources/read result:

JSON
{
  "contents": [
    {
      "uri": "docs://style-guide",
      "mimeType": "text/markdown",
      "text": "# House style (short version)\n- Lead with the point. ..."
    }
  ],
  "resultType": "complete",
  "ttlMs": 0,
  "cacheScope": "private"
}

Things to know:

  • Templates (resources/templates/list) expose parameterized URIs such as file:///{path}, with optional autocompletion.
  • Subscriptions changed in 2026: resources/subscribe is gone, replaced by a single subscriptions/listen stream that the client opens for the change notifications it wants.
  • Not found now returns JSON-RPC error -32602 (it was -32002 before). The example server returned exactly that for docs://nope.
  • Tools can link to resources. A tool result can include a resource_link or an embedded resource, which is how tools and resources work together.

Use a resource when the content is something a person or the app should choose to put in front of the model, and reading it has no side effects.

Prompts: templates the user picks

A prompt is a named, parameterized message template. The user selects it, usually as a slash command, fills in arguments, and the host inserts the resulting messages into the conversation. Its prompts/list result:

JSON
{
  "prompts": [
    {
      "name": "review_draft",
      "title": "Review a draft against the style guide",
      "description": "Ask the model to review a draft using the house style guide",
      "arguments": [{ "name": "draft", "description": "The draft text to review", "required": true }]
    }
  ],
  "resultType": "complete",
  "ttlMs": 0,
  "cacheScope": "private"
}

prompts/get returns messages, each with a role and content that can be text, images, audio, a resource link or an embedded resource. Our review_draft prompt returns two user messages: the embedded style-guide resource, then the instruction with the draft. That's a common pattern: prompts bundle resources with instructions so the user gets a one-command workflow.

Use a prompt when you have a repeatable workflow that a human should trigger deliberately, such as "review this PR" or "draft release notes."

The client-side features: elicitation, sampling, roots

These run in the other direction. The server asks the client for something.

Elicitation lets a server ask the user for input mid-request. In the 2026 spec it uses the new multi round-trip request (MRTR) pattern: instead of the server sending a request to the client, the server returns an input_required result, and the client retries the original call with the answers.

JSON
{
  "resultType": "input_required",
  "inputRequests": {
    "github_login": {
      "method": "elicitation/create",
      "params": {
        "mode": "form",
        "message": "Please provide your GitHub username",
        "requestedSchema": {
          "type": "object",
          "properties": { "name": { "type": "string" } },
          "required": ["name"]
        }
      }
    }
  },
  "requestState": "eyJsb2NhdGlvbiI6Ik5ldyBZb3JrIn0..."
}

The client retries tools/call with a new request ID, inputResponses (for example {"github_login": {"action": "accept", "content": {"name": "octocat"}}}) and the exact requestState it was given. That example comes from the spec. Two rules worth remembering: form mode must never ask for passwords, API keys or tokens, and servers must use URL mode (send the user to a web page) for those. Servers must also treat requestState as attacker-controlled and integrity-protect it if it affects authorization.

Sampling lets a server ask the client's model to generate text. Roots let a client tell a server which directories it may use. Both, along with server logging, are deprecated as of 2026-07-28 (SEP-2577). They still work during the deprecation window, which runs at least until the first spec revision released on or after July 28, 2027. The spec's suggested replacements: call an LLM provider API directly instead of sampling; pass directories through tool parameters, resource URIs or config instead of roots; log to stderr or OpenTelemetry instead of protocol logging.

Tasks: now an extension

Long-running work used to be an experimental core feature. In 2026-07-28 it moved into the official io.modelcontextprotocol/tasks extension. If both sides opt in, a server can answer a tools/call with a task handle (resultType: "task"), and the client polls tasks/get, answers mid-flight questions via tasks/update, and can request tasks/cancel. Statuses are working, input_required, completed, failed and cancelled. Use it for CI runs, batch jobs and approval gates. Don't assume a host supports it; the project's docs say support "varies by client."

Client support differs

Last verified: October 9, 2026

HostToolsResourcesPromptsElicitationNotes
Claude CodeYes@server:protocol://path mentionsSlash commands, listed as /server:prompt (MCP)Yes (form and URL on 2026-07-28 connections)Answers roots/list with working directories
VS Code (GitHub Copilot)YesAdd Context › MCP Resources/<server>.<prompt>Not stated in docsMCP Apps supported
CursorYesYesYesYesAlso lists roots and MCP Apps
Gemini CLIYes@ referencesSlash commandsNot stated in docs

Two practical consequences. First, don't hide important context behind resources alone: if a host doesn't surface them well, nobody will see them. Many servers expose a read-only tool as well. Second, prompts are a UX feature; they only help if your target host shows them.

How to choose

  • Does the model need to decide when to do it? Tool.
  • Is it read-only context someone should pick? Resource (optionally also a read-only tool).
  • Is it a workflow a person kicks off on purpose? Prompt.
  • Does the server need a user's answer mid-call? Elicitation, via an input_required result, never for secrets in form mode.
  • Does it take minutes? Tasks extension, if your hosts support it.
  • Thinking about sampling or roots? They're deprecated. Design around them.

Read next

Comments (0)

More on Model Context Protocol (MCP)

View all
Developers · NewsMLflow RCE CVE-2026-116692 (CVSS 9.8), Google's MCP Toolbox file-exfiltration flaw and a critical LiteLLM advisory: which versions fix themThe MLflow RCE CVE, CVE-2026-116692, is a CVSS 9.8 flaw in third-party scorer loading, fixed in MLflow 3.17.0. It was published on October 27, 2026, a day after Google's CVE-2026-93328 for MCP Toolbox for Databases and alongside a CVSS 10.0 LiteLLM Enterprise advisory. Affected and fixed versions for all three.·7 min read
Agents · NewsAgentic AI Foundation adds Agent Skills, Open Responses, OpenEnv and OpenSandbox: what each project is and who donated itThe Agentic AI Foundation took in four new projects on October 22, 2026: Anthropic's Agent Skills, OpenAI's Open Responses, Hugging Face's OpenEnv and Alibaba's OpenSandbox. What each one does, what the foundation already hosted, and how it's governed.·4 min read
Developers · NewsOpenCode 2.0: one background service for terminal, web and desktop, plus background subagents. What it changes for securityOpenCode 2.0 runs the open-source coding agent as a single shared background service that its terminal, web and desktop clients all connect to, and adds background subagents and async shell commands. Hacker News commenters say the new model broke their sandboxes; OpenCode's docs say shell commands run with your full user authority.·6 min read
Agents · NewsGoogle's Gemini agent: what the "universal agent for work" does, who gets it and what it costsGoogle announced the Gemini agent at Gemini at Work on October 8, 2026. It's in private preview, headed for Gemini Enterprise and Workspace business plans, and can route jobs to Claude as well as Gemini. Here's what is confirmed and what Google hasn't said.·5 min read
The Week in AI · every Friday

New guides and explainers, every Friday.

The week’s biggest stories in AI and tech, what they mean, and what to read next.

Free. One email a week. Unsubscribe in one click.