Resource Context Assembly

By the Syncfusion DevTutorials team · Last updated

Turning MCP resources into safe, useful context: source tags, freshness, field filtering, truncation, redaction, read-only enforcement and scope validation.

A good lawyer does not hand a judge a box of every paper in the office. They pick the papers that matter, note where each one came from and when, hide private details the case does not need, and put the papers in a useful order. That makes it easier to check the sources and understand the evidence.

A host does something similar with resources. It does not add every available resource to the model's context. It assembles the resource data it needs: selecting it, labeling its source, checking how old it is, filtering it, redacting private details, and enforcing access rules.

The main idea: resource context should pass through a host-controlled pipeline: select, tag sources, check freshness, filter and truncate, redact, and enforce scope. These steps govern resource data added through this pipeline; they do not control other context the model may receive.

A Real-World Example

Brightgrid's assistant once answered a delivery question with: "Your order is in bin B7-22, and our cost was $41.20." Both facts were true, but neither belonged in a customer answer. The assistant had received the full order record, including internal details.

The team changed how the host assembles order context:

  • It keeps only order_id, item, status, and eta for the task.
  • It records each piece's URI and the time it was read.
  • It redacts email addresses, phone numbers, and card digits from ticket text when those details are not needed.
  • It allows only the resource schemes needed for customer chats, such as orders:// and kb://, and excludes payroll://.

The host no longer includes those internal order fields in the order-summary context. Access checks still need to confirm that a customer is allowed to read the specific order, not just that its URI starts with orders://.

In simple words: only include the resource data needed for the task, and enforce access and privacy rules in code. Labels and model instructions help organize context, but they do not replace those checks.

What You Will Learn

In this lesson, you will learn:

  1. How to assemble resources with source tags.
  2. How to judge freshness.
  3. How to filter fields and truncate long text.
  4. How to redact personal data.
  5. How to enforce read-only access and scope.

This lesson builds on the Context Engineering course. There you learned to choose, order, and label context. Here you apply those ideas to MCP resources. The lab's Chat with tools tab shows a small version: tool and resource results are added to the model's messages.

Resource data passes through the host's assembly pipeline before the host includes it in model context. Each step can reduce risk or noise, but the checks must be implemented correctly.

Assembly with Source Tags

Once resource data is assembled into context, the source of each piece of information can become unclear. Source tags keep that information attached to the data so answers remain easier to verify and review.

Why Tag Sources

Add a source tag to each piece of context so its origin and read time are visible. Tags can help:

  • the model cite a source in its answer,
  • people check which source supported an answer,
  • the host and model distinguish resource data from surrounding instructions.

A tag helps organize context; it does not prove the content is true or prevent prompt injection. The XML-like tags in the next example are a teaching convention, not a special MCP format. In a real system, escape or serialize attribute values and treat resource text as untrusted data.

Assemble resources with source tags

// Assemble resources into context, each with a source tag the model can cite.
const reads = [
  {
    uri: "orders://A-1002",
    mimeType: "application/json",
    text: '{"item":"Air purifier","status":"processing","eta":"2026-10-18"}',
    readAt: "2026-10-09T10:02Z",
  },
  {
    uri: "kb://shipping",
    mimeType: "text/plain",
    text: "Standard shipping takes 3 to 6 days.",
    readAt: "2026-10-09T10:02Z",
  },
];

const context = reads
  .map(
    (r) =>
      `<source uri="${r.uri}" type="${r.mimeType}" read="${r.readAt}">\n${r.text}\n</source>`,
  )
  .join("\n");
console.log(context);
console.log("\nRule for the model: cite the source uri for every fact you use.");

Expected output:

<source uri="orders://A-1002" type="application/json" read="2026-10-09T10:02Z">
{"item":"Air purifier","status":"processing","eta":"2026-10-18"}
</source>
<source uri="kb://shipping" type="text/plain" read="2026-10-09T10:02Z">
Standard shipping takes 3 to 6 days.
</source>

Rule for the model: cite the source uri for every fact you use.

The order_summary prompt shows a server-side example of the same idea: a message can contain an embedded resource with its URI and MIME type. The host can pass that message to the model along with the task.

