Skip to content

Dynamic Manifest

Prerequisites

Install Vurb.ts before following this guide: npm install @vurb/core @modelcontextprotocol/sdk zod — or scaffold a project with vurb create.

Introduction

When an AI agent connects to your MCP server, it discovers tools via tools/list. But there's no way for it to understand the relationships between tools, which Presenters power which response, or which actions are safe to call in sequence.

The Dynamic Manifest is a live Vurb.ts://manifest.json MCP Resource that describes every tool, action, and presenter on the server — including semantic metadata, schema shapes, and capability flags. An optional RBAC filter strips capabilities per session so unauthorized agents never see hidden tools.

Quick Start

typescript
import { ToolRegistry } from '@vurb/core';

const registry = new ToolRegistry<AppContext>();
registry.registerAll(projectsTool, invoicesTool, adminTool);

registry.attachToServer(server, {
  contextFactory: (extra) => createAppContext(extra),
  serverName: 'my-platform',
  introspection: {
    enabled: process.env.NODE_ENV !== 'production',
  },
});

Clients read the manifest through the standard MCP Resource protocol:

typescript
const manifest = await client.readResource({ uri: 'Vurb.ts://manifest.json' });

Manifest Payload

json
{
  "server": "my-platform",
  "Vurb.ts_version": "1.0.0",
  "architecture": "MVA (Model-View-Agent)",
  "capabilities": {
    "tools": {
      "projects": {
        "description": "Project management. Actions: list, create, archive",
        "tags": ["core", "projects"],
        "actions": {
          "list": {
            "description": "List all projects",
            "destructive": false,
            "idempotent": false,
            "readOnly": true,
            "required_fields": [],
            "returns_presenter": null
          },
          "create": {
            "description": "Create a new project",
            "destructive": false,
            "readOnly": false,
            "required_fields": ["name"],
            "returns_presenter": null
          }
        },
        "input_schema": { "type": "object", "properties": { "..." : "..." } }
      }
    },
    "presenters": {
      "Invoice": {
        "schema_keys": ["id", "total", "client", "status"],
        "ui_blocks_supported": ["item"],
        "has_contextual_rules": false
      }
    }
  }
}

Tool entrydescription (auto-generated), tags (string[]), actions (Record), input_schema (Zod-derived JSON Schema).

Action entrydestructive (boolean), idempotent (boolean), readOnly (boolean), required_fields (string[]), returns_presenter (string | null).

Presenter entryschema_keys (data fields exposed), ui_blocks_supported ('item'/'collection'), has_contextual_rules (dynamic vs static system rules).

RBAC Filtering

The filter callback receives a deep clone of the manifest plus the session context. Mutate freely — each request gets a fresh copy.

Hide entire tools:

typescript
introspection: {
  enabled: true,
  filter: (manifest, ctx) => {
    if (ctx.user.role !== 'admin') {
      delete manifest.capabilities.tools['admin'];
    }
    return manifest;
  },
},

Strip destructive actions:

typescript
filter: (manifest, ctx) => {
  if (ctx.user.role === 'readonly') {
    for (const tool of Object.values(manifest.capabilities.tools)) {
      for (const [key, action] of Object.entries(tool.actions)) {
        if (action.destructive) delete tool.actions[key];
      }
    }
  }
  return manifest;
},

Multi-tenant filtering:

typescript
filter: (manifest, ctx) => {
  const features = ctx.tenant.enabledFeatures;
  if (!features.includes('billing')) {
    delete manifest.capabilities.tools['invoices'];
    delete manifest.capabilities.presenters['Invoice'];
  }
  return manifest;
},

TIP

Use the Dynamic Manifest for developer tooling and agent debugging. Disable it in production with enabled: process.env.NODE_ENV !== 'production' — it exposes internal structure that external agents don't need.

Custom URI

typescript
introspection: {
  enabled: true,
  uri: 'Vurb.ts://v2/capabilities.json',
},

Configuration

typescript
interface IntrospectionConfig<TContext> {
  enabled: boolean;
  uri?: string;                        // default: 'Vurb.ts://manifest.json'
  filter?: (manifest: ManifestPayload, ctx: TContext) => ManifestPayload;
}

introspection.enabled registers the manifest resource. introspection.uri overrides the default Vurb.ts://manifest.json. introspection.filter applies RBAC per session. serverName appears as the server field in the payload (default: 'Vurb.ts-server').

How It Works

text
resources/read (uri = Vurb.ts://manifest.json)


compileManifest(serverName, builders)
    │  Iterates registry builders
    │  Extracts action metadata, tags, schemas
    │  Extracts presenter info via getSchemaKeys(), getUiBlockTypes(), hasContextualRules()

cloneManifest() → deep clone


filter(clone, ctx) → RBAC filtering


JSON response

Compiled per request so late-registered tools always appear. Deep clone before filter so concurrent sessions with different roles never interfere. Presenter metadata extracted via accessors without executing .make().

Dynamic Manifest vs Builder Introspection — Dynamic Manifest is runtime, server-scoped, RBAC-filtered, accessed via MCP Resource protocol. Builder Introspection is development-time, single-builder-scoped, accessed via direct method calls. See Introspection for the builder-level API.