@typia/mcp 12.0.0-dev.20260307-2 → 12.0.0-dev.20260310

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.d.ts CHANGED
@@ -8,9 +8,9 @@ import { IHttpLlmController, ILlmController } from "@typia/interface";
8
8
  * OpenAPI operations via `HttpLlm.controller()` as MCP tools.
9
9
  *
10
10
  * Every tool call is validated by typia. If LLM provides invalid arguments,
11
- * returns {@link IValidation.IFailure} formatted by
12
- * {@link LlmJson.stringify} so that LLM can correct them automatically.
13
- * Below is an example of the validation error format:
11
+ * returns {@link IValidation.IFailure} formatted by {@link LlmJson.stringify} so
12
+ * that LLM can correct them automatically. Below is an example of the
13
+ * validation error format:
14
14
  *
15
15
  * ```json
16
16
  * {
package/lib/index.js CHANGED
@@ -9,9 +9,9 @@ const McpControllerRegistrar_1 = require("./internal/McpControllerRegistrar");
9
9
  * OpenAPI operations via `HttpLlm.controller()` as MCP tools.
10
10
  *
11
11
  * Every tool call is validated by typia. If LLM provides invalid arguments,
12
- * returns {@link IValidation.IFailure} formatted by
13
- * {@link LlmJson.stringify} so that LLM can correct them automatically.
14
- * Below is an example of the validation error format:
12
+ * returns {@link IValidation.IFailure} formatted by {@link LlmJson.stringify} so
13
+ * that LLM can correct them automatically. Below is an example of the
14
+ * validation error format:
15
15
  *
16
16
  * ```json
