HomeJournalThis post

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.

JP
JP Casabianca
AI Engineer and Product Designer · full-stack delivery · Bogotá

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 boundaryApplication-selected resource reads and model-requested tool executions converge only after separate host permission gates.APPLICATION CONTROLMODEL CONTROLbrowse → choose → readpropose → approve → executeHOST GATEHOST GATEresource URI + MIMEtool result + side effects
Two control planes, one context boundary
Application-selected resource reads and model-requested tool executions converge only after separate host permission gates.
Control boundary
PrimitiveInitiatorHost decisionOutput identity
ResourceApplicationSelection and read permissionURI, MIME type, revision
ToolModel proposesExecution and side-effect policyResult shape and provenance
Figure 1: Initiator and executor remain separate even when both paths eventually supply model context.

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 lanesA sequence follows listed resources, resource links, embedded resources, and structured results through the actors and permissions that move them.hostresource servertool servermodellisted read · permission 1tool call · execution policyresource link · no auto-fetchembed / structured result · custody receipt
Four context-custody lanes
A sequence follows listed resources, resource links, embedded resources, and structured results through the actors and permissions that move them.
  1. Listed resource: host chooses a known URI and checks read authority.
  2. Resource link: tool returns identity; host performs a separate fetch decision.
  3. Embedded resource: bounded content travels inside the tool result with its URI and MIME type.
  4. Structured result: validated fields travel with tool execution provenance.
Figure 2: A resource link adds a second decision; an embed carries the selected bytes through the execution boundary.

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 matrixSix named synthetic fixtures map control, custody, freshness, side effects, payload shape, and provenance to an explicit MCP delivery path.handbookListed resourceapp custodyread-onlycustomer-recordResource templateapp custodyread-onlygenerated-reportResource linktool → app custodygeneratedrisk-scoreStructured resulttool custodycomputedlive-statusSubscribed resourceserver → app custodyliveapprove-draftEmbedded resulttool → app custodymutating
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.
Six-case routing, custody, and provenance summary
FixtureDelivery pathControl rationaleCustodyStatus
handbookListed resourceApplication selects a recognizable readapp custodyread-only; synthetic fixture provenance retained
customer-recordResource templateApplication supplies the parameterized URIapp custodyread-only; synthetic fixture provenance retained
generated-reportResource linkTool returns a link to a large independent payloadtool → app custodygenerated; synthetic fixture provenance retained
risk-scoreStructured resultTool returns validated compact fieldstool custodycomputed; synthetic fixture provenance retained
live-statusSubscribed resourceApplication receives a fresh resource updateserver → app custodylive; synthetic fixture provenance retained
approve-draftEmbedded resultTool returns bounded evidence after a mutationtool → app custodymutating; synthetic fixture provenance retained
Figure 3: All six synthetic fixtures are visible in both geometry and semantics; each preserves its delivery path, custody boundary, and provenance status.

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.