MCP Resources and Prompts: The Primitives Your Policy Engine Never Learned to Gate
9 min read Fullmakt Team
- mcp
- governance
- data-ownership
- authentication
- traceability
- agents
Ask a team how their AI agent’s MCP access is governed and you’ll usually
hear about tools: which ones are allowed, which arguments are scoped, which
calls need approval. That answer is true and it is also incomplete, because
MCP doesn’t only expose tools. The spec
defines three first-class primitives a server can serve to a client —
tools, resources, and prompts — and only one of them looks like a
function call with arguments a policy engine can pattern-match against.
The other two are just as capable of moving sensitive data or planting an
instruction, and most of the governance built for agentic AI was designed
before anyone thought to gate them.
The failure: policy written for one shape of request
A policy engine built around tool calls assumes every consequential request
looks the same: a name, a set of arguments, a principal asking. Rules get
written against that shape — “deny send_email to external domains,”
“require approval above this threshold,” “scope this credential to
read-only.” That works exactly as designed for tools/call.
It has nothing to say about resources/read or prompts/get, because those
aren’t calls with arguments in the same sense. A resource is addressed by a
URI the server advertised — file:///reports/q3-churn.csv,
postgres://warehouse/customers, s3://exports/latest.json — and reading
it is often a single, argument-free request: give me this URI, whatever it
currently contains. A prompt is a named template the server hands back,
sometimes filled with server-side context the client never sees until the
response arrives. Neither one carries the “tool, arguments, principal”
triple most agentic policy engines were built to inspect. A rule that reads
“this agent may only touch customer records it’s scoped to” can be airtight
for tools/call and simply never fire for a resources/read against a URI
that happens to resolve to the same customer table.
Why this is a data-ownership problem, not just a taxonomy gap
The practical effect isn’t abstract. A resource can return the same sensitive rows a tool call would have been scoped, redacted, or denied for — an export listing, a support-ticket dump, a warehouse table — except the read reached the agent through a primitive nobody wrote a rule against, because nobody realized there was a second door. That’s the same shape of problem we’ve written about for GDPR and A2A delegation: the data’s owner never approved this exposure, and the gap isn’t a broken control, it’s a control that was never asked to apply here.
Prompts carry the mirror-image risk. A prompt template is server-supplied text that gets inserted into the model’s context, often with variables the server fills in from its own state — which means a compromised or malicious MCP server can plant an instruction inside a “prompt” the same way a poisoned tool description plants one inside a schema, except the injection point is a primitive most security reviews don’t think to check, because the review checklist says “tools” and stops there. It’s the same delivery mechanism behind indirect prompt injection — content the agent was always going to read, carrying instructions nobody vetted — just arriving through a different RPC method than the one that usually gets the security attention.
Why “just review the tools list” doesn’t catch it
The instinctive audit — read the server’s tool list, check each one against
policy — passes a server clean even when it also serves resources and
prompts nobody looked at, because tools/list was never going to show
them. A rug-pulled tool
at least changes something a reviewer nominally approved once. A resource
or prompt that was never on anyone’s approved list to begin with doesn’t
need to pull anything — it just has to exist, unreviewed, on a server the
client already trusts for its tools.
Broadening the tool-call policy to also match on URI patterns or prompt names is a partial fix and a fragile one: it requires someone to remember resources and prompts exist every time policy gets written, for every server, forever, with no structural guarantee that the next server addition gets the same scrutiny the tools did.
Where the fix actually has to live
- Treat every MCP primitive as a call, not just
tools/call. A policy engine sitting at the transport boundary should seeresources/readandprompts/getas gated events with the same standing as a tool invocation — principal, target, timestamp — not traffic that passes underneath the policy layer because it doesn’t match the tool-call shape the rules were written for. - Scope credentials to what a read can reach, not to a tool’s name. If the credential behind a connection can resolve any URI a server chooses to expose as a resource, the tool-level allowlist upstream of it is decorative — the resource read bypasses it entirely and reaches the same data the tool call would have been denied for.
- Log resource reads and prompt fetches in the same trail as tool calls. An incident review that can answer “which tools did this agent call” but not “which resources did it read or which prompts did it receive” is missing exactly the two doors an attacker — or a careless server — has the least reason to expect anyone’s watching. That’s the same completeness gap that makes traceability more than an audit log: it has to cover what actually moved, not just the primitive it’s easiest to instrument.
- Never trust a prompt template’s filled-in content as anything but untrusted data, the same discipline already owed to tool results and A2A payloads — a server-supplied prompt is server-supplied text, and nothing about calling it a “prompt” instead of a “tool result” changes what it’s allowed to make the model do next.
- Don’t let “we reviewed the server” mean “we reviewed the tools.” A
server-level security pass needs to enumerate all three primitive types
before anyone calls a connection reviewed, because a server that looks
clean on
tools/listcan still serve resources and prompts that were never in scope for that review.
The business case: gate what actually reaches the agent, not what’s named a tool
Fullmakt’s MCP surface is deliberately narrow: src/Fullmakt.Api/Mcp serves
tools only — list_workspaces,
get_history, run_saved_request, and the rest — with no resources or
prompts primitives in the mix. That’s not an oversight; it’s the same
reasoning behind every other narrowing decision in the broker: every read
and every write an agent can perform through Fullmakt goes through one
primitive type, gated by one policy engine, logged in one trail. There is no
second RPC method quietly carrying data or instructions around the control
a team thinks they’ve put in place.
That matters beyond Fullmakt’s own surface, because most teams aren’t only talking to Fullmakt’s MCP server — they’re pointing agents at third-party MCP servers whose primitive mix is out of their control:
- Every credential an agent uses to reach a third-party MCP server is scoped and dispensed by Fullmakt, regardless of which primitive the agent ends up calling on the other end. A resource read against a server’s advertised URI still has to resolve through a scoped credential minted for that specific task — not a standing key that happens to work for whatever the server decides to expose next.
- Policy is evaluated on the call Fullmakt brokers, not on a name it recognizes as a “tool.” A connection routed through the broker gets the same scoping and approval logic whether the agent’s request looks like a function call or a URI fetch, because the enforcement point is the credential behind the reach, not a pattern match against a request shape.
- Every brokered request lands in one audit trail, so an incident review isn’t stuck asking whether a server-side “prompt” or “resource” fell outside what got logged — if Fullmakt dispensed the credential that made the read possible, the read is in the trail.
The commercial logic is the one behind every control this series has described at Fullmakt’s chokepoint: a governance program that only recognizes tool calls is one primitive-type change away from a blind spot nobody wrote a rule for. Anchoring control at the credential — which every primitive still has to go through to reach real data — closes the gap without requiring every team to memorize the full MCP spec before writing their next policy rule.
FAQ
Are resources and prompts actually part of the MCP spec, or is this an edge case? They’re first-class, spec-defined primitives alongside tools — a compliant MCP server can serve any combination of the three. A client or policy layer that only expects tools is reading a narrower spec than the one servers are allowed to implement.
Can a resource read really expose the same data a tool call would have been denied for? Yes, if the credential behind the connection can resolve the resource’s URI. Nothing in the resource primitive requires the same scoping discipline teams apply to tool arguments — that discipline has to be imposed independently, at the credential or broker layer, not assumed from the tool policy.
Is a malicious prompt template the same attack as tool poisoning?
Related but distinct: tool
poisoning plants an
instruction in a tool’s description, reviewed once at connection time. A
poisoned prompt template is delivered fresh on every prompts/get call,
which means a server can serve a clean version during review and a
different one afterward — the same rug-pull dynamic, on a primitive most
reviews don’t check at all.
Does Fullmakt expose resources or prompts on its own MCP server? No — Fullmakt’s MCP surface is tools-only by design, so every read and write an agent performs through it goes through the same policy and audit path. Third-party servers an agent connects to may expose all three; Fullmakt’s credential scoping and logging apply to what the agent reaches through them regardless.
What’s the one control that closes this gap without rewriting every policy rule? Scope and log at the credential, not at the request shape. A resource read and a tool call both eventually need a credential to reach real data — gate that handoff once, and it doesn’t matter which primitive the request arrived as.
A login handshake proves which server an agent is talking to. A tool-call
policy governs the requests that look like function calls. Neither one
was ever asked whether a resources/read or a prompts/get should have
been allowed to reach what it reached — and until a governance program
answers that question deliberately, the honest audit of “what can this
agent touch” has to assume the answer is currently nobody.