MCP Server
Now that we’ve covered Function Calling, Real-time UI, and Real-Time 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 createLLMTools 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 { createLLMTools } from "vovk";
import UserController from "@/modules/user/UserController";
import TaskController from "@/modules/task/TaskController";
const { tools } = createLLMTools({
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.
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 { GithubIssuesRPC } from "vovk-client";
// ...
const githubOptions = {
init: {
headers: {
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
"X-GitHub-Api-Version": "2022-11-28",
},
},
};
const { tools } = createLLMTools({
modules: {
UserController,
TaskController,
GithubIssuesRPC: [GithubIssuesRPC, githubOptions],
},
});
// ...