State & Context
Prerequisites
Install Vurb.ts before following this guide: npm install @vurb/core @modelcontextprotocol/sdk zod — or scaffold a project with vurb create.
- Introduction
- Defining Context
- The Context Factory
- Multi-Tenant Context
- Middleware Derivation
- Context in Presenters
Introduction
Every tool handler in Vurb.ts receives two arguments: input (the validated parameters from the LLM) and ctx (the application context). The context carries shared state — database connections, tenant IDs, user sessions, permissions — that every handler needs without manual wiring.
Define it once. Use it everywhere. Fully typed, zero annotations.
Defining Context
Pass a generic to initVurb() and every tool, middleware, and Presenter inherits the type:
import { initVurb } from '@vurb/core';
interface AppContext {
userId: string;
db: PrismaClient;
}
const f = initVurb<AppContext>();Handlers receive ctx fully typed — no annotations, no casting:
const tasks = f.query('tasks.list')
.describe('List tasks for the current user')
.handle(async (input, ctx) => {
// ctx.db and ctx.userId are fully typed
return ctx.db.tasks.findMany({ where: { ownerId: ctx.userId } });
});TIP
Define f in a shared file (e.g. src/vurb.ts) and import it across your tool files. The generic parameter flows through every builder — zero annotations needed downstream.
The Context Factory
The contextFactory runs on every tool invocation. Attach it when connecting the registry to the server:
registry.attachToServer(server, {
contextFactory: async (extra) => ({
userId: extra.session?.userId ?? 'anonymous',
db: getDatabaseInstance(),
}),
});extra is the MCP SDK's RequestHandlerExtra — it carries session (from SSE/WebSocket transports) and signal (the cancellation AbortSignal).
Because the factory is async and runs per-request, you can resolve dynamically renewing values: refreshed OAuth tokens, connection pools, per-tenant config.
NOTE
Pass extra.signal through to your database queries and HTTP calls for cooperative cancellation. See Cancellation for details.
Multi-Tenant Context
Resolve the tenant in the factory. Every handler downstream receives isolated state:
registry.attachToServer(server, {
contextFactory: async (extra) => {
const token = extra.session?.authToken;
const claims = await verifyJwt(token);
const tenant = await loadTenant(claims.tenantId);
return {
userId: claims.sub,
tenantId: claims.tenantId,
db: getTenantDatabase(tenant.databaseUrl),
permissions: claims.permissions,
};
},
});Middleware Derivation
Middleware can add properties to ctx. The returned object merges into context for all downstream handlers:
const requireAuth = f.middleware(async (ctx) => {
if (!ctx.userId || ctx.userId === 'anonymous') {
throw new Error('Authentication required');
}
const user = await ctx.db.users.findUnique({ where: { id: ctx.userId } });
return { role: user.role, email: user.email };
});After this middleware runs, handlers see ctx.role and ctx.email alongside the base AppContext properties. See Middleware for composition patterns and defineMiddleware().
Context in Presenters
Context flows through to Presenter callbacks — .rules(), .suggest(), and .ui() all receive ctx as their second argument:
import { createPresenter, t, suggest } from '@vurb/core';
const InvoicePresenter = createPresenter('Invoice')
.schema({
id: t.string,
amount_cents: t.number.describe('CENTS — divide by 100'),
status: t.enum('paid', 'pending', 'overdue'),
})
.rules((invoice, ctx) => [
'CRITICAL: amount_cents is in CENTS. Divide by 100.',
ctx?.user?.role !== 'admin'
? 'RESTRICTED: Mask exact totals for non-admin users.'
: null,
])
.suggest((invoice, ctx) => {
const hints = [];
if (invoice.status === 'pending') {
hints.push(suggest('billing.pay', 'Process payment'));
}
// Only admins see the void action
if (ctx?.user?.role === 'admin') {
hints.push(suggest('billing.void', 'Void this invoice'));
}
return hints;
});TIP
Context-aware Presenters are powerful for RBAC — different roles see different rules, different suggested actions, and different UI blocks. The same Presenter adapts its output based on who is viewing the data.