Skip to Content
🐺 Vovk.ts is released. Read the blog post →
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, so inputSchemas, provided by the tool, needs to be casted as z.ZodTypeAny in order to satisfy TypeScript. If another validation library is used, you can convert parameters.properties to Zod schemas manually using z.fromJSONSchema().

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, inputSchemas }) => { server.registerTool( name, { title, description, inputSchema: inputSchemas as Partial< Record<"body" | "query" | "params", z.ZodTypeAny> >, }, execute, ); }); }, {}, { basePath: "/api" }, ); export { handler as GET, handler as POST };

See Realtime UI / MCP for more info.

Last updated on