17
17
  * {
package/lib/index.mjs CHANGED
@@ -7,9 +7,9 @@ import { McpControllerRegistrar } from './internal/McpControllerRegistrar.mjs';
7
7
  * OpenAPI operations via `HttpLlm.controller()` as MCP tools.
8
8
  *
9
9
  * Every tool call is validated by typia. If LLM provides invalid arguments,
10
- * returns {@link IValidation.IFailure} formatted by
11
- * {@link LlmJson.stringify} so that LLM can correct them automatically.
12
- * Below is an example of the validation error format:
10
+ * returns {@link IValidation.IFailure} formatted by {@link LlmJson.stringify} so
11
+ * that LLM can correct them automatically. Below is an example of the
12
+ * validation error format:
13
13
  *
14
14
  * ```json
15
15
  * {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typia/mcp",
3
- "version": "12.0.0-dev.20260307-2",
3
+ "version": "12.0.0-dev.20260310",
4
4
  "description": "MCP (Model Context Protocol) integration for typia",
5
5
  "main": "lib/index.js",
6
6
  "exports": {
@@ -23,8 +23,8 @@
23
23
  "homepage": "https://typia.io",
24
24
  "dependencies": {
25
25
  "zod-to-json-schema": ">=3.24.0",
26
- "@typia/interface": "^12.0.0-dev.20260307-2",
27
- "@typia/utils": "^12.0.0-dev.20260307-2"
26
+ "@typia/utils": "^12.0.0-dev.20260310",
27
+ "@typia/interface": "^12.0.0-dev.20260310"
28
28
  },
29
29
  "peerDependencies": {
30
30
  "@modelcontextprotocol/sdk": ">=1.0.0"
package/src/index.ts CHANGED
@@ -1,67 +1,67 @@
1
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
- import { IHttpLlmController, ILlmController } from "@typia/interface";
4
-
5
- import { McpControllerRegistrar } from "./internal/McpControllerRegistrar";
6
-
7
- /**
8
- * Register MCP tools from controllers.
9
- *
10
- * Registers TypeScript class methods via `typia.llm.controller<Class>()` or
11
- * OpenAPI operations via `HttpLlm.controller()` as MCP tools.
12
- *
13
- * Every tool call is validated by typia. If LLM provides invalid arguments,
14
- * returns {@link IValidation.IFailure} formatted by
15
- * {@link LlmJson.stringify} so that LLM can correct them automatically.
16
- * Below is an example of the validation error format:
17
- *
18
- * ```json
19
- * {
20
- * "name": "John",
21
- * "age": "twenty", // ❌ [{"path":"$input.age","expected":"number & Type<\"uint32\">"}]
22
- * "email": "not-an-email", // ❌ [{"path":"$input.email","expected":"string & Format<\"email\">"}]
23
- * "hobbies": "reading" // ❌ [{"path":"$input.hobbies","expected":"Array<string>"}]
24
- * }
25
- * ```
26
- *
27
- * If you use `McpServer.registerTool()` instead, you have to define Zod schema,
28
- * function name, and description string manually for each tool. Also, without
29
- * typia's validation feedback, LLM cannot auto-correct its mistakes, which
30
- * significantly degrades tool calling performance.
31
- *
32
- * @param props Registration properties
33
- */
34
- export function registerMcpControllers(props: {
35
- /**
36
- * Target MCP server to register tools.
37
- *
38
- * Both {@link McpServer} and raw {@link Server} are supported. To combine with
39
- * `McpServer.registerTool()`, set `preserve: true`.
40
- */
41
- server: McpServer | Server;
42
-
43
- /**
44
- * List of controllers to register as MCP tools.
45
- *
46
- * - {@link ILlmController}: from `typia.llm.controller<Class>()`, registers all
47
- * methods of the class as tools
48
- * - {@link IHttpLlmController}: from `HttpLlm.controller()`, registers all
49
- * operations from OpenAPI document as tools
50
- */
51
- controllers: Array<ILlmController | IHttpLlmController>;
52
-
53
- /**
54
- * Preserve existing tools registered via `McpServer.registerTool()`.
55
- *
56
- * If `true`, typia tools coexist with existing McpServer tools. This uses MCP
57
- * SDK's internal (private) API which may break on SDK updates.
58
- *
59
- * If `false`, typia tools completely replace the tool handlers, ignoring any
60
- * tools registered via `McpServer.registerTool()`.
61
- *
62
- * @default false
63
- */
64
- preserve?: boolean | undefined;
65
- }): void {
66
- return McpControllerRegistrar.register(props);
67
- }
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { IHttpLlmController, ILlmController } from "@typia/interface";
4
+
5
+ import { McpControllerRegistrar } from "./internal/McpControllerRegistrar";
6
+
7
+ /**
8
+ * Register MCP tools from controllers.
9
+ *
10
+ * Registers TypeScript class methods via `typia.llm.controller<Class>()` or
11
+ * OpenAPI operations via `HttpLlm.controller()` as MCP tools.
12
+ *
13
+ * Every tool call is validated by typia. If LLM provides invalid arguments,
14
+ * returns {@link IValidation.IFailure} formatted by {@link LlmJson.stringify} so
15
+ * that LLM can correct them automatically. Below is an example of the
16
+ * validation error format:
17
+ *
18
+ * ```json
19
+ * {
20
+ * "name": "John",
21
+ * "age": "twenty", // ❌ [{"path":"$input.age","expected":"number & Type<\"uint32\">"}]
22
+ * "email": "not-an-email", // ❌ [{"path":"$input.email","expected":"string & Format<\"email\">"}]
23
+ * "hobbies": "reading" // ❌ [{"path":"$input.hobbies","expected":"Array<string>"}]
24
+ * }
25
+ * ```
26
+ *
27
+ * If you use `McpServer.registerTool()` instead, you have to define Zod schema,
28
+ * function name, and description string manually for each tool. Also, without
29
+ * typia's validation feedback, LLM cannot auto-correct its mistakes, which
30
+ * significantly degrades tool calling performance.
31
+ *
32
+ * @param props Registration properties
33
+ */
34
+ export function registerMcpControllers(props: {
35
+ /**
36
+ * Target MCP server to register tools.
37
+ *
38
+ * Both {@link McpServer} and raw {@link Server} are supported. To combine with
39
+ * `McpServer.registerTool()`, set `preserve: true`.
40
+ */
41
+ server: McpServer | Server;
42
+
43
+ /**
44
+ * List of controllers to register as MCP tools.
45
+ *
46
+ * - {@link ILlmController}: from `typia.llm.controller<Class>()`, registers all
47
+ * methods of the class as tools
48
+ * - {@link IHttpLlmController}: from `HttpLlm.controller()`, registers all
49
+ * operations from OpenAPI document as tools
50
+ */
51
+ controllers: Array<ILlmController | IHttpLlmController>;
52
+
53
+ /**
54
+ * Preserve existing tools registered via `McpServer.registerTool()`.
55
+ *
56
+ * If `true`, typia tools coexist with existing McpServer tools. This uses MCP
57
+ * SDK's internal (private) API which may break on SDK updates.
58
+ *
59
+ * If `false`, typia tools completely replace the tool handlers, ignoring any
60
+ * tools registered via `McpServer.registerTool()`.
61
+ *
62
+ * @default false
63
+ */
64
+ preserve?: boolean | undefined;
65
+ }): void {
66
+ return McpControllerRegistrar.register(props);
67
+ }
@@ -1,330 +1,330 @@
1
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
- import {
4
- CallToolRequestSchema,
5
- CallToolResult,
6
- ListToolsRequestSchema,
7
- Tool,
8
- } from "@modelcontextprotocol/sdk/types.js";
9
- import {
10
- IHttpLlmController,
11
- IHttpLlmFunction,
12
- ILlmController,
13
- ILlmFunction,
14
- IValidation,
15
- } from "@typia/interface";
16
- import { HttpLlm, LlmJson } from "@typia/utils";
17
- import { ZodObject, ZodType } from "zod";
18
- import { zodToJsonSchema } from "zod-to-json-schema";
19
-
20
- export namespace McpControllerRegistrar {
21
- export const register = (props: {
22
- server: McpServer | Server;
23
- controllers: Array<ILlmController | IHttpLlmController>;
24
- preserve?: boolean | undefined;
25
- }): void => {
26
- // McpServer wraps raw Server - we need raw Server for JSON Schema support
27
- const server: Server =
28
- "server" in props.server
29
- ? (props.server as McpServer).server
30
- : (props.server as Server);
31
-
32
- // Build tool registry from controllers
33
- const registry: Map<string, IToolEntry> = new Map();
34
-
35
- for (const controller of props.controllers) {
36
- if (controller.protocol === "class") {
37
- registerClassController(registry, controller);
38
- } else {
39
- registerHttpController(registry, controller);
40
- }
41
- }
42
-
43
- // Determine preserve mode (default: false)
44
- const preserve: boolean = props.preserve ?? false;
45
-
46
- if (preserve) {
47
- // PRESERVE MODE: Coexist with McpServer.registerTool()
48
- // Uses MCP SDK internal API (_registeredTools, _toolHandlersInitialized)
49
- registerWithPreserve(server, registry, props.server);
50
- } else {
51
- // STANDALONE MODE: Typia tools only, no private API dependency
52
- registerStandalone(server, registry);
53
- }
54
- };
55
-
56
- /**
57
- * Standalone registration without private API. Typia tools completely replace
58
- * any existing tool handlers.
59
- */
60
- const registerStandalone = (
61
- server: Server,
62
- registry: Map<string, IToolEntry>,
63
- ): void => {
64
- // tools/list handler
65
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
66
- tools: Array.from(registry.values()).map((entry: IToolEntry) => ({
67
- name: entry.function.name,
68
- description: entry.function.description,
69
- inputSchema: {
70
- type: "object" as const,
71
- properties: entry.function.parameters.properties,
72
- required: entry.function.parameters.required,
73
- additionalProperties: false,
74
- $defs: entry.function.parameters.$defs,
75
- },
76
- })),
77
- }));
78
-
79
- // tools/call handler
80
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
81
- const name: string = request.params.name;
82
- const args: Record<string, unknown> | undefined =
83
- request.params.arguments;
84
-
85
- const entry: IToolEntry | undefined = registry.get(name);
86
- if (entry !== undefined) {
87
- return handleToolCall(entry, args);
88
- }
89
-
90
- return {
91
- isError: true,
92
- content: [{ type: "text" as const, text: `Unknown tool: ${name}` }],
93
- };
94
- });
95
- };
96
-
97
- /**
98
- * Preserve mode registration with private API. Coexists with tools registered
99
- * via McpServer.registerTool().
100
- */
101
- const registerWithPreserve = (
102
- server: Server,
103
- registry: Map<string, IToolEntry>,
104
- originalServer: McpServer | Server,
105
- ): void => {
106
- // Get McpServer reference for coexistence with McpServer.registerTool()
107
- const mcpServer: McpServer | null =
108
- "server" in originalServer ? (originalServer as McpServer) : null;
109
-
110
- // Helper to get existing tools dynamically (supports tools registered after this call)
111
- const getExistingTools = (): Record<string, any> =>
112
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
113
- mcpServer ? ((mcpServer as any)._registeredTools ?? {}) : {};
114
-
115
- // Check for conflicts with existing McpServer tools at registration time
116
- for (const pair of Object.entries(getExistingTools())) {
117
- if (pair[1].enabled && registry.has(pair[0])) {
118
- throw new Error(
119
- `Duplicate function name "${pair[0]}" between McpServer.registerTool() and controller "${registry.get(pair[0])!.controller}"`,
120
- );
121
- }
122
- }
123
-
124
- // tools/list handler
125
- server.setRequestHandler(ListToolsRequestSchema, async () => {
126
- const existingTools: Record<string, any> = getExistingTools();
127
- return {
128
- tools: [
129
- // Typia controller tools
130
- ...Array.from(registry.values()).map((entry: IToolEntry) => {
131
- return {
132
- name: entry.function.name,
133
- description: entry.function.description,
134
- inputSchema: {
135
- type: "object" as const,
136
- properties: entry.function.parameters.properties,
137
- required: entry.function.parameters.required,
138
- additionalProperties: false,
139
- $defs: entry.function.parameters.$defs,
140
- },
141
- } satisfies Tool;
142
- }),
143
- // Existing McpServer tools
144
- ...Object.entries(existingTools)
145
- .filter(
146
- (pair: [string, any]) =>
147
- !registry.has(pair[0]) && pair[1].enabled,
148
- )
149
- .map((pair: [string, any]) => {
150
- return {
151
- name: pair[0],
152
- description: pair[1].description,
153
- inputSchema: convertZodToJsonSchema(pair[1].inputSchema),
154
- } satisfies Tool;
155
- }),
156
- ],
157
- };
158
- });
159
-
160
- // tools/call handler
161
- server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
162
- const name: string = request.params.name;
163
- const args: Record<string, unknown> | undefined =
164
- request.params.arguments;
165
-
166
- // Check typia registry first
167
- const entry: IToolEntry | undefined = registry.get(name);
168
- if (entry !== undefined) {
169
- return handleToolCall(entry, args);
170
- }
171
-
172
- // Fall back to existing McpServer tools
173
- const existingTools: Record<string, any> = getExistingTools();
174
- const existingTool: any = existingTools[name];
175
- if (existingTool && existingTool.enabled) {
176
- return existingTool.handler(args, extra);
177
- }
178
-
179
- return {
180
- isError: true,
181
- content: [{ type: "text" as const, text: `Unknown tool: ${name}` }],
182
- };
183
- });
184
-
185
- // Mark handlers as initialized to prevent McpServer from overwriting
186
- if (mcpServer) {
187
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
188
- (mcpServer as any)._toolHandlersInitialized = true;
189
- }
190
- };
191
-
192
- const registerClassController = (
193
- registry: Map<string, IToolEntry>,
194
- controller: ILlmController,
195
- ): void => {
196
- const execute: Record<string, unknown> = controller.execute;
197
- for (const func of controller.application.functions) {
198
- const existing: IToolEntry | undefined = registry.get(func.name);
199
- if (existing !== undefined) {
200
- throw new Error(
201
- `Duplicate function name "${func.name}" between controllers "${existing.controller}" and "${controller.name}"`,
202
- );
203
- }
204
-
205
- const method: unknown = execute[func.name];
206
- if (typeof method !== "function") {
207
- throw new Error(
208
- `Method "${func.name}" not found on controller "${controller.name}"`,
209
- );
210
- }
211
-
212
- registry.set(func.name, {
213
- controller: controller.name,
214
- function: func,
215
- execute: async (args: unknown) => method.call(execute, args),
216
- });
217
- }
218
- };
219
-
220
- const registerHttpController = (
221
- registry: Map<string, IToolEntry>,
222
- controller: IHttpLlmController,
223
- ): void => {
224
- const application: IHttpLlmController["application"] =
225
- controller.application;
226
- const connection: IHttpLlmController["connection"] = controller.connection;
227
-
228
- for (const func of application.functions) {
229
- const existing: IToolEntry | undefined = registry.get(func.name);
230
- if (existing !== undefined) {
231
- throw new Error(
232
- `Duplicate function name "${func.name}" between controllers "${existing.controller}" and "${controller.name}"`,
233
- );
234
- }
235
- registry.set(func.name, {
236
- controller: controller.name,
237
- function: func,
238
- execute: async (args: unknown) => {
239
- if (controller.execute !== undefined) {
240
- const response = await controller.execute({
241
- connection,
242
- application,
243
- function: func,
244
- arguments: args as object,
245
- });
246
- return response.body;
247
- }
248
- return HttpLlm.execute({
249
- application,
250
- function: func,
251
- connection,
252
- input: args as object,
253
- });
254
- },
255
- });
256
- }
257
- };
258
-
259
- const handleToolCall = async (
260
- entry: IToolEntry,
261
- args: unknown,
262
- ): Promise<CallToolResult> => {
263
- const coerced: unknown = LlmJson.coerce(args, entry.function.parameters);
264
- const validation: IValidation<unknown> = entry.function.validate(coerced);
265
- if (!validation.success) {
266
- return {
267
- isError: true,
268
- content: [
269
- {
270
- type: "text" as const,
271
- text: LlmJson.stringify(validation),
272
- },
273
- ],
274
- };
275
- }
276
-
277
- try {
278
- const result: unknown = await entry.execute(validation.data);
279
- return {
280
- content: [
281
- {
282
- type: "text" as const,
283
- text:
284
- result === undefined
285
- ? "Success"
286
- : JSON.stringify(result, null, 2),
287
- },
288
- ],
289
- };
290
- } catch (error) {
291
- return {
292
- isError: true,
293
- content: [
294
- {
295
- type: "text" as const,
296
- text:
297
- error instanceof Error
298
- ? `${error.name}: ${error.message}`
299
- : String(error),
300
- },
301
- ],
302
- };
303
- }
304
- };
305
-
306
- const convertZodToJsonSchema = (
307
- zodSchema: ZodType | ZodObject<any> | undefined,
308
- ): Tool["inputSchema"] => {
309
- if (zodSchema === undefined) {
310
- return { type: "object", properties: {} };
311
- }
312
-
313
- // @todo: error TS2589: Type instantiation is excessively deep and possibly infinite.
314
- const converted: object = (zodToJsonSchema as any)(zodSchema);
315
- if (
316
- typeof converted === "object" &&
317
- "type" in converted &&
318
- converted.type === "object"
319
- ) {
320
- return converted as Tool["inputSchema"];
321
- }
322
- return { type: "object", properties: {} };
323
- };
324
- }
325
-
326
- interface IToolEntry {
327
- controller: string;
328
- function: ILlmFunction | IHttpLlmFunction;
329
- execute: (args: unknown) => Promise<unknown>;
330
- }
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import {
4
+ CallToolRequestSchema,
5
+ CallToolResult,
6
+ ListToolsRequestSchema,
7
+ Tool,
8
+ } from "@modelcontextprotocol/sdk/types.js";
9
+ import {
10
+ IHttpLlmController,
11
+ IHttpLlmFunction,
12
+ ILlmController,
13
+ ILlmFunction,
14
+ IValidation,
15
+ } from "@typia/interface";
16
+ import { HttpLlm, LlmJson } from "@typia/utils";
17
+ import { ZodObject, ZodType } from "zod";
18
+ import { zodToJsonSchema } from "zod-to-json-schema";
19
+
20
+ export namespace McpControllerRegistrar {
21
+ export const register = (props: {
22
+ server: McpServer | Server;
23
+ controllers: Array<ILlmController | IHttpLlmController>;
24
+ preserve?: boolean | undefined;
25
+ }): void => {
26
+ // McpServer wraps raw Server - we need raw Server for JSON Schema support
27
+ const server: Server =
28
+ "server" in props.server
29
+ ? (props.server as McpServer).server
30
+ : (props.server as Server);
31
+
32
+ // Build tool registry from controllers
33
+ const registry: Map<string, IToolEntry> = new Map();
34
+
35
+ for (const controller of props.controllers) {
36
+ if (controller.protocol === "class") {
37
+ registerClassController(registry, controller);
38
+ } else {
39
+ registerHttpController(registry, controller);
40
+ }
41
+ }
42
+
43
+ // Determine preserve mode (default: false)
44
+ const preserve: boolean = props.preserve ?? false;
45
+
46
+ if (preserve) {
47
+ // PRESERVE MODE: Coexist with McpServer.registerTool()
48
+ // Uses MCP SDK internal API (_registeredTools, _toolHandlersInitialized)
49
+ registerWithPreserve(server, registry, props.server);
50
+ } else {
51
+ // STANDALONE MODE: Typia tools only, no private API dependency
52
+ registerStandalone(server, registry);
53
+ }
54
+ };
55
+
56
+ /**
57
+ * Standalone registration without private API. Typia tools completely replace
58
+ * any existing tool handlers.
59
+ */
60
+ const registerStandalone = (
61
+ server: Server,
62
+ registry: Map<string, IToolEntry>,
63
+ ): void => {
64
+ // tools/list handler
65
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
66
+ tools: Array.from(registry.values()).map((entry: IToolEntry) => ({
67
+ name: entry.function.name,
68
+ description: entry.function.description,
69
+ inputSchema: {
70
+ type: "object" as const,
71
+ properties: entry.function.parameters.properties,
72
+ required: entry.function.parameters.required,
73
+ additionalProperties: false,
74
+ $defs: entry.function.parameters.$defs,
75
+ },
76
+ })),
77
+ }));
78
+
79
+ // tools/call handler
80
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
81
+ const name: string = request.params.name;
82
+ const args: Record<string, unknown> | undefined =
83
+ request.params.arguments;
84
+
85
+ const entry: IToolEntry | undefined = registry.get(name);
86
+ if (entry !== undefined) {
87
+ return handleToolCall(entry, args);
88
+ }
89
+
90
+ return {
91
+ isError: true,
92
+ content: [{ type: "text" as const, text: `Unknown tool: ${name}` }],
93
+ };
94
+ });
95
+ };
96
+
97
+ /**
98
+ * Preserve mode registration with private API. Coexists with tools registered
99
+ * via McpServer.registerTool().
100
+ */
101
+ const registerWithPreserve = (
102
+ server: Server,
103
+ registry: Map<string, IToolEntry>,
104
+ originalServer: McpServer | Server,
105
+ ): void => {
106
+ // Get McpServer reference for coexistence with McpServer.registerTool()
107
+ const mcpServer: McpServer | null =
108
+ "server" in originalServer ? (originalServer as McpServer) : null;
109
+
110
+ // Helper to get existing tools dynamically (supports tools registered after this call)
111
+ const getExistingTools = (): Record<string, any> =>
112
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
113
+ mcpServer ? ((mcpServer as any)._registeredTools ?? {}) : {};
114
+
115
+ // Check for conflicts with existing McpServer tools at registration time
116
+ for (const pair of Object.entries(getExistingTools())) {
117
+ if (pair[1].enabled && registry.has(pair[0])) {
118
+ throw new Error(
119
+ `Duplicate function name "${pair[0]}" between McpServer.registerTool() and controller "${registry.get(pair[0])!.controller}"`,
120
+ );
121
+ }
122
+ }
123
+
124
+ // tools/list handler
125
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
126
+ const existingTools: Record<string, any> = getExistingTools();
127
+ return {
128
+ tools: [
129
+ // Typia controller tools
130
+ ...Array.from(registry.values()).map((entry: IToolEntry) => {
131
+ return {
132
+ name: entry.function.name,
133
+ description: entry.function.description,
134
+ inputSchema: {
135
+ type: "object" as const,
136
+ properties: entry.function.parameters.properties,
137
+ required: entry.function.parameters.required,
138
+ additionalProperties: false,
139
+ $defs: entry.function.parameters.$defs,
140
+ },
141
+ } satisfies Tool;
142
+ }),
143
+ // Existing McpServer tools
144
+ ...Object.entries(existingTools)
145
+ .filter(
146
+ (pair: [string, any]) =>
147
+ !registry.has(pair[0]) && pair[1].enabled,
148
+ )
149
+ .map((pair: [string, any]) => {
150
+ return {
151
+ name: pair[0],
152
+ description: pair[1].description,
153
+ inputSchema: convertZodToJsonSchema(pair[1].inputSchema),
154
+ } satisfies Tool;
155
+ }),
156
+ ],
157
+ };
158
+ });
159
+
160
+ // tools/call handler
161
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
162
+ const name: string = request.params.name;
163
+ const args: Record<string, unknown> | undefined =
164
+ request.params.arguments;
165
+
166
+ // Check typia registry first
167
+ const entry: IToolEntry | undefined = registry.get(name);
168
+ if (entry !== undefined) {
169
+ return handleToolCall(entry, args);
170
+ }
171
+
172
+ // Fall back to existing McpServer tools
173
+ const existingTools: Record<string, any> = getExistingTools();
174
+ const existingTool: any = existingTools[name];
175
+ if (existingTool && existingTool.enabled) {
176
+ return existingTool.handler(args, extra);
177
+ }
178
+
179
+ return {
180
+ isError: true,
181
+ content: [{ type: "text" as const, text: `Unknown tool: ${name}` }],
182
+ };
183
+ });
184
+
185
+ // Mark handlers as initialized to prevent McpServer from overwriting
186
+ if (mcpServer) {
187
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
188
+ (mcpServer as any)._toolHandlersInitialized = true;
189
+ }
190
+ };
191
+
192
+ const registerClassController = (
193
+ registry: Map<string, IToolEntry>,
194
+ controller: ILlmController,
195
+ ): void => {
196
+ const execute: Record<string, unknown> = controller.execute;
197
+ for (const func of controller.application.functions) {
198
+ const existing: IToolEntry | undefined = registry.get(func.name);
199
+ if (existing !== undefined) {
200
+ throw new Error(
201
+ `Duplicate function name "${func.name}" between controllers "${existing.controller}" and "${controller.name}"`,
202
+ );
203
+ }
204
+
205
+ const method: unknown = execute[func.name];
206
+ if (typeof method !== "function") {
207
+ throw new Error(
208
+ `Method "${func.name}" not found on controller "${controller.name}"`,
209
+ );
210
+ }
211
+
212
+ registry.set(func.name, {
213
+ controller: controller.name,
214
+ function: func,
215
+ execute: async (args: unknown) => method.call(execute, args),
216
+ });
217
+ }
218
+ };
219
+
220
+ const registerHttpController = (
221
+ registry: Map<string, IToolEntry>,
222
+ controller: IHttpLlmController,
223
+ ): void => {
224
+ const application: IHttpLlmController["application"] =
225
+ controller.application;
226
+ const connection: IHttpLlmController["connection"] = controller.connection;
227
+
228
+ for (const func of application.functions) {
229
+ const existing: IToolEntry | undefined = registry.get(func.name);
230
+ if (existing !== undefined) {
231
+ throw new Error(
232
+ `Duplicate function name "${func.name}" between controllers "${existing.controller}" and "${controller.name}"`,
233
+ );
234
+ }
235
+ registry.set(func.name, {
236
+ controller: controller.name,
237
+ function: func,
238
+ execute: async (args: unknown) => {
239
+ if (controller.execute !== undefined) {
240
+ const response = await controller.execute({
241
+ connection,
242
+ application,
243
+ function: func,
244
+ arguments: args as object,
245
+ });
246
+ return response.body;
247
+ }
248
+ return HttpLlm.execute({
249
+ application,
250
+ function: func,
251
+ connection,
252
+ input: args as object,
253
+ });
254
+ },
255
+ });
256
+ }
257
+ };
258
+
259
+ const handleToolCall = async (
260
+ entry: IToolEntry,
261
+ args: unknown,
262
+ ): Promise<CallToolResult> => {
263
+ const coerced: unknown = LlmJson.coerce(args, entry.function.parameters);
264
+ const validation: IValidation<unknown> = entry.function.validate(coerced);
265
+ if (!validation.success) {
266
+ return {
267
+ isError: true,
268
+ content: [
269
+ {
270
+ type: "text" as const,
271
+ text: LlmJson.stringify(validation),
272
+ },
273
+ ],
274
+ };
275
+ }
276
+
277
+ try {
278
+ const result: unknown = await entry.execute(validation.data);
279
+ return {
280
+ content: [
281
+ {
282
+ type: "text" as const,
283
+ text:
284
+ result === undefined
285
+ ? "Success"
286
+ : JSON.stringify(result, null, 2),
287
+ },
288
+ ],
289
+ };
290
+ } catch (error) {
291
+ return {
292
+ isError: true,
293
+ content: [
294
+ {
295
+ type: "text" as const,
296
+ text:
297
+ error instanceof Error
298
+ ? `${error.name}: ${error.message}`
299
+ : String(error),
300
+ },
301
+ ],
302
+ };
303
+ }
304
+ };
305
+
306
+ const convertZodToJsonSchema = (
307
+ zodSchema: ZodType | ZodObject<any> | undefined,
308
+ ): Tool["inputSchema"] => {
309
+ if (zodSchema === undefined) {
310
+ return { type: "object", properties: {} };
311
+ }
312
+
313
+ // @todo: error TS2589: Type instantiation is excessively deep and possibly infinite.
314
+ const converted: object = (zodToJsonSchema as any)(zodSchema);
315
+ if (
316
+ typeof converted === "object" &&
317
+ "type" in converted &&
318
+ converted.type === "object"
319
+ ) {
320
+ return converted as Tool["inputSchema"];
321
+ }
322
+ return { type: "object", properties: {} };
323
+ };
324
+ }
325
+
326
+ interface IToolEntry {
327
+ controller: string;
328
+ function: ILlmFunction | IHttpLlmFunction;
329
+ execute: (args: unknown) => Promise<unknown>;
330
+ }