Freshness

How Old Is Too Old?

Resource data can change over time. An answer that was correct an hour ago may be wrong now if an order status, inventory level, or log entry has changed. Before adding resource data to the model, the host should decide whether it is still fresh enough for the task.

Different data goes stale at different speeds:

  • An order's status may change within minutes.
  • Today's error logs may need to be current within 15 minutes.
  • A returns policy may stay current for weeks.

MCP does not choose a freshness limit for your data. The host sets a maximum age based on the task and records when it read each resource. If a resource is too old, the host can read it again or mark it as possibly out of date. For example, a shipping policy may be allowed to stay in context longer than an order status.

Check freshness

// Freshness: is each piece still fresh enough for this question?
const now = Date.parse("2026-10-09T10:00:00Z");
const pieces = [
  { uri: "orders://A-1002", readAt: "2026-10-09T09:58:00Z", maxAgeMin: 5 },
  { uri: "kb://shipping", readAt: "2026-10-01T08:00:00Z", maxAgeMin: 60 * 24 * 30 },
  { uri: "logs://api/today", readAt: "2026-10-09T08:00:00Z", maxAgeMin: 15 },
];

for (const p of pieces) {
  const ageMin = Math.round((now - Date.parse(p.readAt)) / 60000);
  console.log(
    p.uri.padEnd(18),
    `age ${String(ageMin).padStart(5)} min`,
    ageMin <= p.maxAgeMin ? "fresh" : "STALE: read it again",
  );
}

Expected output:

orders://A-1002    age     2 min fresh
kb://shipping      age 11640 min fresh
logs://api/today   age   120 min STALE: read it again

Filtering and Truncation

Resources often contain information that is useful for storage or operations but irrelevant to the current task. Sending unnecessary fields increases context size and can expose information the model does not need.

Field Filtering

A record may contain more fields than a task needs, such as internal costs, warehouse codes, or supplier contracts. Keep only the fields needed for the task. This can reduce the context size and the chance of exposing information the model does not need.

Models can process only a limited amount of context at a time. Large logs, histories, and documents can consume that space quickly, leaving less room for information that is more relevant to the task.

Truncation

Long text, such as a history or a large log, may not fit the available context budget. If you cut it, say that you did so the model and a reviewer know the text is incomplete. This example truncates plain text by character count; real context limits are often measured in tokens. For structured data, cut at sensible record boundaries when possible so the result remains understandable.

Filter fields and truncate

// Field filtering and truncation: send only what the question needs, within a budget.
const record = {
  order_id: "A-1002",
  item: "Air purifier",
  status: "processing",
  eta: "2026-10-18",
  internal_cost: 41.2,
  warehouse_bin: "B7-22",
  supplier_contract: "SUP-99-1188",
  history: Array.from(
    { length: 40 },
    (_, i) => `2026-09-${String((i % 28) + 1).padStart(2, "0")} status check`,
  ),
};

const keep = ["order_id", "item", "status", "eta"];
const filtered = Object.fromEntries(keep.map((k) => [k, record[k]]));

function truncate(text, maxChars) {
  return text.length <= maxChars
    ? text
    : text.slice(0, maxChars) + ` …[cut ${text.length - maxChars} characters]`;
}

console.log("full record:", JSON.stringify(record).length, "characters");
console.log("filtered:   ", JSON.stringify(filtered));
console.log("history, cut:", truncate(record.history.join("; "), 60));

Expected output:

full record: 1215 characters
filtered:    {"order_id":"A-1002","item":"Air purifier","status":"processing","eta":"2026-10-18"}
history, cut: 2026-09-01 status check; 2026-09-02 status check; 2026-09-03 …[cut 938 characters]

When practical, have the server return a narrow result instead of sending a full record and relying on the host to trim it. The host should still apply its own filtering and access rules before adding data to context.

Redaction

Not every piece of information in a resource is necessary for a task. Personal or sensitive information that provides no value to the current request should be removed before it reaches the model.

Hide What the Model Doesn't Need

