MCP.so
Sign In
Back to blog

MCP Resources vs Tools (vs Prompts): When to Use Each

Oct 15, 2026MCP.so TeamMCP.so Team

MCP resources vs tools explained: who controls each primitive, when to use resources, tools or prompts, TypeScript and Python examples, and real client support.

In MCP, tools are functions the model decides to call (model-controlled), while resources are read-only data identified by a URI that the client application decides to attach as context (application-controlled). Prompts, the third primitive, are templates the user explicitly picks, usually as slash commands (user-controlled). The quick rule is to use a tool when the model must act or compute, a resource when the host should be able to load data as context, and a prompt when a human starts a known workflow.

That one-line answer hides the decision that matters in practice: who should be in charge of pulling the data in, and does the client you target even support it? This guide covers both, with code for the current TypeScript and Python SDKs (both v2, implementing the 2026-07-28 spec).

The control model: who decides?

The MCP server spec sorts the three server primitives by who controls them:

PrimitiveControlTypical UIExample
PromptsUser-controlledSlash commands, menu items/review-code, "Summarize this PR"
ResourcesApplication-controlled@-mentions, attach/context pickers, automatic inclusionFile contents, DB schema, git history
ToolsModel-controlledInvoked by the LLM, ideally with user approvalCreate issue, run query, send email

"Control" means who decides when the primitive is used, not who writes it. The server authors all three. The spec also says these are design intents: clients may surface any primitive however they like. In practice, that flexibility is why client support varies so much (more below).

Tools: actions and computation

A tool has a name, a description, an inputSchema (JSON Schema) and optionally an outputSchema. The client lists tools with tools/list and the model asks to invoke one through tools/call. Tools are the right choice when:

  • The operation has side effects: write a file, open a PR, post a message, charge a card.
  • The result depends on arguments the model chooses at runtime: search queries, filters, date ranges.
  • The work is computation, not lookup: run SQL, render a chart, call an external API.
  • You need the model to decide on its own that it needs something mid-task.

Tool results can carry text, images, audio, structured JSON (structuredContent) and, notably, resources: either a resource_link (a URI the client can fetch) or an embedded resource with its contents inline. That is the bridge between the two primitives.

TypeScript (@modelcontextprotocol/server v2):

import { McpServer } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';

const server = new McpServer({ name: 'catalog', version: '1.0.0' });

server.registerTool(
  'search_products',
  {
    description: 'Search the product catalog by name',
    inputSchema: z.object({
      query: z.string().describe('Substring to match against product names'),
      limit: z.number().int().max(50).optional(),
    }),
  },
  async ({ query, limit }) => {
    const hits = await db.searchProducts(query, limit ?? 10);
    return {
      content: [{ type: 'text', text: hits.map((h) => h.name).join('\n') }],
    };
  }
);

Python (mcp v2, MCPServer):

from mcp.server import MCPServer

mcp = MCPServer("catalog")


@mcp.tool()
def search_products(query: str, limit: int = 10) -> list[str]:
    """Search the product catalog by name."""
    return [p.name for p in db.search_products(query, limit)]

In Python the type hints are the schema and the docstring is the description the model sees.

Resources: data the application attaches

A resource is read-only content with a URI, a name and a MIME type. Clients discover them with resources/list and fetch them with resources/read. Resources fit when:

  • The data is context, not an action: a config file, a schema, documentation, a report.
  • A human or the host app should choose what goes into context (the "attach file" pattern).
  • The same content is reused across many turns or many conversations and can be cached.
  • You want the content addressable, so a tool result or a prompt can point at it by URI.

TypeScript, a static resource:

server.registerResource(
  'db-schema',
  'schema://main',
  {
    title: 'Database schema',
    description: 'DDL for the main database',
    mimeType: 'text/plain',
  },
  async (uri) => ({
    contents: [{ uri: uri.href, text: await db.dumpSchema() }],
  })
);

Python:

@mcp.resource("schema://main", mime_type="text/plain")
def db_schema() -> str:
    """DDL for the main database."""
    return db.dump_schema()

The read callback returns a contents array. Each item echoes the URI and carries either text or a base64 blob, so binary files such as PNGs and PDFs work too.

Resource templates

Most real data is parameterized. Resource templates expose a whole URI pattern using RFC 6570 URI templates, and clients discover them via resources/templates/list. Arguments can be auto-completed through the completion API.

import { ResourceTemplate } from '@modelcontextprotocol/server';

server.registerResource(
  'user-profile',
  new ResourceTemplate('users://{userId}/profile', { list: undefined }),
  { title: 'User profile', mimeType: 'application/json' },
  async (uri, { userId }) => ({
    contents: [
      {
        uri: uri.href,
        mimeType: 'application/json',
        text: JSON.stringify(await getUser(userId)),
      },
    ],
  })
);
@mcp.resource("users://{user_id}/profile")
def user_profile(user_id: str) -> str:
    """Profile data for one user."""
    return json.dumps(get_user(user_id))

In the TypeScript SDK list is a required key. Pass undefined when the instances can't be enumerated, or a callback returning concrete URIs if you want them to show up in resources/list.

Subscriptions and change notifications

Resources are the only primitive with per-item change tracking, and this changed in the latest spec:

  • notifications/resources/list_changed tells clients the set of resources changed (declare listChanged: true). The TS SDK sends it automatically when you register or remove a resource.
  • Per-resource updates (declare subscribe: true) tell a client that one URI's content changed via notifications/resources/updated. On 2025-era connections the client opts in with resources/subscribe. On 2026-07-28 connections, that method is gone: clients pass resource URIs in the resourceSubscriptions filter of a subscriptions/listen request, and updates arrive on that stream.

