Skip to Content
MCP Formatting

MCP (Model Context Protocol) Output Formatting

Derived tools can be used as MCP tools with ToModelOutput.MCP formatter. It formats the tool output to meet MCP tool specification , supporting the folowing types of outputs: text, image, audio, alongside with meta information, described with annotations object.

const { tools } = deriveTools({ modules: { UserController }, toModelOutput: ToModelOutput.MCP, });

JSON Content

JSON responses (including ones created with Response or NextResponse) will be formatted as text content with structuredContent field.

export default class UserController { @get('{id}') static getUser = procedure().handle(async (req, { id }) => { return { hello: 'world' }; }); }

When the tool is executed with ToModelOutput.MCP formatter, the output will be:

{ "content": [ { "type": "text", "text": "{\"hello\":\"world\"}", } ], "structuredContent": { "hello": "world" } }

Audio and Image Content

If procedure returns a Response with Content-Type header set to audio/* or image/*, the output will be formatted accordingly.

export default class MediaController { @get('image') static getImage = procedure().handle(() => { return new Response(buffer, { headers: { 'Content-Type': 'image/png' }, }); }); }

When the tool is executed with ToModelOutput.MCP formatter, the output will be:

{ "content": [ { "type": "image", "data": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...", "mimeType": "image/png" } ] }

The response can be created with toDownloadResponse utility, or using fetch to get media from an external source.

import { toDownloadResponse } from 'vovk'; export default class MediaController { @get('audio') static getAudio = procedure().handle(() => { return toDownloadResponse(buffer, { contentType: 'audio/mpeg' }); }); }
export default class MediaController { @get('from-url') static getAudioFromURL = procedure().handle(() => { return fetch('https://example.com/audio.mp3'); }); }

Text Content

If procedure returns a Response with Content-Type header set to text/* or other text-based types, such as XML, the output will be formatted as text content.

export default class TextController { @get('greeting') static getGreeting = procedure().handle(() => { return new Response('Hello, world!', { headers: { 'Content-Type': 'text/plain' }, }); }); }

When the tool is executed with ToModelOutput.MCP formatter, the output will be:

{ "content": [ { "type": "text", "text": "Hello, world!" } ] }

annotations

Annotations can be added to the output by setting a special metadata key mcpOutput using req.vovk.meta function. This approach makes sure that if the procedure used as an endpoint, the response will not be affected.

export default class AnnotatedController { @get('annotated-image') static getAnnotatedImage = procedure().handle((req) => { req.vovk.meta({ mcpOutput: { annotations: { audience: ['user'], priority: 5 } }, }); return fetch('https://example.com/image.jpg'); }); }

When the tool is executed with ToModelOutput.MCP formatter, the output will include the annotations object:

{ "content": [ { "type": "image", "data": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD...", "mimeType": "image/jpeg" } ], "annotations": { "audience": ["user"], "priority": 5 } }

Note that mcpOutput metadata key can also override other MCP output properties, including content, structuredContent, and annotations. This might be useful if you want to customize the MCP output without changing the actual procedure response.

MCP Handler Example

With mcp-handler  package, you can create an MCP-compatible API route that is going to control the back-end functionality exposed to MCP clients.

By writing this documentation, mcp-handler supports Zod schemas only. The tool’s merged inputSchema is a single Standard Schema, so we convert its JSON Schema back to a Zod object with z.fromJSONSchema() and pass its .shape — the body/query/params slots — to registerTool.

src/app/api/mcp/route.ts
import { createMcpHandler } from "mcp-handler"; import { deriveTools, ToModelOutput } from "vovk"; import z from "zod"; import UserController from "@/modules/user/UserController"; const { tools } = deriveTools({ modules: { UserController }, toModelOutput: ToModelOutput.MCP, }); const handler = createMcpHandler( (server) => { tools.forEach(({ title, name, execute, description, inputSchema }) => { // `inputSchema` is a single merged Standard Schema; mcp-handler wants a Zod // raw shape, so convert its JSON Schema back to Zod and take the object shape. const shape = inputSchema ? (z.fromJSONSchema(inputSchema["~standard"].jsonSchema.input({ target: "draft-2020-12" })) as z.ZodObject).shape : {}; server.registerTool(name, { title, description, inputSchema: shape }, execute); }); }, {}, { basePath: "/api" }, ); export { handler as GET, handler as POST };

See Realtime Kanban / MCP for more info.

Last updated on