People mix these three up because they all show up in the same Inspector tabs and they all look like "things a server exposes." The protocol does not treat them as flavors of the same object. The split is who is allowed to decide.

  • Tools are model-controlled. The language model picks a name from tools/list and the client sends tools/call.
  • Prompts are user-controlled. The human picks one from a menu (slash command, button, "Insert prompt"). The client sends prompts/get. The model does not choose the template.
  • Resources are application-driven. The host decides what context to load. The client sends resources/read for a URI. The model does not browse your filesystem unless the host put that URI in front of it.

That three-way split is in the 2026-07-28 spec, not a blog convention. Tools say "the language model can discover and invoke tools automatically." Prompts say they are "exposed from servers to clients with the intention of the user being able to explicitly select them." Resources say "host applications determining how to incorporate context."

This page is the practical version of that, with wire methods, a small Python SDK sketch, and DialMCP as the first-party phone-call example. It is not a reprint of the spec. For the JSON-RPC methods themselves (tools/list vs tools/call), see MCP server commands. For a one-file server, see the MCP server tutorial.

Checked against the 2026-07-28 tools, prompts, and resources spec pages and the Python SDK docs on 16 September 2026.

The one-line test

Open wooden library card-catalog drawer labeled 5184
Photo: eflon / Flickr (CC BY 2.0).

Ask "who clicks?"

