@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 +3 -3
- package/lib/index.js +3 -3
- package/lib/index.mjs +3 -3
- package/package.json +3 -3
- package/src/index.ts +67 -67
- package/src/internal/McpControllerRegistrar.ts +330 -330
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
|
-
*
|
|
13
|
-
*
|
|
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
|
-
*
|
|
14
|
-
*
|
|
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
|
-
*
|
|
12
|
-
*
|
|
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.
|
|
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/
|
|
27
|
-
"@typia/
|
|
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
|
-
*
|
|
16
|
-
*
|
|
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
|
+
}
|