# Resource Context Assembly

Course: [Model Context Protocol](https://www.devtutorials.ai/learn/mcp) · Section: Execution and Context
Source: https://www.devtutorials.ai/learn/mcp/execution-context/resource-context-assembly

> 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.

## 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.

```js runnable title="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:

```text
<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.

```js runnable title="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:

```text
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.

```js runnable title="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:

```text
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.

```js runnable title="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:

```text
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.

```js runnable title="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:

```text
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.
