Skip to content

Migration Guide

Prerequisites

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

Convert an existing raw-SDK MCP server to Vurb.ts incrementally — one domain at a time, without breaking your running server. Typical migration: 15-30 minutes per tool domain.

Checklist

  • [ ] Identify tool clusters by domain
  • [ ] Initialize const f = initVurb<AppContext>()
  • [ ] Convert server.tool() calls to f.query(), f.mutation(), or f.action()
  • [ ] Register in ToolRegistry and attach to server
  • [ ] Verify tools are visible and callable
  • [ ] Move repeated auth to .use() middleware (optional)
  • [ ] Add semantic verbs (query, mutation, action) for MCP annotations
  • [ ] Set up autoDiscover() + createDevServer() (optional — see DX Guide)

Step 1: Identify Tool Clusters

typescript
// Before: 6 separate MCP tools
server.tool('list_projects', { ... }, listProjects);
server.tool('create_project', { ... }, createProject);
server.tool('delete_project', { ... }, deleteProject);
server.tool('list_users', { ... }, listUsers);
server.tool('invite_user', { ... }, inviteUser);
server.tool('remove_user', { ... }, removeUser);

Group by domain — each group becomes individual Fluent API calls with dotted names (projects.list, projects.create):

text
projects → list, create, delete
users    → list, invite, remove

Step 2: Initialize Vurb.ts

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

interface AppContext {
  userId: string;
  db: PrismaClient;
  session: Session;
}

const f = initVurb<AppContext>();

Step 3: Convert Tools

Each server.tool() maps to a semantic verb — f.query() (read-only), f.mutation() (destructive), or f.action() (neutral):

typescript
// read-only → f.query()
const listProjects = f.query('projects.list')
  .describe('List workspace projects')
  .handle(async (input, ctx) => {
    return await ctx.db.project.findMany();
  });

// neutral → f.action()
const createProject = f.action('projects.create')
  .describe('Create a project')
  .withString('name', 'Project name (1-100 chars)')
  .handle(async (input, ctx) => {
    return await ctx.db.project.create({
      data: { name: input.name, ownerId: ctx.userId },
    });
  });

// destructive → f.mutation()
const deleteProject = f.mutation('projects.delete')
  .describe('Delete a project')
  .withString('project_id', 'Project ID')
  .handle(async (input, ctx) => {
    await ctx.db.project.delete({ where: { id: input.project_id } });
  });

TIP

Semantic verbs set MCP annotations automatically — f.query() adds readOnlyHint: true, f.mutation() adds destructiveHint: true. No manual annotation required.

For more complex tools, see Building Tools which covers all three APIs (f.query(), createTool(), defineTool()).

Step 4: Register and Attach

typescript
const registry = f.registry();
registry.registerAll(listProjects, createProject, deleteProject);

registry.attachToServer(server, {
  contextFactory: async (extra) => ({
    userId: extra.session.userId,
    db: prisma,
    session: extra.session,
  }),
});

contextFactory runs on every request — resolve auth, create DB sessions, inject tenant info.

Step 5: Verify

Quick check — tool count:

typescript
console.log(`Registered: ${registry.size} tools`);
for (const tool of registry.getAllTools()) {
  console.log(`  ${tool.name} — ${tool.description}`);
}

Smoke test — direct .execute():

typescript
const result = await listProjects.execute(
  { userId: 'test', db: prisma, session: mockSession },
  {},
);
console.log(result);

Runs the full pipeline (validation → middleware → handler) without an MCP server.

Integration test — createVurbTester:

typescript
import { createVurbTester } from 'Vurb.ts/testing';

const tester = createVurbTester(registry, {
  contextFactory: () => ({
    userId: 'test-user',
    db: prisma,
    session: mockSession,
  }),
});

const result = await tester.callAction('projects', 'list');
console.log(result.data);        // parsed response data
console.log(result.systemRules); // Presenter rules (if any)
console.log(result.isError);     // false if successful

Runs the full pipeline — Zod validation, middleware, handler, Presenter — without an MCP server or transport.

Key Differences

ConceptRaw MCP SDKVurb.ts
Tool count1 per action1 per domain, or individual f.query() / f.mutation()
ContextManual / globalinitVurb<T>() — type once
ValidationManual JSON SchemaAuto from Zod, JSON descriptors, or Standard Schema
DescriptionHand-writtenAuto-generated 3-layer
AnnotationsManual per-toolf.query() = readOnly, f.mutation() = destructive
Error handlingAd-hocf.error(), Result<T>
MiddlewareNone.use() + pre-compiled chains
TestingRequires MCP serverDirect .execute() or createVurbTester
File routingNoneautoDiscover()
Hot-reloadRestart entire servercreateDevServer() HMR