If the model can fire it without the user picking a template, it is a tool. If the user has to choose it from a list the server advertised, it is a prompt. If the host (or the user, through the host's file picker) loads a URI as context, it is a resource.

A recording URL inside a tool result is still a tool result. It does not secretly become a resource just because it is a URI. The spec is explicit: a tool MAY return a resource_link, and that URI is not guaranteed to appear in resources/list.

Tools: the model acts

A tool is a named function plus a JSON Schema. The client discovers the catalog with tools/list and invokes with tools/call. Servers that support tools declare a tools capability; listChanged means they will notify listeners when the catalog changes.

Names are case-sensitive, 1 to 128 characters, and should stick to letters, digits, underscore, hyphen, and dot. No spaces. Uniqueness is per server. Two servers that both expose search are the client's problem to prefix, not the protocol's.

The result has two error channels, and mixing them is the usual client bug:

  1. Protocol errors are JSON-RPC error objects. Unknown tool, malformed request, server failure. Example: -32602 with Unknown tool.
  2. Tool execution errors are successful JSON-RPC responses with isError: true in the result. Bad arguments, a rejected objective, a number the server will not dial. Hosts should feed these back to the model so it can retry.

A finished result uses resultType: "complete" on 2026-07-28. It carries a content array (usually type: "text"). If the tool advertised an outputSchema, it SHOULD also return structuredContent that matches, plus the same JSON serialized in a text block for older clients.

Tool annotations (read-only, destructive, idempotent, open-world) are hints. The spec says clients MUST treat them as untrusted unless they come from a trusted server. Do not build your security model on a client honoring a read-only hint.

Stateful tools have no session handle

MCP has no protocol-level session for tool state. If you need a cart, a browser tab, or a phone call that is still ringing, you return an explicit handle as an ordinary string and accept it as an argument on the next call. The tools spec's "Stateful Tools" section is non-normative and that is the whole design.

DialMCP is that pattern. place_call returns a call ID immediately. The call keeps going in the background. The agent polls get_call with that ID until a terminal status, then reads the structured resolution, transcript, and recording link. end_call hangs up early. list_calls finds an older ID. There is no hidden "current call" on the connection.

That is also why DialMCP does not stream the phone call as a long tools/call SSE body. One POST returns the ID. Later POSTs ask about it. Same as a shopping-cart basket_id.

What DialMCP actually ships

Four tools, after OAuth and SMS verification. Calls are US and Canada, destination-local hours, from the user's SMS-verified number. Guardrails (AI disclosure, recording, rate limits, blocked ranges) are enforced on the hosted server. They are not extra JSON-RPC methods.

DialMCP does not document prompts. Phone calls go through tools. A recording link in a get_call result is a URI in the tool payload, not a resources/list entry. If you are connecting rather than implementing, the tool reference is the page you want.

Prompts: the user picks

A prompt is a named template the user selects. The client lists them with prompts/list and renders one with prompts/get, passing arguments. The server returns a messages array (user and/or assistant turns) that the host drops into the conversation as if the user had typed them.

That last part is the whole point. Tools execute. Prompts fill the chat.

Prompt arguments are a flat list of named string values. There is no inputSchema. The Python SDK is blunt about it: a form a person fills in, not a payload a model constructs. Required is a flag on each argument. A missing required argument fails the request itself. There is no isError tool-result for the model to recover from, because no model is in the loop.

On the spec, missing required arguments SHOULD be -32602 (Invalid params). The Python SDK, checked 16 September 2026, raises MCPError and surfaces -32603 (Internal server error) for that case. If you are writing a client, handle both. If you are writing a server in the SDK, do not document -32602 as what your process will actually return until you have watched Inspector do it.

Servers that support prompts declare a prompts capability. listChanged means they will notify. The set MAY be empty. It MAY change over time. It MUST NOT vary per-connection as a side effect of other requests, but it MAY vary by the authorization on the request (scopes). Same rule as tools and resources.

Clients typically surface prompts as slash commands or an insert-prompt picker. The protocol does not mandate a UI. A headless client can skip prompts entirely and still be a valid MCP client.

A prompt can return more than one message, including an assistant turn that steers the next reply. It can embed a resource or attach image/audio content. That is still a prompt. Embedding a style guide inside prompts/get does not make the style guide a tool.

Resources: the host loads context

A resource is data addressed by URI. The client lists with resources/list and reads with resources/read. Templates (file:///{path}, users://{user_id}/profile) live under resources/templates/list. The function runs on read, not on list. Listing a thousand URIs is cheap; you pay for the ones somebody opens.

The spec's control model is application-driven. Hosts can show a tree, let the user search, or auto-include based on heuristics. The model does not get a resources/call. If you want the model to fetch something on its own, that is a tool that happens to return text.

Capabilities: listChanged for catalog changes, subscribe for per-resource update notifications on a subscriptions/listen stream. Either, both, or neither.

URI schemes the spec names: https:// (only when the client can fetch the web itself), file:// (filesystem-like, not necessarily a real disk), git://, plus custom schemes that follow RFC 3986. DialMCP does not publish a resource catalog. Do not invent call:// URIs for us.

A missing resource is a JSON-RPC error, code -32602. Older revisions used -32002; clients SHOULD still accept that. Servers MUST NOT return an empty contents array for a URI that does not exist. Empty could mean "exists but blank" or "never heard of it."

Python: three decorators, three audiences

Official Python SDK, MCPServer from mcp.server (v2). Type hints are the contract. This is a sketch, not a runnable product.

from mcp.server import MCPServer

mcp = MCPServer("notes")

@mcp.tool()
def add_note(title: str, body: str) -> str:
    """Save a note. The model calls this."""
    return f"saved {title!r}"

@mcp.prompt()
def review_note(title: str) -> str:
    """Ask for a review of a note. The user picks this."""
    return f"Please review the note titled {title}."

@mcp.resource("notes://inbox")
def inbox() -> str:
    """The current inbox. The host reads this."""
    return "3 unread"

What the SDK infers:

  • Tools: function name, docstring, JSON Schema from type hints. Defaults make arguments optional. Annotated[..., Field(...)] adds descriptions and constraints. Bad inputs can be rejected before your function runs, as a tool error the model can read.
  • Prompts: same name and docstring, but arguments stay a flat string list. Return a str and it becomes one user message. Return a list of UserMessage / AssistantMessage to seed a conversation.
  • Resources: the URI is the address. The function name is only the name field. A {placeholder} in the URI makes it a template and moves it to resources/templates/list. Placeholder names must match parameter names or the decorator fails at import.

Inspector (uv run mcp dev server.py) is the fastest way to see the split. Tools tab: the model-shaped form with types. Prompts tab: a string form. Resources tab: click a URI, content appears. Same binary as MCP Inspector.

If you are still on FastMCP import paths from 2025 tutorials, check the current SDK. The v2 docs use from mcp.server import MCPServer.

How they combine without turning into sludge

A good server is boringly specific.

Use a tool when something should happen: write a row, place a call, open a PR. Side effects belong here, with a human in the loop for anything destructive.

Use a prompt when you want a consistent first message: "review this diff," "draft the commit," "explain this error." The user opts in. You are not hoping the model remembers your house style.

Use a resource when the host should be able to pin context: a schema, a style guide, yesterday's transcript. Read-only from the model's point of view. If the model needs to mutate it, that is a tool.

Crossing the streams is how servers get confusing:

  • A "prompt" that hits the network and books a meeting is a tool wearing a costume.
  • A "tool" named get_style_guide that only returns a markdown file is a resource you made the model poll for.
  • A resource URI that triggers a phone call when read is a side effect hiding in resources/read. Don't.

DialMCP stays on the first line: four tools, async handle, no prompt menu, no resource tree. That is a product choice, not a protocol requirement. A notes server might ship all three. A filesystem server might be resources-heavy with a couple of write tools.

Common mix-ups

place_call is not a method. You never POST method: "place_call". You POST method: "tools/call" with params.name set to place_call. Same for every tool on every server. The commands page exists because search results smash JSON-RPC methods, tool names, and shell launchers into one query.

prompts/get is not tools/call. Rendering a prompt does not run your business logic, except the template function itself. If your @mcp.prompt() handler places a phone call, you have designed it wrong.

A resource link in a tool result is not resources/list. The recording from get_call can be a URI the client fetches or shows. It does not have to be advertised as a resource. The tools spec says resource links returned by tools are not guaranteed to appear in resources/list.

Capabilities are per primitive. Declaring tools does not imply prompts or resources. An empty list is legal. A tools-only server is valid MCP. DialMCP documents four tools and no prompt catalog.

2026-07-28 dropped initialize on modern HTTP. Each request carries version and client info in _meta. Dual-era servers may still answer initialize for older clients. A missing handshake is not a dead server if the client is on the current revision. Details in the tutorial and remote MCP servers.

What to build first

If you are writing a server: start with one tool that does the thing, with a tight inputSchema and an honest description. Add a prompt only when users keep typing the same preamble. Add a resource only when the host needs to pin a document without spending a tool call.

If you are connecting a client: look at tools/list first. That is what the model will see. Prompts and resources are extra UI the host may or may not show. For DialMCP, connect the remote URL, finish OAuth, then ask the agent to place a call and approve the tool. Recipe: install and client setup.

Further reading

Need the model to place a real phone call? DialMCP is a hosted remote MCP server with four tools, no prompt pack, at https://mcp.dialmcp.com/mcp. Connect, finish OAuth, approve place_call.

DialMCP tool reference

FAQ

What is the difference between MCP tools, prompts, and resources?

Tools are model-controlled functions (tools/call) with a JSON Schema. Prompts are user-selected message templates (prompts/get) with a flat argument list. Resources are URI-addressed data the host reads (resources/read). Spec 2026-07-28.

Are MCP prompts the same as a system prompt?

No. A system prompt is host configuration. An MCP prompt is a server-defined template the user picks at runtime. The server authors the text; the user decides when it is inserted.

Do I need all three primitives?

No. Declare the capabilities you implement. A tools-only server is normal. DialMCP is tools-only on purpose.

Can a tool return a resource?

It can return a resource_link or an embedded resource in the tool result. That URI does not have to show up in resources/list.

How do I list MCP prompts from a client?

Send prompts/list. If the server omitted the prompts capability, do not expect a catalog. Inspector's Prompts tab is the same request with a form on top.

Where does DialMCP fit?

It is a hosted remote MCP server for outbound phone calls. Four tools, Streamable HTTP at https://mcp.dialmcp.com/mcp, OAuth, no API key. Not a prompt pack and not a resource browser.