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

Type Restrictions

Every method’s parameter type must be a keyworded object type with static keys — not a primitive, array, or union. The return type must also be an object type or void. Primitive return types like number or string are not allowed; wrap them in an object (e.g., { value: number }). See typia.llm.application() Restrictions for details.

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

registerMcpControllers() embeds typia.validate<T>() in every tool for automatic argument validation. When validation fails, the error is returned as text content with inline // ❌ comments at each invalid property:

{ "name": "John", "age": "twenty", // ❌ [{"path":"$input.age","expected":"number"}] "email": "not-an-email", // ❌ [{"path":"$input.email","expected":"string & Format<\"email\">"}] "hobbies": "reading" // ❌ [{"path":"$input.hobbies","expected":"Array<string>"}] }

The LLM reads this feedback and self-corrects on the next turn.

In the AutoBe  project (AI-powered backend code generator), qwen3-coder-next showed only 6.75% raw function calling success rate on compiler AST types. However, with validation feedback, it reached 100%.

Working on compiler AST means working on any type and any use case.

AutoBeTest.IExpression
// Compiler AST may be the hardest type structure possible // // Unlimited union types + unlimited depth + recursive references export type IExpression = | IBooleanLiteral | INumericLiteral | IStringLiteral | IArrayLiteralExpression // <- recursive (contains IExpression[]) | IObjectLiteralExpression // <- recursive (contains IExpression) | INullLiteral | IUndefinedKeyword | IIdentifier | IPropertyAccessExpression // <- recursive | IElementAccessExpression // <- recursive | ITypeOfExpression // <- recursive | IPrefixUnaryExpression // <- recursive | IPostfixUnaryExpression // <- recursive | IBinaryExpression // <- recursive (left & right) | IArrowFunction // <- recursive (body is IExpression) | ICallExpression // <- recursive (args are IExpression[]) | INewExpression // <- recursive | IConditionalPredicate // <- recursive (then & else branches) | ... // 30+ expression types total

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