Skip to content

Enterprise Quickstart

A production-grade MCP server with JWT authentication, tenant isolation, field-level data protection, audit logging, and cognitive affordances. Uses @vurb/oauth for OAuth Device Flow (RFC 8628). About 5 minutes of work.

By the end, unauthenticated requests are rejected before any handler runs. A viewer-role agent receives user records without email addresses. An admin-role agent sees everything — same tool, same handler, different perception.

Let your AI agent scaffold this entire pipeline

Vurb.ts ships a SKILL.md — a machine-readable architectural contract. Instead of following these steps manually, point your agent at the spec and prompt:

"Build an MCP server with JWT auth, tenant isolation, role-based
 Presenters that strip PII for viewers, and OAuth Device Flow."

The agent produces the full pipeline — middleware, Presenters with .redactPII(), typed context with tenant scope, error recovery with f.error() — on the first pass. Works with Cursor, Claude Code, GitHub Copilot, Windsurf, and Cline.

If you don't need authentication yet, start with the Lightspeed Quickstart. Every layer below is additive.

The Pipeline

Every tool call executes this pipeline in order:

contextFactory → authMiddleware → handler → Presenter → agent

Each stage has one job. If any stage throws, everything after it is skipped — the handler cannot run if middleware rejects the request.

Step 1 — Scaffold with Lightspeed

bash
vurb create secure-api --vector oauth --transport sse --yes
cd secure-api

The CLI scaffolds a complete project with OAuth middleware, SSE transport, autoDiscover(), Vitest, and pre-configured IDE connections — all dependencies installed. You're ready to code in seconds.

Manual setup?

If you prefer manual setup: npm install @vurb/core @modelcontextprotocol/sdk zod — then follow the Traditional Quickstart.

Step 2 — Define Your Context Type

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

interface AppContext {
  db: PrismaClient;
  user: { id: string; role: 'admin' | 'viewer'; tenantId: string };
}

export const f = initVurb<AppContext>();

The f object provides typed factory methods — f.query(), f.mutation(), f.action(), f.presenter(), f.middleware(), f.registry() — that all inherit AppContext. TypeScript knows ctx.user.tenantId is a string in every handler.

Step 3 — Authentication Middleware

Middleware follows tRPC's context derivation pattern. Your function receives the current ctx, returns an object, and that object is merged via Object.assign. TypeScript infers the resulting type.

If any middleware throws, the handler never executes — runtime guarantee, not convention.

typescript
// src/middleware/auth.ts
export const authMiddleware = f.middleware(async (ctx) => {
  const token = (ctx as any).rawToken;
  if (!token) throw new Error('Missing authentication token');

  const payload = await verifyJWT(token);
  const user = await prisma.user.findUniqueOrThrow({
    where: { id: payload.sub },
  });

  return { db: prisma, user: { id: user.id, role: user.role, tenantId: user.tenantId } };
});

For multiple sequential stages — authentication, then rate limiting, then feature flags — use an array: middleware: [authMiddleware, rateLimiter, featureFlags].

OAuth — Device Authorization Grant

For enterprise environments with an OAuth provider, use the OAuth Device Flow module:

bash
npm install @vurb/oauth
typescript
import { createAuthTool, requireAuth } from '@vurb/oauth';

const auth = createAuthTool<AppContext>({
    clientId: process.env.OAUTH_CLIENT_ID!,
    authorizationEndpoint: 'https://auth.example.com/device/code',
    tokenEndpoint: 'https://auth.example.com/oauth/token',
    tokenManager: { configDir: '.secure-api', envVar: 'SECURE_API_TOKEN' },
    onAuthenticated: (token, ctx) => ctx.client.setToken(token),
});

registry.register(auth);

The agent receives 4 actions — login, complete, status, logout. When an unauthenticated agent calls a protected tool, requireAuth() returns a structured error with recovery hints, enabling the LLM to self-heal by calling auth action=login automatically. See OAuth Guide for full configuration.

Step 4 — The Presenter

