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.
Validation feedback is embedded automatically.
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
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.
// 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.