Skip to Content

registerMcpControllers() function

undefined

@typia/mcp
export function registerMcpControllers(props: { server: McpServer | Server; controllers: Array<ILlmController | IHttpLlmController>; preserve?: boolean | undefined; }): void;

MCP (Model Context Protocol)  integration for typia.

registerMcpControllers() converts TypeScript classes or OpenAPI documents into MCP tools at once.

Every class method becomes a tool, JSDoc comments become tool descriptions, and TypeScript types become JSON schemas — all at compile time. For OpenAPI documents, every API endpoint is converted to an MCP tool with schemas from the specification.

Validation feedback is embedded automatically.

Setup

Terminal
npm install @typia/mcp @modelcontextprotocol/sdk npm install typia npx typia setup

From TypeScript Class

src/main.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { registerMcpControllers } from "@typia/mcp"; import typia from "typia"; import { BbsArticleService } from "./BbsArticleService"; import { Calculator } from "./Calculator"; const server: McpServer = new McpServer({ name: "my-server", version: "1.0.0", }); registerMcpControllers({ server, controllers: [ typia.llm.controller<Calculator>("calculator", new Calculator()), typia.llm.controller<BbsArticleService>( "bbs", new BbsArticleService(), ), ], });

Create controllers from TypeScript classes with typia.llm.controller<Class>(), and pass them to registerMcpControllers().

  • server: Target MCP server instance (McpServer or raw Server from @modelcontextprotocol/sdk)
  • controllers: Array of controllers created via typia.llm.controller<Class>() or HttpLlm.controller()
  • preserve: When true, typia tools coexist with existing McpServer.registerTool() tools. Default is false

From OpenAPI Document

src/main.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { registerMcpControllers } from "@typia/mcp"; import { HttpLlm } from "@typia/utils"; const server: McpServer = new McpServer({ name: "my-server", version: "1.0.0", }); registerMcpControllers({ server, controllers: [ HttpLlm.controller({ name: "shopping", document: await fetch( "https://shopping-be.wrtn.ai/editor/swagger.json", ).then((r) => r.json()), connection: { host: "https://shopping-be.wrtn.ai", headers: { Authorization: "Bearer ********" }, }, }), ], });

Create controllers from OpenAPI documents with HttpLlm.controller(), and pass them to registerMcpControllers().

  • name: Controller name used as prefix for tool names
  • document: Swagger/OpenAPI document (v2.0, v3.0, or v3.1)
  • connection: HTTP connection info including host and optional headers

Validation Feedback

