Skip to Content
๐Ÿ”„ MCP Handler ๐Ÿšง

MCP Server

Now that weโ€™ve covered Function Calling, Realtime UI, and Realtime Database Polling, letโ€™s add the final piece and demonstrate that your API is an MCPย  when built with Vovk.ts. Weโ€™ll use the mcp-handlerย  package, which makes it straightforward to create MCP servers on Next.js.

The deriveTools function accepts a resultFormatter option to format a toolโ€™s execution result before itโ€™s returned to the LLM. It also accepts the special string value mcp, which formats the result to meet MCP server expectations. Thatโ€™s all you need to produce MCPโ€‘compatible tool output in the following shape:

{ content: [ { type: 'text', text: 'Result text', }, ], }
import { deriveTools } from 'vovk'; import UserController from '@/modules/user/UserController'; import TaskController from '@/modules/task/TaskController'; const { tools } = deriveTools({ modules: { UserController, TaskController, }, resultFormatter: 'mcp', });

This is where custom operation properties such as x-tool-successMessage and x-tool-errorMessage become especially useful for making outputs more LLMโ€‘friendly.

import { prefix, get, put, post, del, operation } from 'vovk'; import UserService from './UserService'; import { z } from 'zod'; import { BASE_FIELDS } from '@/constants'; import { UserSchema } from '../../../prisma/generated/schemas'; import { withZod } from '@/lib/withZod'; @prefix('users') export default class UserController { @operation({ summary: 'Update user', description: 'Updates an existing user with the provided details, such as their email or name.', 'x-tool-successMessage': 'User updated successfully', 'x-tool-errorMessage': 'Failed to update user', }) @put('{id}') static updateUser = withZod({ body: UserSchema.omit(BASE_FIELDS).partial(), params: UserSchema.pick({ id: true }), handle: async ({ vovk }) => UserService.updateUser(vovk.params().id, await vovk.body()), }); // ... }

This sets the text field to โ€œUser updated successfullyโ€ or โ€œFailed to update user,โ€ and appends the serialized result or error message.

{ content: [ { type: 'text', text: 'User updated successfully\nResult: { id: 1, email: "user@example.com" }', }, ], }

Now create the route file following your chosen MCP libraryโ€™s documentation.

Weโ€™ll use UserController and TaskController as the source of MCP tools. Controller methods run in the same context (rather than over HTTP) because they follow callable handler rules.

src/app/api/mcp/route.ts
import { createMcpHandler } from "mcp-handler"; import { deriveTools, ToModelOutput } from "vovk"; import UserController from "@/modules/user/UserController"; import TaskController from "@/modules/task/TaskController"; import { jsonSchemaObjectToZodRawShape } from "zod-v3-via-v4-from-json-schema"; // TODO: Temporary fix import z from "zod"; const { tools } = deriveTools({ modules: { UserController, TaskController, }, toModelOutput: ToModelOutput.MCP, onExecute: (result, { name }) => console.log(`${name} executed`, result), onError: (e, { name }) => console.error(`Error in ${name}`, e), }); const handler = createMcpHandler( (server) => { tools.forEach(({ title, name, execute, description, inputSchemas }) => { server.registerTool( name, { title, description, inputSchema: inputSchemas as Partial<Record<'body' | 'query' | 'params', z.ZodTypeAny>>, }, execute, ); }); }, {}, { basePath: "/api" }, ); const authorizedHandler = (req: Request) => { const { MCP_ACCESS_KEY } = process.env; const accessKey = new URL(req.url).searchParams.get("mcp_access_key"); if (MCP_ACCESS_KEY && accessKey !== MCP_ACCESS_KEY) { return new Response( "Unable to authorize the MCP request: mcp_access_key query parameter is invalid", { status: 401 }, ); } return handler(req); }; export { authorizedHandler as GET, authorizedHandler as POST };

The code above is fetched from GitHub repository.ย 

Because the server.tool parameters argument expects Zod schemas, convert each toolโ€™s JSON Schema parameters to Zod using zod-from-json-schemaย . This approach works with all supported validation libraries. If you already use Zod to validate incoming requests, you can instead access Zod schemas directly via a toolโ€™s models property, as shown below:

// ... const handler = createMcpHandler( (server) => { tools.forEach(({ name, execute, description, models }) => { server.tool(name, description, models, execute); }); }, {}, { basePath: '/api' } ); // ...

Note that the models property is available only for callable handlers, not RPC modules.

You can also bring in thirdโ€‘party OpenAPI Mixins to extend your MCP server with additional tools.

import { GithubIssuesAPI } from 'vovk-client'; // ... const githubOptions = { init: { headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, 'X-GitHub-Api-Version': '2022-11-28', }, }, }; const { tools } = deriveTools({ modules: { UserController, TaskController, GithubIssuesAPI: [GithubIssuesAPI, githubOptions], }, }); // ...
Last updated on