Instead of excluding what shouldn't be in the response, declare what should. The Zod schema is an allowlist — anything not declared is stripped by parse():

typescript
// src/presenters/user.presenter.ts
import { createPresenter, t, suggest } from '@vurb/core';

export const UserPresenter = createPresenter('User')
  .schema({
    id:        t.string,
    name:      t.string,
    email:     t.zod.string().email().describe('User email address'),
    role:      t.enum('admin', 'viewer'),
    createdAt: t.string,
  })
  .rules((user, ctx) => [
    'Dates are in ISO 8601 format.',
    (ctx as any).user?.role !== 'admin'
      ? 'Email addresses are included for display only.'
      : null,
  ])
  .suggest((user) => [
    suggest('users.get', 'View user details'),
    suggest('users.update', 'Update user profile'),
  ])
  .limit(50);

The database row has 10+ fields. The agent sees 5. When a developer adds a new column, it doesn't leak unless explicitly added to the schema.

.suggest() gives the agent concrete next-steps instead of hallucinating tool names. .limit() truncates large collections and teaches the agent to use filters.

Step 5 — Tools

typescript
// src/tools/users/list.ts
import { f } from '../../vurb.js';
import { authMiddleware } from '../../middleware/auth.js';
import { UserPresenter } from '../../presenters/user.presenter.js';

export default f.query('users.list')
  .describe('List users in the current tenant')
  .withOptionalNumber('limit', 'Max results (default 20)')
  .withOptionalString('search', 'Search by name')
  .use(authMiddleware)
  .returns(UserPresenter)
  .handle(async (input, ctx) => {
    return ctx.db.user.findMany({
      where: { tenantId: ctx.user.tenantId, ...(input.search ? { name: { contains: input.search } } : {}) },
      take: input.limit ?? 20,
    });
  });

Drop the file in src/tools/users/autoDiscover() registers it automatically. No imports to update. Git diffs stay clean.

The handler has one job — query the database with tenant scope. Authentication is middleware. Column filtering is the Presenter. Collection capping is .limit(). Each concern is independently testable.

Write Tool with Error Recovery

typescript
// src/tools/users/delete.ts
import { f } from '../../vurb.js';
import { authMiddleware } from '../../middleware/auth.js';

export default f.mutation('users.delete')
  .describe('Permanently delete a user account')
  .withString('id', 'User ID to delete')
  .tags('admin')
  .use(authMiddleware)
  .handle(async (input, ctx) => {
    if (ctx.user.role !== 'admin') {
      return f.error('FORBIDDEN', 'Only admin users can delete accounts')
        .suggest('Contact an administrator')
        .actions('users.list', 'users.get')
        .build();
    }
    await ctx.db.user.delete({ where: { id: input.id, tenantId: ctx.user.tenantId } });
    return { deleted: true, id: input.id };
  });

.tags('admin') makes this tool invisible when the registry is filtered with exclude: ['admin']. The agent doesn't waste tokens discovering tools it can't use.

f.error() gives the agent a structured error code, recovery suggestion, and available fallback actions — no blind retries.

Step 6 — Run

bash
vurb dev

vurb dev starts with autoDiscover(), SSE transport, observability, and HMR — edit any tool, middleware, or Presenter and the server reloads instantly. No manual restarts during development. See HMR Dev Server for configuration details.

Connect it to your MCP client:

Cursor — Zero-Click Integration

Already configured — the CLI generates .cursor/mcp.json. Open the project in Cursor and the MCP connection is live.

Claude Code

bash
claude mcp add secure-api npx tsx src/server.ts

Claude Desktop

Add to claude_desktop_config.json:

json
{
  "mcpServers": {
    "secure-api": {
      "command": "npx",
      "args": ["tsx", "src/server.ts"]
    }
  }
}

Windsurf · Cline · VS Code + Copilot

Same JSON format — add to ~/.codeium/windsurf/mcp_config.json (Windsurf), cline_mcp_settings.json (Cline), or .vscode/mcp.json (VS Code Copilot — uses "servers" key).

