Building Tools
Prerequisites
Install Vurb.ts before following this guide: npm install @vurb/core @modelcontextprotocol/sdk zod — or scaffold a project with vurb create.
Quick Example
Everything — input.status, ctx.db, ctx.tenantId — is fully typed, zero annotations. The handler returns raw data; the framework wraps it with success() automatically.
Context Setup
Define the shared state every handler receives. Pass a generic to initVurb() and import f across all tool files:
TIP
The generic parameter flows through every builder, middleware, and Presenter — zero annotations needed downstream.
Semantic Verbs
| Verb | MCP Annotations | When to Use |
|---|---|---|
f.query() | readOnly: true | Fetching data — lists, searches, lookups |
f.action() | Neutral (no flags) | Creating or updating — reversible side effects |
f.mutation() | destructive: true | Deleting, purging, revoking — irreversible |
MCP clients like Claude Desktop read these annotations and show confirmation dialogs before destructive operations — no prompt engineering needed.
Parameter Declaration
Chainable with*() methods replace Zod schemas. Every method generates proper JSON Schema under the hood:
| Method | Required | TypeScript Type |
|---|---|---|
.withString(name, desc) | ✅ | string |
.withOptionalString(name, desc) | ❌ | string | undefined |
.withNumber(name, desc) | ✅ | number |
.withOptionalNumber(name, desc) | ❌ | number | undefined |
.withBoolean(name, desc) | ✅ | boolean |
.withOptionalBoolean(name, desc) | ❌ | boolean | undefined |
.withEnum(name, values, desc) | ✅ | Union of values |
.withOptionalEnum(name, values, desc) | ❌ | Union | undefined |
.withArray(name, type, desc) | ✅ | T[] |
.withOptionalArray(name, type, desc) | ❌ | T[] | undefined |
Bulk Parameters v3.5.0
When a tool has many parameters of the same type, bulk variants accept a Record<string, string>:
TIP
Mix singular and bulk methods freely. Use singular for one-off required fields and bulk for groups of optional filters.
Model-Driven Parameters v3.6.0
When input fields map to a domain entity, .fromModel() reads the Model's fillable profile and generates the schema:
| Operation | Field Optionality | Use Case |
|---|---|---|
'create' | All required | Creating a new entity |
'update' | All optional | Partial updates |
'filter' | All optional | Search / list filters |
AI Instructions
.instructions() injects system-level guidance into the tool description — prompt engineering embedded in the framework:
TIP
Use .instructions() for when to use the tool. Use .describe() for what the tool does. Together they eliminate hallucinated tool calls.
Semantic Overrides & Annotations
.readOnly()readOnlyHint: true — override any verb to declare no side effects..destructive()destructiveHint: true — triggers confirmation dialogs in MCP clients..idempotent()idempotentHint: true — safe to retry, no duplicate side effects.Use .tags('internal', 'admin') for selective tool exposure and .annotations({ openWorldHint: true }) for custom MCP metadata.
Connecting a Presenter
.returns() attaches a Presenter that controls exactly what the agent sees:
The handler returns raw database data. The Presenter strips undeclared fields, validates with Zod, truncates to 50 items, and attaches rules and affordances. See the full Presenter guide.
Middleware — Context Derivation
.use() enriches context before it reaches the handler. Derived properties are automatically typed:
Stack multiple .use() calls for layered derivations (auth → permissions → tenant). See the full Middleware guide.
State Sync — Cache & Invalidation
| Method | Cache Directive | Use When |
|---|---|---|
.cached() | immutable | Reference data — country codes, timezones |
.stale() | no-store | Volatile data — always re-fetch |
.invalidates(...) | Causal signal | Mutations — tell agent what data changed |
See the full State Sync guide for registry-level policies and cross-domain invalidation.
Runtime Guards
.concurrency({ max: 2, queueSize: 5 }).egress(1_000_000)See the Runtime Guards guide for the full reference.
Streaming Progress
Long-running operations report progress via generator handlers:
TIP
The final return goes through the normal Presenter pipeline. yield calls are side-channel progress notifications.
Registering & Serving
TIP
Use autoDiscover() for file-based routing — drop tool files in a directory and they're registered automatically. See Routing & Groups.
Deploy Your Tools
Every tool is transport-agnostic. The same ToolRegistry works on Stdio, SSE, and serverless runtimes.
vurb deploy — global edge, built-in DLP, kill switch.