I made Vovk.ts: Next.js route handlers that are also MCP tools, a typed client and OpenAPI

# nextjs# mcp# typescript# ai
I made Vovk.ts: Next.js route handlers that are also MCP tools, a typed client and OpenAPIAndrey Gubanov

I'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;
  });
}
Enter fullscreen mode Exit fullscreen mode

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 };
Enter fullscreen mode Exit fullscreen mode

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.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.
  • With 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.