I've been working on Vovk.ts for a few years. It's a back-end layer on top of App Router route handlers: you write a controller with procedures, and each procedure is a normal route handler, a method on a generated fetch client (TypeScript, Python, Rust), an operation in an OpenAPI spec, and an AI tool.
The tool part is what I worked on lately. Two procedures:
import { get, operation, post, prefix, procedure } from 'vovk';
import { z } from 'zod';
const TaskSchema = z.object({ id: z.string(), title: z.string(), done: z.boolean() });
const tasks: z.infer<typeof TaskSchema>[] = [];
@prefix('tasks')
export default class TaskController {
@operation({ summary: 'List tasks', description: 'Returns every task.' })
@get()
static listTasks = procedure({ output: TaskSchema.array() }).handle(() => tasks);
@operation({ summary: 'Create task', description: 'Creates a task with the given title.' })
@post()
static createTask = procedure({
body: z.object({ title: z.string().min(1).describe('What needs doing') }),
output: TaskSchema,
}).handle(async (req) => {
const task = { id: crypto.randomUUID(), title: (await req.vovk.body()).title, done: false };
tasks.push(task);
return task;
});
}
And the MCP route, on the official SDK:
// app/api/mcp/route.ts
import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';
import { deriveTools, ToModelOutput } from 'vovk';
import TaskController from '@/modules/task/task-controller';
const tools = deriveTools({ modules: { TaskController }, toModelOutput: ToModelOutput.MCP });
const mcp = createMcpHandler(() => {
const server = new McpServer({ name: 'my-app', version: '1.0.0' });
tools.forEach(({ title, name, execute, description, inputSchema }) => {
server.registerTool(name, { title, description, inputSchema }, execute);
});
return server;
});
const handler = (req: Request) => mcp.fetch(req);
export { handler as GET, handler as POST };
deriveTools turns each procedure into a plain object: a name (TaskController_createTask), a title and description from @operation, an inputSchema built from the procedure's schemas (here { body: { title } }), and execute, which calls the procedure in-process with the same validation as the HTTP endpoint. The route only registers those objects with the SDK.
After claude mcp add --transport http my-app http://localhost:3000/api/mcp, Claude can create and list tasks.
It's App Router only and needs Node 24.15+ (or Bun / Deno) and Next 15+. The controllers look like NestJS, but there's no DI.
Two things about the MCP SDK I ran into while writing the guide, useful without Vovk.ts too:
- Don't export
mcp.fetchasGET/POST. Next passes{ params }as the second argument and the SDK reads it as its options, sonext buildfails with TS2344. Wrap it like above. - With
requireBearerAuth, the verifier must returnexpiresAt, or every token gets 401 "Token has no expiration time".
Guide, including per-user auth (each user's agent sees only their own data): https://vovk.dev/agent-ready
Repo (MIT): https://github.com/finom/vovk
Feedback welcome, especially from anyone running MCP on Next.js in production.
Top comments (0)