Introspection
Prerequisites
Install Vurb.ts before following this guide: npm install @vurb/core @modelcontextprotocol/sdk zod — or scaffold a project with vurb create.
Introduction
Runtime access to action metadata — registered tools, schemas, middleware coverage, destructive flags. Use it for compliance audits, admin dashboards, test coverage checks, and prompt previews. This is the development-time, single-builder complement to the server-scoped Dynamic Manifest.
Action Keys
After building a tool, call getActionNames() to list available actions:
import { initVurb } from '@vurb/core';
const f = initVurb<AppContext>();
const listUsers = f.query('platform.users_list')
.describe('List all users')
.handle(async (input, ctx) => { /* ... */ });
const banUser = f.mutation('platform.users_ban')
.describe('Ban a user')
.withString('id', 'User ID')
.handle(async (input, ctx) => { /* ... */ });
// Get action names from the built tool
console.log(listUsers.getActionNames()); // ['list']
console.log(banUser.getActionNames()); // ['ban']For GroupedToolBuilder with hierarchical groups, getActionNames() returns the full dotted path: ['users.list', 'users.ban'].
Action Metadata
getActionMetadata() returns parsed metadata for every action:
interface ActionMetadata {
key: string; // 'users.ban' or flat 'delete'
actionName: string; // 'ban'
groupName?: string; // 'users' (undefined for flat)
description?: string;
destructive: boolean;
idempotent: boolean;
readOnly: boolean;
requiredFields: string[]; // extracted from Zod schema
hasMiddleware: boolean;
}requiredFields is extracted from the Zod shape graph via SchemaUtils.getActionRequiredFields() — it inspects the internal Zod structure and pushes non-optional keys.
Use Cases
Compliance Audit
Ensure no destructive action ships without middleware:
function auditDestructiveActions(builder: GroupedToolBuilder<AppContext>) {
const report: Array<{ tool: string; action: string }> = [];
const meta = builder.getActionMetadata();
for (const action of meta) {
if (action.destructive && !action.hasMiddleware) {
report.push({
tool: builder.getName(),
action: action.key
});
}
}
if (report.length > 0) {
console.warn('CRITICAL: DESTRUCTIVE ACTIONS WITHOUT MIDDLEWARE:');
for (const entry of report) {
console.warn(` ${entry.tool} → ${entry.action}`);
}
}
}Admin Dashboard
Map execution scopes into a capabilities view:
function getServerCapabilities(builders: GroupedToolBuilder<AppContext>[]) {
return builders.map(builder => ({
tool: builder.getName(),
tags: builder.getTags(),
actions: builder.getActionMetadata().map(action => ({
key: action.key,
description: action.description ?? '(no description)',
readOnly: action.readOnly,
requiredFields: action.requiredFields,
})),
}));
}Middleware Coverage
Track what percentage of actions have scoped middleware:
function middlewareCoverageReport(builder: GroupedToolBuilder<AppContext>) {
const meta = builder.getActionMetadata();
const withMiddleware = meta.filter(a => a.hasMiddleware).length;
console.log(`Coverage: ${Math.round(withMiddleware / meta.length * 100)}%`);
const unprotected = meta.filter(a => !a.hasMiddleware);
for (const action of unprotected) {
if (action.destructive) {
console.warn(`CRITICAL UNPROTECTED: ${action.key}`);
}
}
}Test Coverage
Compare deployed action keys against tested ones:
import { describe, it } from 'vitest';
function ensureTestCoverage(builder: GroupedToolBuilder<AppContext>, testedActions: string[]) {
const allActions = builder.getActionNames();
const untested = allActions.filter(name => !testedActions.includes(name));
if (untested.length > 0) {
throw new Error(
`Missing coverage for deployed endpoints: ${untested.join(', ')}`
);
}
}
describe('platform tool', () => {
const testedActions: string[] = [];
it('users.list works', async () => {
testedActions.push('users.list');
// ...
});
it('all actions have test coverage', () => {
ensureTestCoverage(platformBuilder, testedActions);
});
});Build-Time Prompt Preview
previewPrompt() shows the exact MCP payload the LLM receives — no server needed:
const projects = f.query('projects.list')
.describe('List workspace projects')
.withString('workspace_id', 'Workspace ID')
.handle(async (input, ctx) => { /* ... */ });
console.log(projects.previewPrompt());Output includes tool name, action count, tags, description, JSON Schema, annotations, and token estimate (~185 tokens / 740 chars in this example). Auto-calls buildToolDefinition() if not yet built.
Use it for token budgeting, prompt grammar checks, schema validation, and TOON comparison (.toonDescription()).
TIP
Run previewPrompt() in a test suite to catch schema regressions and token budget overflows before deployment.
Connection to the Engine
Introspection data is the same mapping engine that produces LLM payloads. The destructive flag in metadata identically matches how AnnotationAggregator sets destructiveHint on the wire.