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
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
Validation Test
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),
);
};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.
| Components | typia | TypeBox | ajv | io-ts | zod | C.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.meansclass-validator
This validation feedback strategy also covers restriction properties:
string:minLength,maxLength,pattern,format,contentMediaTypenumber:minimum,maximum,exclusiveMinimum,exclusiveMaximum,multipleOfarray:minItems,maxItems,uniqueItems,items
Preserve 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.