Tools vs Resources vs Prompts: the MCP primitive you're misusing | Prasham H Trivedi<br>Tools vs Resources vs Prompts: the MCP primitive you're misusing<br>Part 2 of 2 · MCP in production<br>Jul 13, 2026<br>14 min read<br>Series: MCP in production<br>Part 1 - MCP transports, decided<br>Part 2 - Tools vs Resources vs Prompts: the MCP primitive you're misusing (you are here)
Table of contentsThe whole post in one picture: who pulls the trigger. The user fires a Prompt (blue), the app pulls a Resource (green), the model reaches for a Tool (purple).<br>There are three MCP primitives, mainly, and almost everyone ships just one of them. Tools, and then more Tools, until every read and every write is just a button the model can choose to press. Then you hit the wall that everyone hits and blame the protocol for it: context eaten before any real work starts, the model reaching for things it shouldn’t, or skipping a read it plainly needed.<br>The other two primitives are the fix, and I would rather show you that from a server you can open and read for yourself than from a story you have to take on faith.<br>So a word on what Pustak is, since it is going to be the running example. I build a lot of interactive HTML explainers these days, an idea I cribbed straight from Thariq Shihipar’s unreasonable effectiveness of HTML: when you want to explain something, a self-contained HTML page with a diagram or a widget the reader can poke will usually beat a wall of markdown.<br>Pustak is the tool I built to live inside that loop, and it uploads, stores, reads, and shares those explainers for me. To do all that it wears two faces. One is a web app, where a human logs in to see and manage their pages. The other is an MCP server, so the agent that generates and updates the explainers can work with them directly.<br>It is also one of the very few servers I can talk about in public that has a legitimate use for all three MCP primitives at once: a Resource, a Tool, and a Prompt I really did call explainer. The source is open now, and the interactive explainers scattered through this post came out of that same workflow.<br>That is also what makes it such a clean example to argue from. Pustak exposes the very same page read two ways on purpose, as a read_page Tool and as a pustak://page/{path} Resource, while the writes stay Tools. Set those two reads side by side and the whole argument falls out of them, because one hands the model a decision and the other never asks it to make one.<br>MCP primitives sorted by who pulls the trigger<br>Forget “read versus write,” and forget “what does the call actually do.” The question that sorts MCP’s three primitives is narrower, and a little meaner, than either of those: who pulls the trigger on this call?<br>Tools are model-controlled. The model decides, mid-reasoning, to call one, and that is the entire point of a Tool. You hand the model an action and you trust it to pick the moment. In Pustak, write_page and delete_page are Tools, and rightly so, because whether to write or delete a page is a real judgement call and the model is the thing that should be making it.<br>Resources are app-controlled. The client pulls them by URI, the way a browser issues a GET, and the model no more “decides” to read a Resource than a browser decides to fetch a stylesheet. Pustak registers pustak://pages for the catalogue and pustak://page/{path} for a single page’s content, and the host wires those in before the model ever starts to reason.<br>Prompts are user-controlled. They are slash-command shaped, and a human is the one who fires them. Pustak exposes an explainer prompt for exactly that shape: a workflow a person kicks off deliberately, not one the agent stumbles into on its own.1<br>The MCP spec says all of this in plain language: Tools are model-controlled, Resources are application-controlled, Prompts are user-controlled. None of it is my reinterpretation; it is written down. The catch is that the SDKs let you ignore it completely, and registering everything as a Tool compiles fine, ships fine, and demos well enough that you never notice what you gave away.<br>One honest nuance before we go on. The protocol does have a resource-read request path, so it is not literally true that a model can never trigger a read. But the design intent, and the default flow, is the client pulling by URI, so when I say the model doesn’t decide, I mean that a Resource takes the decision out of its path by construction. That is the lens I want you holding for the part that actually costs you something.<br>Play it: pick a call, then pick its primitive. Box it wrong and watch the failure play out. That is the whole argument, in the small.The misuse that bites: read-only data as a Tool<br>Here is the misuse in its smallest form. You hand the agent a get_customer(id) tool: no side effect, just a read. It looks completely harmless, and that is exactly the problem. By making it a Tool, you have quietly turned whether to read the...