Redaction means replacing sensitive details with a label, like [email]. Do it before the text enters model context. Examples include:

  • email addresses and phone numbers,
  • card and account numbers,
  • passwords, keys, and tokens, which should not be included in a resource unless there is a specific, secure need.

The patterns below illustrate the idea; regular expressions like these do not detect every real-world format. Use tested, appropriate checks for the data you handle.

Redact personal data

// Redaction: remove personal data the model doesn't need, before it enters the context.
const ticket =
  "Customer Asha Rao (asha.rao@example.com, +1 415 555 0134) says order A-1002 is late. Card ending 4242.";

const rules = [
  [/[\w.+-]+@[\w-]+\.[\w.]+/g, "[email]"],
  [/\+?\d[\d ]{8,}\d/g, "[phone]"],
  [/card ending \d{4}/gi, "[card]"],
];

let safe = ticket;
for (const [pattern, label] of rules) {
  safe = safe.replace(pattern, label);
}
console.log(safe);

Expected output:

Customer Asha Rao ([email], [phone]) says order A-1002 is late. [card].

Keep information the task needs. If the model must write "Hi Asha", the name may be needed. If it only needs to know the order is late, remove contact details. Redaction should follow the task and the data-handling policy.

Read-Only Enforcement and Scope

Reading information and modifying information are different actions. During context assembly, the goal is to gather data for the model, not to change business records or system state.

Read-Only

A resource is read through resources/read; that operation should not change the underlying data. The server is responsible for implementing resource reads without side effects. Hosts should send changes through tools and apply their usual approval rules to tool calls.

Scope Validation

Scope is the set of resources a chat or user is allowed to read. The host and server should check access before returning resource data. Checks may include:

  • the allowed URI schemes or prefixes, such as allowing kb:// and orders:// but not payroll://,
  • ownership, such as whether this customer may read this particular order,
  • file paths, including rejecting traversal attempts such as ../.

The example below demonstrates a simple scheme allowlist and a method check. It does not check who owns an order, and a production system needs that authorization check too.

Check scope before reading

// Scope validation: allow only reads for schemes this chat is allowed to use.
const allowed = ["kb://", "orders://"];

function canRead(uri, method) {
  if (method !== "resources/read") {
    return "blocked: this check only allows resource reads";
  }
  if (!allowed.some((prefix) => uri.startsWith(prefix))) {
    return "blocked: outside this chat's scope";
  }
  return "ok";
}

console.log(canRead("orders://A-1002", "resources/read"));
console.log(canRead("kb://returns-policy", "resources/read"));
console.log(canRead("payroll://2026-09", "resources/read"));
console.log(canRead("orders://A-1002", "tools/call"));

Expected output:

ok
ok
blocked: outside this chat's scope
blocked: this check only allows resource reads

Scope checks belong in code, in both the host and the server. A model instruction such as "don't read payroll" does not enforce access. Code can enforce a policy only when it checks the right identity and resource using trusted, correctly parsed values.

Common mistakes

  • Too much context. Whole records and full logs crowd out what matters and may expose details the task does not need.
  • No timestamps. Without a read time, nobody can tell how old a value such as processing is.
  • Silent truncation. The model may say something is missing when it was cut from the context.
  • Redacting after the model saw it. Once data is in context, it may appear in an answer.
  • Treating labels as security. Source tags help identify data, but do not prevent prompt injection or enforce authorization.
  • Scope in prompts. Use code to check access to each URI; do not rely on instructions to the model.

Exercises

Each exercise ends with a value on the last line. Press Check to compare it with the expected answer.

What to Remember

  • Assembly is a pipeline: select, tag, check freshness, filter and truncate, redact, enforce scope.
  • Source tags with URI and read time make context easier to check; they do not guarantee accurate citations or prevent prompt injection.
  • Freshness depends on the kind of data. Re-read stale pieces.
  • Keep only needed fields, and say when you truncate.
  • Redact personal data before it enters the context.
  • Servers must keep resource reads free of side effects; enforce scope in host and server code.

Next: Chapter 5, Multi Round-Trip Requests, how servers ask for input in the middle of a request.