MCP Resources vs Tool Results for Context
Choose MCP resources, links, embeds, or tool-result content by control, timing, side effects, payload, permission, and provenance.
MCP resources vs tool results is a control decision before it is a payload decision. This guide traces initiator, side effects, freshness, size, authority, and provenance so each context path stays explainable.
MCP resources vs tool results start with control
The most useful distinction is not “static versus dynamic.” It is who initiates the context transfer. Resources are application-driven: a host can list, browse, select, cache, or subscribe to material before the model asks for an action. Tools are model-controlled: the model chooses an operation, the server executes it, and the result returns through the tool call. MCP resources vs tool results therefore begins as a custody decision.
That distinction changes consent and timing. A product can let a person attach a resource deliberately, display its URI, and show exactly what enters the prompt. A tool result arrives after the model has proposed an execution and the host has applied its policy. Neither path is automatically safer. The host still owns permission checks, disclosure, size limits, and treatment of returned content as untrusted data.
Start a design review by writing two labels beside every context source: initiator and executor. “Application / read” points toward a resource. “Model / execute” points toward a tool. That is the practical core of agent context ownership. When the labels differ, preserve both in the receipt instead of flattening them into a generic retrieval event.
- Two control planes, one context boundary
- Application-selected resource reads and model-requested tool executions converge only after separate host permission gates.
| Primitive | Initiator | Host decision | Output identity |
|---|---|---|---|
| Resource | Application | Selection and read permission | URI, MIME type, revision |
| Tool | Model proposes | Execution and side-effect policy | Result shape and provenance |
Separate listed resources from URI templates
A listed resource is a concrete address the server exposes for discovery: a handbook, schema, or current project brief. A resource template describes an address family with parameters, such as a customer record keyed by an identifier. In both cases the application remains responsible for deciding whether to surface or read the address. The MCP Resources specification also defines annotations and optional subscriptions, but those hints do not override host policy.
MCP resources vs tool results becomes clearer when URI identity survives the entire trip. Keep the original URI, declared MIME type, server name, fetch time, and any content digest together. If a template expands to a URI, record the parameters separately; do not present the expanded content as though it were a timeless attachment. A second read may be different.
Use listing for browseable, user-recognizable context and templates for large, addressable collections. MCP resource links remain distinct because they preserve an address inside another operation's result instead of appearing in discovery. Avoid listing millions of instances merely because the protocol allows pagination. Discovery should help a person or application choose, not smuggle an index into the context window. A resource is an addressable object, not permission to read every object in its namespace.
Distinguish resource links from embedded content
A tool result can carry ordinary text, structured content, a resource link, or an embedded resource. MCP tool result content therefore has more than one custody shape. A resource link preserves an address and asks the client to decide if or when to fetch it. An embedded resource carries the contents inside the tool result. Those shapes may describe the same underlying document, yet they create different payload, freshness, and audit behavior. The MCP Tools specification makes that plurality explicit.
Choose a link when the payload is large, independently permissioned, reusable, or likely to change. Choose an embed when the returned bytes are the bounded evidence needed to interpret this specific execution. Choose structured content when downstream code needs a declared result shape rather than a document identity. MCP resources vs tool results is not a contest with one winner; it is a choice among custody shapes at a particular boundary.
Do not silently dereference a resource link and then log only the tool name. That erases the second authorization decision and the source URI. Conversely, do not wrap every small result in a URI merely to appear traceable. The same rule applies to embedded MCP resources: keep their identity, authorization, and size record. Provenance requires meaningful identity plus a readable chain of actions, not ceremonial indirection.
Choose by freshness and side effects
Freshness and mutation are separate axes. A resource read can be fresh and still read-only. A tool can be read-only while producing a result calculated from live state. Another tool may mutate state and return a resource link to the changed object. The correct model records each axis explicitly: desired freshness, execution side effects, and whether a subsequent read is required.
For MCP resources vs tool results, prefer a resource when the user wants to inspect known context and the application can make staleness visible. Prefer a tool when obtaining the answer requires computation, search, validation, or mutation. A live dashboard snapshot is not automatically a tool; an application-driven resource read can supply it. A “generate report” operation is a tool even if its output is later exposed as a resource.
Subscriptions can notify a client that a resource changed, but notification is not content and does not grant renewed authority. Re-read through the same permission gate. If the system cannot say whether an operation has side effects, reject it before execution. The local fixture deliberately rejects a case labeled “resource” that declares mutation, because a friendly label cannot repair a false control contract.
Preserve URI, MIME type, and provenance
Context loses value when its origin disappears. Keep the URI for identity, MIME type for interpretation, server and method for custody, retrieved time for freshness, and digest for byte-level comparison. If content is transformed—PDF to text, table to JSON, or image to description—record the transform as a new step. The MCP schema reference is the place to confirm exact content-block fields rather than guessing from a client UI.
MCP resources vs tool results also needs a trust label. Titles, descriptions, audience hints, annotations, embedded text, and structured fields can all contain adversarial instructions. Treat them as data. A display label such as “approved policy” is not proof of approval. This is the same boundary used when you treat retrieved and tool-returned content as untrusted.
Stable identity supports later citation. A digest alone cannot explain what was read, and a URI alone cannot prove which revision appeared. Pair them. If source documents evolve, preserve source identity as documents change by keeping revision metadata or content hashes beside the human-readable citation.
- Four context-custody lanes
- A sequence follows listed resources, resource links, embedded resources, and structured results through the actors and permissions that move them.
- Listed resource: host chooses a known URI and checks read authority.
- Resource link: tool returns identity; host performs a separate fetch decision.
- Embedded resource: bounded content travels inside the tool result with its URI and MIME type.
- Structured result: validated fields travel with tool execution provenance.
Budget large context without hiding truncation
Large context should remain addressable before it becomes tokens. List metadata first, fetch the selected range, and expose pagination or truncation as part of the result. A host may summarize a long resource, but the receipt should name the summarizer, source ranges, original size, output size, and whether the full material remains reachable. “Success” cannot mean silently dropping the end of a document.
MCP resources vs tool results offers a practical payload rule. Embed the bounded evidence required to understand a tool execution. Return a link when content is independently useful, exceeds the host’s inline budget, needs separate permission, or benefits from later refresh. Use structured content for fields the consumer must validate. The fixture’s size limit is a local teaching policy, not a protocol maximum.
Hosts with many capabilities can defer tool definitions without losing policy, but deferred definitions and deferred content are different concerns. A tool catalog controls which operations are visible; a resource catalog controls which addresses are discoverable. Log each budget separately so a small tool schema does not conceal a giant embedded result, and a giant resource list does not crowd out the selected document.
Recheck authority at every read boundary
Discovery is not authority. Seeing a resource in a list means the server disclosed metadata under the current session; it does not guarantee that a later read is allowed. Template expansion, subscription refresh, resource-link dereference, and tool execution each deserve a current check. Bind the decision to subject, server, operation, resource identity, and relevant tenant or audience.
This matters when MCP resources vs tool results cross paths. A permitted tool might return a link to a resource the caller cannot read. The client should preserve the link, explain the blocked follow-up, and avoid laundering it through a privileged background fetch. An embedded resource should be authorized before embedding; the fact that bytes already arrived is evidence of a server-side policy problem, not consent to reveal them.
Presentation needs the same discipline. Keep MCP presentation inside a host-owned boundary so returned markup or labels cannot impersonate approvals. Show which actor proposed the tool, which actor approved it, which server executed it, and which resource was read. Those labels are more useful than a single green “trusted” badge because they let reviewers locate the broken decision.
Run six context-delivery cases
The committed Node artifact routes six synthetic needs: a user-selected handbook, a templated customer record, a large generated report, a compact calculated answer, a fresh subscribed status document, and a state-changing operation. Each case declares initiator, side effects, freshness, payload bytes, permission, URI, MIME type, and provenance. The router selects exactly one of listed resource, templated resource, resource link, embedded resource, or structured tool result.
Then the MCP resources vs tool results lab turns hostile. It rejects a stale case that requires current data, a denied resource read, an oversized embed, and a mutation mislabeled as a resource. These are deterministic protocol-shape checks against local policy. They do not connect to a server, benchmark a client, or prove remote authorization.
Run it with the command printed beside the artifact. Read the rejection code before reading the selected path; rejection quality is part of the interface. Change one input at a time and confirm the receipt carries the original URI and MIME type. If a real integration cannot produce the same custody fields, the missing field is a design issue to resolve before adding more context.
Runnable artifact — A local protocol-shape simulator; it does not connect to an MCP server, benchmark clients, or prove remote authorization.
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
const sha256 = (value) => createHash("sha256").update(value).digest("hex");
const policy = Object.freeze({ embedLimitBytes: 4096, now: "2026-09-04T12:00:00.000Z" });
const cases = Object.freeze([
{ id:"handbook", initiator:"application", operation:"read", shape:"listed-resource", uri:"kb://handbook/v4", mime:"text/markdown", bytes:1800, current:true, allowed:true, sideEffects:false, provenance:"docs-server@v4" },
{ id:"customer-record", initiator:"application", operation:"read-template", shape:"templated-resource", uri:"crm://customer/fixture-17", mime:"application/json", bytes:920, current:true, allowed:true, sideEffects:false, provenance:"crm-fixture@17" },
{ id:"generated-report", initiator:"model", operation:"generate", shape:"resource-link", uri:"report://synthetic/weekly-7", mime:"text/html", bytes:18000, current:true, allowed:true, sideEffects:false, provenance:"report-tool@2" },
{ id:"risk-score", initiator:"model", operation:"calculate", shape:"structured-tool-result", uri:"urn:fixture:risk:42", mime:"application/json", bytes:340, current:true, allowed:true, sideEffects:false, provenance:"risk-fixture@1" },
{ id:"live-status", initiator:"application", operation:"read-subscribed", shape:"listed-resource", uri:"status://service/current", mime:"application/json", bytes:580, current:true, allowed:true, sideEffects:false, provenance:"status-fixture@12" },
{ id:"approve-draft", initiator:"model", operation:"mutate", shape:"embedded-resource", uri:"draft://fixture/9", mime:"application/json", bytes:760, current:true, allowed:true, sideEffects:true, provenance:"draft-tool@3" },
]);
function route(input) {
const custody = { actor:input.initiator, permissionChecked:true, uri:input.uri, mime:input.mime, provenance:input.provenance, bytes:input.bytes, retrievedAt:policy.now, sideEffects:input.sideEffects };
if (!input.allowed) return { id:input.id, accepted:false, rejection:"permission-denied", custody };
if (!input.current && input.requiresCurrent) return { id:input.id, accepted:false, rejection:"stale-content", custody };
if (input.initiator === "application" && input.sideEffects) return { id:input.id, accepted:false, rejection:"resource-mislabeled-side-effect", custody };
if (input.shape === "embedded-resource" && input.bytes > policy.embedLimitBytes) return { id:input.id, accepted:false, rejection:"embed-over-budget", custody };
const legal = input.initiator === "application"
? ["listed-resource","templated-resource"].includes(input.shape)
: ["resource-link","embedded-resource","structured-tool-result"].includes(input.shape);
if (!legal) return { id:input.id, accepted:false, rejection:"control-shape-mismatch", custody };
return { id:input.id, accepted:true, selected:input.shape, custody };
}
const accepted = cases.map(route);
const adversarial = [
route({ ...cases[4], id:"stale-status", current:false, requiresCurrent:true }),
route({ ...cases[0], id:"denied-handbook", allowed:false }),
route({ ...cases[5], id:"oversize-embed", bytes:9000 }),
route({ ...cases[0], id:"mislabeled-mutation", sideEffects:true }),
];
assert.ok(accepted.every((row) => row.accepted));
assert.deepEqual(accepted.map((row) => row.selected), cases.map((row) => row.shape));
assert.deepEqual(adversarial.map((row) => row.rejection), ["stale-content","permission-denied","embed-over-budget","resource-mislabeled-side-effect"]);
assert.ok([...accepted,...adversarial].every((row) => row.custody.uri && row.custody.mime && row.custody.provenance && row.custody.permissionChecked));
const payload = { fixture:"mcp-context-custody-v1", remoteServerCalled:false, policy, accepted, adversarial };
const serialized = JSON.stringify(payload);
console.log(JSON.stringify({ ...payload, sha256:sha256(serialized) }, null, 2));
console.log("PASS: six custody routes and four named rejections preserve actor, URI, MIME, provenance, size, and side effects");
Read the custody matrix as a decision tool
The third figure turns the cases into a compact matrix. Application-selected, read-only, known-address material sits in the resource region. Model-requested work with computed structured output sits in the tool-result region. Large or separately governed output moves toward a resource link. Small evidence inseparable from the execution can remain embedded. Every row keeps a provenance requirement because provenance is not optional in any region.
Use the matrix during API review, not as runtime magic. MCP resources vs tool results still depends on product context: whether the person recognizes the source, whether the model needs an operation, whether the payload can fit, and whether the host can explain a second fetch. A decision table can expose assumptions; it cannot supply absent authorization policy.
The semantic legend repeats all encodings without color, including initiator, custody, freshness, payload, and rejection markers. That makes the thesis usable at 200% zoom and with the SVG unavailable. The matrix is intentionally about decisions rather than performance because no remote latency measurements were collected for this article.
- Six-case context-delivery decision matrix
- Six named synthetic fixtures map control, custody, freshness, side effects, payload shape, and provenance to an explicit MCP delivery path.
| Fixture | Delivery path | Control rationale | Custody | Status |
|---|---|---|---|---|
| handbook | Listed resource | Application selects a recognizable read | app custody | read-only; synthetic fixture provenance retained |
| customer-record | Resource template | Application supplies the parameterized URI | app custody | read-only; synthetic fixture provenance retained |
| generated-report | Resource link | Tool returns a link to a large independent payload | tool → app custody | generated; synthetic fixture provenance retained |
| risk-score | Structured result | Tool returns validated compact fields | tool custody | computed; synthetic fixture provenance retained |
| live-status | Subscribed resource | Application receives a fresh resource update | server → app custody | live; synthetic fixture provenance retained |
| approve-draft | Embedded result | Tool returns bounded evidence after a mutation | tool → app custody | mutating; synthetic fixture provenance retained |
Ship a context-custody receipt
A release receipt should answer: who initiated the transfer, which operation ran, what source identity survived, which permissions were checked, what bytes entered context, what was truncated or transformed, and whether state changed. Store rejection reasons as first-class outcomes. If an operator must infer those facts from free-form logs, the interface is not yet reviewable.
For MCP resources vs tool results, choose the narrowest shape that preserves control. Use application-selected resources for inspectable context, tool execution for model-requested work, resource links for large or independently governed outputs, embeds for bounded execution evidence, and structured results for validated fields. Revisit the decision when the protocol revision, content-block schema, or client control model changes.
The action is deliberately singular: run the custody fixture before adding the next context-producing MCP capability. Archive its JSON receipt with the design review, then compare the real integration field by field. If the host cannot name actor, authority, identity, size, freshness, and side effects, stop there. More context would only make the custody gap harder to see.