Cognitive Guardrails
Prerequisites
Install MCP Fusion before following this recipe: npm install @vinkius-core/mcp-fusion @modelcontextprotocol/sdk zod — or scaffold a project with npx fusion create.
- Introduction
- The Token Explosion Problem
- .limit() — Automatic Truncation
- .agentLimit() — Custom Guidance
- Combining with Affordances
- Collection UI Blocks
Introduction
LLMs have finite context windows. When your database returns 10,000 rows and your handler blindly passes them through, the result is a token DDoS — 10,000 rows × ~500 tokens each = 5,000,000 tokens. The LLM chokes, the response is expensive, and the output quality degrades drastically.
MCP Fusion's Cognitive Guardrails solve this by truncating data before it reaches the LLM and injecting intelligent guidance about what was omitted. The agent learns to use filters instead of paginating through massive datasets.
The Token Explosion Problem
Without guardrails, a simple list query can cost more tokens than the entire conversation:
User: "Show me all users"
Handler returns: 10,000 user objects
LLM receives: ~5,000,000 tokens of JSON
Result: Slow, expensive, and the LLM loses track of the conversation contextWith a .limit(50) guardrail:
User: "Show me all users"
Handler returns: 10,000 user objects
Presenter slices: 50 items kept, 9,950 hidden
LLM receives: ~25,000 tokens + truncation warning
Result: Fast, cheap, and the LLM knows to suggest filters.limit() — Automatic Truncation
The simplest guardrail is .limit(). It slices the array before validation and appends an auto-generated truncation message:
import { createPresenter, t } from '@vinkius-core/mcp-fusion';
const UserPresenter = createPresenter('User')
.schema({
id: t.string,
name: t.string,
role: t.enum('admin', 'member', 'guest'),
})
.limit(50);When the handler returns 10,000 users, the AI receives exactly 50 items plus:
⚠️ Dataset truncated. 50 shown, 9950 hidden. Use filters to narrow results.This is enough to teach the AI to add filters on the next call. No configuration needed — the message is auto-generated from the truncation counts.
TIP
The truncation happens before Zod validation. This means you never waste CPU parsing 10,000 objects when the AI will only see 50.
.agentLimit() — Custom Guidance
When the auto-generated message isn't specific enough, .agentLimit() lets you craft a custom truncation message that guides the AI to the right filter:
import { createPresenter, t, ui } from '@vinkius-core/mcp-fusion';
const InvoicePresenter = createPresenter('Invoice')
.schema({
id: t.string,
amount_cents: t.number.describe('Value in CENTS — divide by 100'),
status: t.enum('paid', 'pending', 'overdue'),
client_name: t.string,
})
.agentLimit(30, (omitted) =>
ui.summary(
`⚠️ Showing 30 of ${30 + omitted} invoices. ` +
`Use the 'status' filter (paid/pending/overdue) or ` +
`'client' filter to narrow results.`
)
);The callback receives the number of omitted items, so you can build a dynamic message. The AI now knows exactly which filters are available instead of guessing.
Combining with Affordances
The real power emerges when you combine guardrails with Agentic Affordances. The truncation warning tells the AI that data is missing; the suggested actions tell it how to get what it needs:
import { createPresenter, t, suggest, ui } from '@vinkius-core/mcp-fusion';
const TaskPresenter = createPresenter('Task')
.schema({
id: t.string,
title: t.string,
status: t.enum('open', 'in_progress', 'done'),
priority: t.enum('low', 'medium', 'high'),
assignee: t.string,
})
.limit(50)
.suggest(() => [
suggest('tasks.search', 'Search tasks by title or keyword'),
suggest('tasks.by_assignee', 'Filter tasks by team member'),
]);When the AI receives a truncated dataset, it sees:
⚠️ Dataset truncated. 50 shown, 1432 hidden. Use filters to narrow results.
[SYSTEM HINT]: → tasks.search: Search tasks by title or keyword
[SYSTEM HINT]: → tasks.by_assignee: Filter tasks by team memberInstead of requesting "page 2", the AI follows the hint and calls tasks.search with a targeted query — fewer tokens, better results.
Collection UI Blocks
For array responses, use .collectionUiBlocks() to generate aggregate visualizations instead of N individual charts. This pairs naturally with guardrails — show a summary chart of the full dataset, even when individual items are truncated:
const InvoicePresenter = createPresenter('Invoice')
.schema({
id: t.string,
amount_cents: t.number,
status: t.enum('paid', 'pending', 'overdue'),
})
.limit(50)
.collectionUiBlocks((invoices) => [
ui.echarts({
xAxis: { data: invoices.map(i => i.id) },
series: [{ type: 'bar', data: invoices.map(i => i.amount_cents / 100) }],
}),
ui.summary(`${invoices.length} invoices shown.`),
]);The chart renders from the kept items, giving the AI visual context even when the full dataset is truncated. Combined with system rules and suggested actions, this creates a complete perception package that keeps the AI on the right track.