registerMcpControllers() function
undefined
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.
Lenient JSON parsing, type coercion, and validation feedback are all embedded automatically — the complete function calling harness that turns unreliable LLM output into 100% correct structured data.
Setup
npm install @typia/mcp @modelcontextprotocol/sdk
npm install typia
npx typia setupFrom TypeScript Class
MCP Server
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 (McpServeror rawServerfrom@modelcontextprotocol/sdk)controllers: Array of controllers created viatypia.llm.controller<Class>()orHttpLlm.controller()preserve: Whentrue, typia tools coexist with existingMcpServer.registerTool()tools. Default isfalse
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
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 namesdocument: Swagger/OpenAPI document (v2.0, v3.0, or v3.1)connection: HTTP connection info includinghostand optionalheaders
The Function Calling Harness
registerMcpControllers() embeds lenient JSON parsing, type coercion, and validation feedback in every tool — all automatically. 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 by Wrtn Technologies ), qwen3-coder-next showed only 6.75% raw function calling success rate on compiler AST types. However, with the complete harness, it reached 100% — across all four tested Qwen models.
Working on compiler AST means working on any type and any use case.
// 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 totalPreserve Mode
Preserve Test
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.