test_mcp_class_controller_validation.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { TestValidator } from "@nestia/e2e"; import { ILlmController, IValidation } from "@typia/interface"; import { registerMcpControllers } from "@typia/mcp"; import { stringifyValidationFailure } from "@typia/utils"; import typia from "typia"; import { Calculator } from "../structures/Calculator"; export const test_mcp_class_controller_validation = async (): Promise<void> => { // 1. Create class-based controller using typia.llm.controller const controller: ILlmController<Calculator> = typia.llm.controller<Calculator>("calculator", new Calculator()); // 2. Create McpServer with tools capability const mcpServer: McpServer = new McpServer( { name: "test-server", version: "1.0.0", }, { capabilities: { tools: {}, }, }, ); // 3. Register controller registerMcpControllers({ server: mcpServer, controllers: [controller], preserve: false, }); // 4. Get tools/call handler const rawServer: Server = mcpServer.server; const requestHandlers: Map<string, Function> = (rawServer as any) ._requestHandlers; const callHandler: Function = requestHandlers.get("tools/call")!; // 5. Test validation failure: wrong type (string instead of number) const result: CallToolResult = await callHandler( { method: "tools/call", params: { name: "add", arguments: { x: "not a number", y: 5, }, }, }, { signal: new AbortController().signal }, ); const expected: IValidation = typia.validate<Calculator.IProps>({ x: "not a number", y: 5, }); if (expected.success === true) throw new Error("Expected validation to fail, but it succeeded."); const message: string = stringifyValidationFailure(expected); TestValidator.predicate( "Validation failure", () => expected.success === false && result.content.some((x) => x.type === "text" && x.text === message), ); };
validation-feedback-concept.ts
import { ILlmApplication, ILlmFunction, IValidation } from "@samchon/openapi"; import { FunctionCall } from "pseudo"; export const correctFunctionCall = (props: { functionCall: FunctionCall; application: ILlmApplication<"chatgpt">; retry: (reason: string, errors?: IValidation.IError[]) => Promise<unknown>; }): Promise<unknown> => { // FIND FUNCTION const func: ILlmFunction<"chatgpt"> | undefined = props.application.functions.find((f) => f.name === call.name); if (func === undefined) { // never happened in my experience return props.retry( "Unable to find the matched function name. Try it again.", ); } // VALIDATE const result: IValidation<unknown> = func.validate( props.functionCall.arguments, ); if (result.success === false) { // 1st trial: 30% (gpt-4o-mini in shopping mall chatbot) // 2nd trial with validation feedback: 99% // 3nd trial with validation feedback again: never have failed return props.retry( "Type errors are detected. Correct it through validation errors", { errors: result.errors, }, ); } return result.data; };

When LLM sends { x: "not a number", y: 5 }, the validation failure is returned as text content via stringifyValidationFailure(), including the exact path, expected type, and actual value. The LLM reads this and self-corrects on the next turn.

In my experience, OpenAI gpt-4o-mini makes type-level mistakes about 70% of the time on complex schemas (Shopping Mall service). With validation feedback, the success rate jumps from 30% to 99% on the second attempt. Third attempt has never failed.

The embedded typia.validate<T>() creates validation logic by analyzing TypeScript source codes and types at the compilation level — more accurate and detailed than any runtime validator.

ComponentstypiaTypeBoxajvio-tszodC.V.
Easy to use
Object (simple) 
Object (hierarchical) 
Object (recursive) 
Object (union, implicit) 
Object (union, explicit) 
Object (additional tags) 
Object (template literal types) 
Object (dynamic properties) 
Array (rest tuple) 
Array (hierarchical) 
Array (recursive) 
Array (recursive, union) 
Array (R+U, implicit) 
Array (repeated) 
Array (repeated, union) 
Ultimate Union Type

C.V. means class-validator

This validation feedback strategy also covers restriction properties:

  • string: minLength, maxLength, pattern, format, contentMediaType
  • number: minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf
  • array: minItems, maxItems, uniqueItems, items

Preserve Mode

test_mcp_class_controller_preserve.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { TestValidator } from "@nestia/e2e"; import { ILlmController } from "@typia/interface"; import { registerMcpControllers } from "@typia/mcp"; import typia from "typia"; import { z } from "zod"; import { Calculator } from "../structures/Calculator"; export const test_mcp_class_controller_preserve = async (): Promise<void> => { // 1. Create class-based controller using typia.llm.controller const controller: ILlmController<Calculator> = typia.llm.controller<Calculator>("calculator", new Calculator()); // 2. Create McpServer with tools capability const mcpServer: McpServer = new McpServer( { name: "test-server", version: "1.0.0", }, { capabilities: { tools: {}, }, }, ); // 3. Register existing tool via McpServer.registerTool() with zod schema const inputSchema: z.ZodObject<{ message: z.ZodString }> = z.object({ message: z.string().describe("Message to echo"), }); (mcpServer.registerTool as Function)( "echo", { description: "Echo a message", inputSchema, }, async (args: z.infer<typeof inputSchema>) => ({ content: [ { type: "text", text: args.message, }, ], }), ); // 4. Register with preserve: true registerMcpControllers({ server: mcpServer, controllers: [controller], preserve: true, }); // 5. Verify private API IS used const registeredTools: Record<string, unknown> = (mcpServer as any)._registeredTools ?? {}; const handlersInitialized: boolean = (mcpServer as any) ._toolHandlersInitialized; TestValidator.equals( "existing echo tool should be present", Object.keys(registeredTools).includes("echo"), true, ); TestValidator.equals( "_toolHandlersInitialized should be true", handlersInitialized, true, ); // 6. Call tools/list to verify all tools (echo + calculator) const rawServer: Server = mcpServer.server; const requestHandlers: Map<string, Function> = (rawServer as any) ._requestHandlers; const listHandler: Function = requestHandlers.get("tools/list")!; const result: { tools: { name: string }[] } = await listHandler( { method: "tools/list", params: {}, }, { signal: new AbortController().signal, }, ); const toolNames: string[] = result.tools.map((t) => t.name).sort(); // Should have 5 tools: echo + add, subtract, multiply, divide TestValidator.equals("should have 5 tools total", result.tools.length, 5); TestValidator.predicate( "should include echo tool", toolNames.includes("echo"), ); TestValidator.predicate("should include add tool", toolNames.includes("add")); TestValidator.predicate( "should include subtract tool", toolNames.includes("subtract"), ); TestValidator.predicate( "should include multiply tool", toolNames.includes("multiply"), ); TestValidator.predicate( "should include divide tool", toolNames.includes("divide"), ); };

By default, registerMcpControllers() replaces the MCP server’s tool handlers (standalone mode). Set preserve: true to coexist with McpServer.registerTool().

In the above test, the "echo" tool registered via McpServer.registerTool() and the calculator tools registered via registerMcpControllers() work together — 5 tools total. If duplicate names are detected, it throws at registration time.

Last updated on