If you support both eras, the TypeScript SDK docs show the bookkeeping. Note that subscriptions need a connection that outlives the request, so they won't deliver anything behind a stateless legacy HTTP handler.

Prompts: workflows the user starts

A prompt is a named, argument-taking template that returns a list of messages (prompts/list, prompts/get). Use one when a human should trigger a repeatable workflow, such as "review this diff against our style guide" or "draft a release note for version X". Prompt messages can embed resources, so a prompt can pull the style guide in by URI.

server.registerPrompt(
  'review-code',
  {
    title: 'Code review',
    description: 'Review code against our conventions',
    argsSchema: z.object({ code: z.string().describe('The code to review') }),
  },
  ({ code }) => ({
    messages: [
      {
        role: 'user',
        content: { type: 'text', text: `Review this code:\n\n${code}` },
      },
    ],
  })
);
@mcp.prompt(title="Code review")
def review_code(code: str) -> str:
    """Review code against our conventions."""
    return f"Review this code:\n\n{code}"

Resources vs tools vs prompts: comparison

ToolsResourcesPrompts
Controlled byModelApplication / hostUser
Identified byName + input schemaURI (or URI template)Name + arguments
Side effectsAllowed (should be confirmed)None, read-onlyNone (returns messages)
Discoverytools/listresources/list, resources/templates/listprompts/list
Usetools/callresources/readprompts/get
Change trackinglist_changedlist_changed + per-URI updateslist_changed
Can reference resourcesYes (resource_link, embedded)n/aYes (embedded, links)
Best forActions, queries with runtime argsContext, documents, schemasRepeatable human-started workflows

Client support: the part tutorials skip

The spec is only half the decision. A beautifully designed resource is invisible in a client that ignores resources. As of October 2026:

ClientToolsResourcesPrompts
Claude CodeYesYes: @server:protocol://path mentions, plus built-in tools to list and read resourcesYes: /mcp__server__prompt slash commands
CursorYesYesYes
VS Code (Copilot)YesYes: Add Context → MCP Resources, MCP: Browse ResourcesYes: slash commands in chat
Claude API MCP connectorYesNo (use the SDK's client-side helpers)No (same)
OpenAI API / ChatGPTYes (centered on tools such as search and fetch)UI resources for apps onlyNot documented

Two practical consequences follow:

  1. If the model must find the data on its own, ship a tool. Even in clients that support resources, most surface them to the user (via a picker or @-mention) rather than letting the model browse freely. Claude Code is an exception because it gives the model list/read helpers.
  2. Dual-expose important data. A common pattern is a read_doc or get_schema tool that returns a resource_link or embedded resource for the same URI you register as a resource. Tool-only clients still work, and resource-aware clients get caching, pickers and subscriptions.

For more on how clients select tools and why huge tool lists hurt, see MCP tool discovery. If you're weighing resources against a retrieval pipeline, MCP vs RAG covers that trade-off.

Decision checklist

  • Does it change anything outside the server? → Tool.
  • Does the model need to choose arguments at runtime (a query, an ID it discovered mid-task)? → Tool, optionally returning a resource_link.
  • Is it stable context a user would "attach" (docs, schema, a file)? → Resource, and a template if it's parameterized.
  • Do clients need to know when it changes? → Resource with subscribe / listChanged.
  • Is it a workflow a person kicks off with a few inputs? → Prompt, embedding resources as needed.
  • Are your target clients tool-only (API connectors, many agent frameworks)? → Expose the data through a tool too.

Common mistakes

  • Read-only data only as tools. It works everywhere, but you lose caching and user-driven attachment, and every lookup spends a model turn. Add a resource as well.
  • Writes hidden in resources. resources/read must not have side effects, because clients may prefetch or re-read freely.
  • Unvalidated template variables. A template like file:///{path} must resolve and confine paths. The spec requires servers to validate URIs and sanitize paths against directory traversal.
  • Returning an empty contents array for a missing resource. Under 2026-07-28, return a JSON-RPC -32602 error instead (clients should still accept the older -32002).
  • Prompts that assume a client. Not every client renders prompts, so keep the underlying capability reachable via a tool or resource.

Test all three primitives side by side in MCP Inspector. It has separate tabs for tools, resources (including templates) and prompts, so it's the fastest way to see what a client will actually receive.

Next steps

Look at how existing servers split their surface. For example, compare the Filesystem, GitHub and Postgres servers in Inspector to see which data they expose as tools versus resources. Then browse the MCP.so server directory for servers in your stack, and check Claude Code MCP setup to try @ resources and / prompts in a client that supports all three.

Frequently asked questions

What is the difference between MCP resources and tools?

Tools are functions the model decides to call and may have side effects, while resources are read-only data identified by a URI that the client application chooses to load as context. In short, tools are model-controlled and resources are application-controlled.

When should I use an MCP resource instead of a tool?

Use a resource for stable, read-only context such as files, schemas or documentation that a user or host app should be able to attach and cache. Use a tool when the model must pick arguments at runtime, compute something or change state.

What are MCP prompts used for?

Prompts are user-controlled templates that return a list of messages, usually shown as slash commands. They package repeatable workflows such as a code review and can embed resources for context.

Do all MCP clients support resources and prompts?

No. Claude Code, Cursor and VS Code support tools, resources and prompts, but API-level integrations such as the Claude API MCP connector only use tools. If the model must reach the data in every client, expose it through a tool as well.