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.
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],
},
});
// ...