Observability
Prerequisites
Install Vurb.ts before following this guide: npm install @vurb/core @modelcontextprotocol/sdk zod — or scaffold a project with vurb create.
- Introduction
- Quick Start
- Attachment Levels
- createDebugObserver()
- Event Types
- Practical Patterns
- Governance Observability
Introduction
Debugging MCP tool calls is hard — the LLM sends a request, your server processes it through validation, middleware, and handlers, and the response goes back. Without observability, you're blind to what happened between request and response.
Vurb.ts emits structured events at each pipeline stage. When debug is off (default), the hot path has zero runtime overhead — no conditionals, no observer objects, no Date.now() calls. Events only flow when you opt in.
Quick Start
import { ToolRegistry, createDebugObserver } from '@vurb/core';
const registry = new ToolRegistry<AppContext>();
registry.registerAll(projectsTool, usersTool, billingTool);
registry.attachToServer(server, {
contextFactory: (extra) => createAppContext(extra),
debug: createDebugObserver(),
});[Vurb.ts] route projects/list
[Vurb.ts] validate projects/list ✓ 0.2ms
[Vurb.ts] execute projects/list ✓ 14.3msAttachment Levels
Per-tool — attach to a single tool during development:
const tool = f.query('users.list')
.describe('List users')
.handle(async (input, ctx) => { /* ... */ });
// Attach debug after building (via GroupedToolBuilder)
tool.debug(createDebugObserver());Registry-wide — propagate to every registered builder:
registry.enableDebug(createDebugObserver());Server-wide — pass as AttachOptions.debug (calls enableDebug() internally). One entry point, full pipeline visibility.
createDebugObserver()
import { createDebugObserver } from '@vurb/core';
// Default — pretty console.debug output
const observer = createDebugObserver();
// Custom — receive typed DebugEvent objects
const observer = createDebugObserver((event) => {
myTelemetry.record(event.type, {
tool: event.tool,
action: event.action,
timestamp: event.timestamp,
});
});No argument → formats events to console.debug. With a handler function → returns that function directly. Type signature: DebugObserverFn = (event: DebugEvent) => void.
Event Types
Every event has type, tool, action, and timestamp. The type field is the discriminant for exhaustive switch handling.
RouteEvent — first event, call matched to a tool and action:
{ type: 'route', tool: 'projects', action: 'list', timestamp: 1740195418000 }ValidateEvent — after Zod validation, includes duration and pass/fail:
{ type: 'validate', tool: 'projects', action: 'create', valid: true, durationMs: 0.3, timestamp: ... }
{ type: 'validate', tool: 'projects', action: 'create', valid: false, error: 'Validation failed', durationMs: 0.1, timestamp: ... }MiddlewareEvent — when middleware chain starts (skipped when no middleware):
{ type: 'middleware', tool: 'projects', action: 'create', chainLength: 3, timestamp: ... }ExecuteEvent — handler completed, contains total pipeline duration:
{ type: 'execute', tool: 'projects', action: 'list', durationMs: 14.3, isError: false, timestamp: ... }ErrorEvent — unrecoverable routing errors. step indicates where: 'route', 'validate', 'middleware', or 'execute':
{ type: 'error', tool: 'unknown_tool', action: '?', error: 'Unknown tool', step: 'route', timestamp: ... }Pipeline order: route → validate → middleware → execute. Validation failure short-circuits after validate. No middleware → middleware event skipped. Unknown action → only error.
Practical Patterns
Telemetry Integration
const observer = createDebugObserver((event) => {
switch (event.type) {
case 'execute':
histogram.record(event.durationMs, {
tool: event.tool,
action: event.action,
status: event.isError ? 'error' : 'success',
});
break;
case 'error':
errorCounter.add(1, { tool: event.tool, step: event.step });
break;
}
});Latency Alerting
const observer = createDebugObserver((event) => {
if (event.type === 'execute' && event.durationMs > 100) {
console.warn(`Slow handler: ${event.tool}/${event.action} took ${event.durationMs.toFixed(1)}ms`);
}
});Error-Only (Production)
const observer = createDebugObserver((event) => {
if (event.type === 'error') {
logger.error('MCP pipeline error', { tool: event.tool, error: event.error, step: event.step });
}
if (event.type === 'execute' && event.isError) {
logger.warn('Handler returned error', { tool: event.tool, action: event.action });
}
});Governance Observability
The governance stack emits GovernanceEvent objects through the same DebugObserverFn pipeline.
{
type: 'governance',
operation: 'lockfile.generate',
label: 'Generate lockfile for payments-api',
outcome: 'success',
detail: '12 tools, 3 prompts',
durationMs: 4.2,
timestamp: 1740195418000
}operation is one of: 'contract.compile', 'contract.diff', 'digest.compute', 'lockfile.generate', 'lockfile.check', 'lockfile.write', 'lockfile.read', 'attestation.sign', 'attestation.verify', 'entitlement.scan', 'token.profile'. outcome is 'success', 'failure', or 'drift'. detail is optional context.
createGovernanceObserver() wraps governance operations with debug events and optional tracing spans:
import { createGovernanceObserver, createNoopObserver } from 'Vurb.ts/introspection';
const observer = createGovernanceObserver({
debug: createDebugObserver(),
tracer: myOtelTracer, // optional
});
// Zero-overhead passthrough
const noop = createNoopObserver();Usage with observe() / observeAsync():
const lockfile = observer.observe(
'lockfile.generate',
'Generate lockfile for payments-api',
() => generateLockfile('payments-api', contracts, version),
);
const attestation = await observer.observeAsync(
'attestation.sign',
'Sign server digest',
() => attestServerDigest(digest, signer),
);Each call emits a GovernanceEvent to the debug observer and (if configured) creates a tracing span. On failure, the span records the exception.