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.

By ShajanthanUpdated 6 min read
ByShajanthanFounder & Editor
Published
Reading6 MIN
Three columns comparing MCP tools, resources and prompts by who controls them
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.
Contents

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.

About this storyBased on the sources linked below. Editorial standards

Was this useful?Report an error
Comments
0

More on Model Context Protocol (MCP) & developer tools

The Week in AI

New guides and explainers, every Friday.

0