MCP Resources vs Tools vs Prompts: Stop Putting Everything in a Tool
Most first MCP servers export one giant run_query tool and call it a day. It works in a demo and then the model starts guessing table names, stuffing 40k tokens of JSON into the next turn, and skipping the resource that already had the schema. The protocol gives you three primitives — resources, tools, and prompts — because they hit the model at different times with different contracts. Mixing them up is how you get an "agent" that is just a confused RPC client.
What each primitive is for
Tools are actions: they have a JSON schema, the model chooses to call them, and they return a result into the conversation. Side effects belong here — create a record, send a mail, run a search. So do reads that are parameterized by a decision the model must make ("fetch case 123 because the user named it").
Resources are fetchable content with a URI. The client (or the user, or the host) can attach them; some hosts let the model request them. They are for data that should be in context as documents, not as the return value of an improvised query: a schema catalog, a runbook, yesterday's incident notes, a Dataverse table definition. Resources can be listed and read without inventing arguments. They cache. They have MIME types.
Prompts are named templates the host can offer as slash-commands or menu items: "explain this plugin trace," "draft a dual-write runbook." They are not model-invented. They encode a workflow you already know is useful, with slots for resources and arguments. Think of them as saved plays, not as tools with a prettier name.
If you only remember one split: tools change the world or answer a question the model posed; resources are the library; prompts are the playbook.
A Dataverse example that stays honest
A bad server: one tool dataverse_fetchxml with a free-string fetchXml argument. The model will write invalid FetchXML, retry, and burn budget.
A better split:
- Resource
dataverse://org/tables/{logicalName}— entity metadata, attributes, relationships. Small, cacheable, meant to be attached before the model writes a query. - Resource
dataverse://org/tables— list of logical names the agent is allowed to see (already permission-filtered). - Tool
dataverse_retrieve—logicalName,id, optionalcolumns. Tight schema, no FetchXML in the common path. - Tool
dataverse_query— a constrained filter object (field, op, value), not a raw query language, plus a hard row cap. - Prompt
debug-plugin-trace— slots for a trace resource URI and a table name, with instructions that say "read metadata first, then retrieve, never guess attribute names."
The model still acts. It just cannot skip the catalog or open a firehose.
Size and freshness
Tool results land in the transcript and live there until compaction. A 200-row Dataverse dump in a tool result is a tax on every later turn. Resources can be re-read, paginated, or omitted from the next request. Prefer a resource for anything over a couple of kilobytes that the user might want to keep pinning; prefer a tool for a short, one-off answer.
Stale data: a tool is always "now." A resource needs a cache header or an etag in the server so the host can refresh. If you expose "current inventory" as a resource with a 10-minute cache, say so in the description or you will debug "the agent hallucinated stock" that was just a cached resource.
Permissions sit on different edges
Tool calls should go through the same authorization as your API — user token, never a god-mode service account "because it's an agent." Resource URIs are capabilities: if you can mint dataverse://org/tables/incident, you have granted read on incidents. Do not put secrets in resource bodies. Do not make a tool that accepts a raw SQL string and a connection name; that is a confused deputy with extra steps.
Prompts should not silently escalate. A prompt that includes "you may call delete_record" is an instruction, not an allowlist. The allowlist is the tool set the host enabled.
Rule of thumb
- Catalog, schema, docs, blobs, traces → resource
- Search, get-by-id, create, update, delete, "run this job" → tool
- Recurring specialist workflow with a known opening move → prompt
- If you are about to add a tool argument called
modewith seven enums, you probably wanted two tools and a prompt
MCP is not a second REST. It is a way to hang a library, a set of hands, and a few rehearsed scripts on a model that is otherwise guessing. Use all three, or the guesswork comes back.
Keep reading
Building Your First MCP Server in Python: A Hands-On Walkthrough
From zero to a working Model Context Protocol server in about 100 lines — tools, resources, a local client test, and the traps that will bite you on day one.
MCP Went Stateless: What the 2026-07-28 Spec Changes for Your Server
The Model Context Protocol 2026-07-28 release removes sessions and the initialize handshake, adds routing headers and cacheable lists, moves Tasks to an extension, and deprecates Dynamic Client Registration. What to change in an MCP server.
GitHub Copilot AI Model Comparison: Which Model to Use for Each Task
AI model comparison for GitHub Copilot Chat: GPT-5.6, Claude, Gemini, Grok, and Kimi, plus when Auto is the cheaper default.
MCP Tool Descriptions That Route the Model to the Right Call
How to write MCP tool names, descriptions, and schemas so the model picks one tool on purpose, and what to do when two tools sound the same.
An MCP Server for Dynamics 365 Finance and Operations: Natural-Language Access to ERP
How to expose Dynamics 365 Finance and Operations to Claude through MCP — the architecture, the data entity choices, the auth model, and the operations that should never be one prompt away.
Claude Code Hooks: Automating Repo Guardrails Without Pre-Commit Fatigue
Use Claude Code hooks to enforce policy at the moment actions happen — before edits, before tool calls, on session start — without relying on pre-commit hooks everyone learns to bypass.
Newsletter
New posts, straight to your inbox
One email per post. No spam, no tracking pixels, unsubscribe anytime.
Comments
- No comments yet. Be the first.