Step 7 — Deploy to Production

Vinkius Cloud — One Command Deploy

Deploy your enterprise MCP server to Vinkius Cloud's global edge with built-in DLP, kill switch, audit logging, FinOps controls, and a managed MCP token:

bash
vurb deploy

The CLI packages your server, deploys it, and returns a connection token. Every deployment is protected by eight layers of security out of the box. Share the token with any MCP client and they connect instantly — no infrastructure to manage.

bash
# Deploy with a custom server name
vurb deploy --name secure-api

# Deploy to a specific environment
vurb deploy --env production

Learn more about Vinkius Cloud →

TIP

Install the Vinkius extension to monitor your deployed servers directly from VS Code, Cursor, or Windsurf — live connections, logs, DLP events, token management, and tool toggling without leaving your IDE.

Self-Hosted Alternatives

MCP servers were designed for long-lived processes with stateful transports — SSE sessions stored in-memory, persistent WebSocket connections, streaming notifications. Serverless runtimes break every one of those assumptions: stateless isolates, no filesystem, cold starts that re-run Zod reflection on every invocation.

The adapters solve this by splitting work into two phases. Registry compilation — Zod reflection, Presenter compilation, schema generation, middleware resolution — happens once at cold start and is cached at module scope. Warm requests only instantiate an ephemeral McpServer + WebStandardStreamableHTTPServerTransport, route the JSON-RPC call, and return. No reflection, no re-compilation.

text
┌──────────────────────────────────────────────────────┐
│  COLD START (once per isolate/function instance)      │
│  ✓ Zod reflection → cached                           │
│  ✓ Presenter compilation → cached                    │
│  ✓ Schema generation → cached                        │
│  ✓ Middleware resolution → cached                    │
└──────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────┐
│  WARM REQUEST (per invocation — near-zero overhead)   │
│  1. new McpServer()           → ephemeral             │
│  2. new Transport()           → stateless JSON-RPC    │
│  3. contextFactory(req, env)  → per-request context   │
│  4. attachToServer()          → trivial wiring        │
│  5. handleRequest()           → route + execute       │
│  6. server.close()            → cleanup               │
└──────────────────────────────────────────────────────┘

Both adapters use enableJsonResponse: true — pure JSON-RPC request/response over the MCP SDK's native WebStandardStreamableHTTPServerTransport. No SSE sessions to lose, no streaming state to manage, no session leaks across isolates.

Vercel — Next.js App Router

bash
npm install @vurb/vercel
typescript
// app/api/mcp/route.ts
import { vercelAdapter } from '@vurb/vercel';

export const POST = vercelAdapter<AppContext>({
  registry,
  serverName: 'secure-api',
  contextFactory: async (req) => ({
    rawToken: req.headers.get('authorization'),
    dbUrl: process.env.DATABASE_URL!,
  }),
});

// Optional: run on Vercel's global Edge Network (~0ms cold start)
export const runtime = 'edge';

See Vercel Adapter for Edge vs Node.js runtime comparison and full configuration reference.

Cloudflare Workers — Global Edge with D1 & KV

bash
npm install @vurb/cloudflare
typescript
// src/worker.ts
import { cloudflareWorkersAdapter } from '@vurb/cloudflare';

export interface Env { DB: D1Database; CACHE: KVNamespace; API_SECRET: string }

export default cloudflareWorkersAdapter<Env, AppContext>({
  registry,
  serverName: 'secure-api',
  contextFactory: async (req, env) => ({
    db: env.DB,
    tenantId: req.headers.get('x-tenant-id') || 'default',
  }),
});

See Cloudflare Adapter for wrangler configuration and full API reference.

Next Steps

WhatWhere
Understand tool definitions, annotations, Zod schemasBuilding Tools
Shape what the LLM sees with PresentersPresenter Guide
Add auth, rate limiting, loggingMiddleware
Register prompts and dynamic manifestsPrompt Engine
Run the full test harnessTesting
Lock your capability surfaceCapability Governance
Tracing and observabilityObservability