Agentic Affordances
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 Problem — Blind Navigation
- suggest() — Next-Action Hints
- Context-Aware Suggestions
- Wiring Presenters to Tools
- Collection-Level Guidance
- How It Looks to the AI
Introduction
In a traditional MCP server, after receiving a response the AI must scan the entire tools/list to figure out what to do next. With 50+ tools, it often picks the wrong one or hallucinates a tool name that doesn't exist.
MCP Fusion introduces Agentic Affordances — a HATEOAS-style mechanism where each response carries suggested next actions based on the current data state. The AI no longer guesses. It follows the shortest path.
The Problem — Blind Navigation
Consider an invoice lookup. Without affordances:
AI receives: { id: "INV-001", status: "pending", amount_cents: 45000 }
AI thinks: "The invoice is pending... I should probably pay it? Or maybe send a reminder?
What were those tool names again? billing.pay? billing.charge? billing.process?"The AI wastes tokens scanning tool lists and may hallucinate non-existent tool names. With affordances, the response itself tells the AI exactly what to do.
suggest() — Next-Action Hints
The suggest() helper creates HATEOAS-style hints inside a Presenter's .suggest() method. Each hint carries a tool name and a human-readable reason:
import { createPresenter, t, suggest } from '@vinkius-core/mcp-fusion';
const InvoicePresenter = createPresenter('Invoice')
.schema({
id: t.string,
amount_cents: t.number.describe('CENTS — divide by 100 for display'),
status: t.enum('paid', 'pending', 'overdue'),
})
.suggest((invoice) => {
if (invoice.status === 'pending') {
return [
suggest('billing.pay', 'Process immediate payment'),
suggest('billing.send_reminder', 'Send payment reminder'),
];
}
if (invoice.status === 'overdue') {
return [
suggest('billing.escalate', 'Escalate to collections'),
suggest('billing.pay', 'Attempt late payment'),
];
}
return [
suggest('billing.archive', 'Invoice is settled — archive it'),
];
});The suggestions are data-driven — a pending invoice suggests "pay" and "remind", while an overdue invoice suggests "escalate". A paid invoice suggests "archive". The AI always receives the most relevant next steps for the current state.
Context-Aware Suggestions
The .suggest() callback receives ctx as the second argument, enabling suggestions based on user role, tenant config, or any context property:
const UserPresenter = createPresenter('User')
.schema({
id: t.string,
name: t.string,
role: t.enum('admin', 'member', 'guest'),
status: t.enum('active', 'suspended', 'pending'),
})
.suggest((user, ctx) => {
const hints = [];
if (user.status === 'pending') {
hints.push(suggest('users.approve', 'Approve pending user'));
hints.push(suggest('users.reject', 'Reject and notify'));
}
if (user.status === 'active' && user.role !== 'admin') {
hints.push(suggest('users.promote', 'Promote to admin'));
}
if (user.status === 'suspended') {
hints.push(suggest('users.reactivate', 'Reactivate suspended account'));
}
// Only admins see the delete action
if (ctx?.user?.role === 'admin') {
hints.push(suggest('users.delete', 'Permanently remove user'));
}
return hints;
});TIP
Use null filtering for conditional suggestions. Return null for suggestions that don't apply and .filter(Boolean) the array. The Presenter framework handles null values gracefully.
Wiring Presenters to Tools
Connect your Presenter to a tool with .returns(). The handler just returns raw data — the Presenter handles validation, rules, UI blocks, and affordances automatically:
import { initFusion } from '@vinkius-core/mcp-fusion';
const f = initFusion<AppContext>();
export const getInvoice = f.query('billing.get_invoice')
.describe('Get an invoice by ID')
.withString('id', 'Invoice ID')
.returns(InvoicePresenter)
.handle(async (input, ctx) => {
return ctx.db.invoices.findUnique({ where: { id: input.id } });
});That's it. The handler is three lines. The Presenter does all the heavy lifting:
- Validates the returned data against the schema (strips undeclared fields)
- Attaches system rules (e.g. "amount_cents is in CENTS")
- Generates UI blocks if configured
- Injects suggested actions based on the data state
Collection-Level Guidance
For list endpoints, affordances guide the AI to use filters instead of scrolling through massive datasets:
const ProjectPresenter = createPresenter('Project')
.schema({
id: t.string,
name: t.string,
status: t.enum('active', 'archived'),
})
.limit(50)
.suggest(() => [
suggest('projects.search', 'Search by name for specific projects'),
suggest('projects.filter', 'Filter by status or date range'),
]);When the dataset is truncated (showing 50 of 2,000), the agent receives both the truncation warning and the suggested actions. Instead of requesting "page 2", it follows the hint and calls projects.search with a targeted query.
How It Looks to the AI
The AI's response contains a structured perception package. The affordances appear as system hints at the end:
[DATA]: { "id": "INV-001", "amount_cents": 45000, "status": "pending" }
[DOMAIN RULES]: amount_cents is in CENTS. Divide by 100 for display.
[SYSTEM HINT]: → billing.pay: Process immediate payment
[SYSTEM HINT]: → billing.send_reminder: Send payment reminderThe AI reads these hints and follows the shortest path to the next action. No scanning tools/list. No hallucinating tool names. Just structured guidance based on real data.