Andrey GubanovI've been working on Vovk.ts for a few years. It's a back-end layer on top of App Router route...
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:
mcp.fetch as GET/POST. Next passes { params } as the second argument and the SDK reads it as its options, so next build fails with TS2344. Wrap it like above.requireBearerAuth, the verifier must return expiresAt, 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.