@nestia/core 11.2.0 → 11.3.0
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/adaptors/McpAdaptor.d.ts +75 -0
- package/lib/adaptors/McpAdaptor.js +256 -0
- package/lib/adaptors/McpAdaptor.js.map +1 -0
- package/lib/decorators/McpRoute.d.ts +73 -0
- package/lib/decorators/McpRoute.js +60 -0
- package/lib/decorators/McpRoute.js.map +1 -0
- package/lib/decorators/internal/IMcpRouteReflect.d.ts +2 -0
- package/lib/decorators/internal/IMcpRouteReflect.js +3 -0
- package/lib/decorators/internal/IMcpRouteReflect.js.map +1 -0
- package/lib/module.d.ts +2 -0
- package/lib/module.js +2 -0
- package/lib/module.js.map +1 -1
- package/lib/programmers/McpRouteProgrammer.d.ts +26 -0
- package/lib/programmers/McpRouteProgrammer.js +56 -0
- package/lib/programmers/McpRouteProgrammer.js.map +1 -0
- package/lib/programmers/McpRouteReturnProgrammer.d.ts +24 -0
- package/lib/programmers/McpRouteReturnProgrammer.js +63 -0
- package/lib/programmers/McpRouteReturnProgrammer.js.map +1 -0
- package/lib/programmers/internal/check_mcp_object.d.ts +1 -0
- package/lib/programmers/internal/check_mcp_object.js +28 -0
- package/lib/programmers/internal/check_mcp_object.js.map +1 -0
- package/lib/transformers/McpRouteTransformer.d.ts +33 -0
- package/lib/transformers/McpRouteTransformer.js +182 -0
- package/lib/transformers/McpRouteTransformer.js.map +1 -0
- package/lib/transformers/MethodTransformer.js +6 -0
- package/lib/transformers/MethodTransformer.js.map +1 -1
- package/lib/transformers/ParameterDecoratorTransformer.js +3 -0
- package/lib/transformers/ParameterDecoratorTransformer.js.map +1 -1
- package/package.json +4 -3
- package/src/adaptors/McpAdaptor.ts +276 -0
- package/src/decorators/McpRoute.ts +159 -0
- package/src/decorators/internal/IMcpRouteReflect.ts +41 -0
- package/src/module.ts +2 -0
- package/src/programmers/McpRouteProgrammer.ts +75 -0
- package/src/programmers/McpRouteReturnProgrammer.ts +77 -0
- package/src/programmers/internal/check_mcp_object.ts +37 -0
- package/src/transformers/McpRouteTransformer.ts +271 -0
- package/src/transformers/MethodTransformer.ts +6 -0
- package/src/transformers/ParameterDecoratorTransformer.ts +5 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
|
|
4
|
+
import { INestiaTransformContext } from "../options/INestiaTransformProject";
|
|
5
|
+
import { McpRouteProgrammer } from "../programmers/McpRouteProgrammer";
|
|
6
|
+
import { McpRouteReturnProgrammer } from "../programmers/McpRouteReturnProgrammer";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Rewrites `@McpRoute(...)` decorator calls at compile time.
|
|
10
|
+
*
|
|
11
|
+
* Responsibilities:
|
|
12
|
+
*
|
|
13
|
+
* 1. Normalize the public string form (`@McpRoute("tool_name")`) into an internal
|
|
14
|
+
* config object literal (`@McpRoute({ name: "tool_name" })`) so downstream
|
|
15
|
+
* consumers — runtime adaptor + SDK reflection — only ever see the rich
|
|
16
|
+
* object shape.
|
|
17
|
+
* 2. Enforce MCP-spec constraints by delegating to typia validators:
|
|
18
|
+
*
|
|
19
|
+
* - Exactly one `@McpRoute.Params()` parameter;
|
|
20
|
+
* - Parameter type is an object without dynamic properties
|
|
21
|
+
* ({@link McpRouteProgrammer});
|
|
22
|
+
* - Return type is `void` or a single object without dynamic properties
|
|
23
|
+
* ({@link McpRouteReturnProgrammer}).
|
|
24
|
+
* 3. Inject typia-generated `inputSchema` (and `outputSchema`, when the return
|
|
25
|
+
* type is non-void) into the config literal.
|
|
26
|
+
* 4. Parse the method's JSDoc and inject `description` / `title` into the config
|
|
27
|
+
* literal when not already explicit. The JSDoc comment is the canonical
|
|
28
|
+
* source for human-readable tool metadata.
|
|
29
|
+
*
|
|
30
|
+
* @author wildduck - https://github.com/wildduck2
|
|
31
|
+
*/
|
|
32
|
+
export namespace McpRouteTransformer {
|
|
33
|
+
export const transform = (props: {
|
|
34
|
+
context: INestiaTransformContext;
|
|
35
|
+
decorator: ts.Decorator;
|
|
36
|
+
method: ts.MethodDeclaration;
|
|
37
|
+
}): ts.Decorator => {
|
|
38
|
+
if (!ts.isCallExpression(props.decorator.expression))
|
|
39
|
+
return props.decorator;
|
|
40
|
+
|
|
41
|
+
const signature = props.context.checker.getResolvedSignature(
|
|
42
|
+
props.decorator.expression,
|
|
43
|
+
);
|
|
44
|
+
if (!signature?.declaration) return props.decorator;
|
|
45
|
+
|
|
46
|
+
const location = path.resolve(
|
|
47
|
+
signature.declaration.getSourceFile().fileName,
|
|
48
|
+
);
|
|
49
|
+
if (LIB_PATHS.every((str) => location.indexOf(str) === -1))
|
|
50
|
+
return props.decorator;
|
|
51
|
+
|
|
52
|
+
// Already transformed (config has injected inputSchema entry — second run).
|
|
53
|
+
if (
|
|
54
|
+
props.decorator.expression.arguments.length >= 1 &&
|
|
55
|
+
ts.isObjectLiteralExpression(props.decorator.expression.arguments[0]!) &&
|
|
56
|
+
hasField(
|
|
57
|
+
props.decorator.expression.arguments[0] as ts.ObjectLiteralExpression,
|
|
58
|
+
"inputSchema",
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
return props.decorator;
|
|
62
|
+
|
|
63
|
+
enforceParams(props);
|
|
64
|
+
|
|
65
|
+
const paramsType = findParamsType(props);
|
|
66
|
+
const inputSchema = McpRouteProgrammer.generate({
|
|
67
|
+
context: props.context,
|
|
68
|
+
modulo: props.decorator.expression.expression,
|
|
69
|
+
type: paramsType,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Validate the return type to catch MCP-illegal shapes at compile time
|
|
73
|
+
// (non-object, dynamic-property records, mixed `void | object` unions).
|
|
74
|
+
// We do NOT emit `outputSchema` onto the wire — declaring an output schema
|
|
75
|
+
// obliges the server to return `structuredContent` per the MCP spec, which
|
|
76
|
+
// the v1 adaptor does not produce. Validation alone is enough to honor
|
|
77
|
+
// the constraint.
|
|
78
|
+
const returnType = getReturnType(props);
|
|
79
|
+
if (returnType)
|
|
80
|
+
McpRouteReturnProgrammer.generate({
|
|
81
|
+
context: props.context,
|
|
82
|
+
modulo: props.decorator.expression.expression,
|
|
83
|
+
type: returnType,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const config: ts.ObjectLiteralExpression = normalizeConfig(
|
|
87
|
+
props.decorator.expression.arguments[0],
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const enriched = injectFields(config, [
|
|
91
|
+
...(!hasField(config, "inputSchema")
|
|
92
|
+
? [
|
|
93
|
+
ts.factory.createPropertyAssignment(
|
|
94
|
+
"inputSchema",
|
|
95
|
+
inputSchema as ts.Expression,
|
|
96
|
+
),
|
|
97
|
+
]
|
|
98
|
+
: []),
|
|
99
|
+
...jsDocFields(props.method, config),
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
return ts.factory.createDecorator(
|
|
103
|
+
ts.factory.updateCallExpression(
|
|
104
|
+
props.decorator.expression,
|
|
105
|
+
props.decorator.expression.expression,
|
|
106
|
+
props.decorator.expression.typeArguments,
|
|
107
|
+
[enriched],
|
|
108
|
+
),
|
|
109
|
+
);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Reject methods that do not expose exactly one `@McpRoute.Params()` argument
|
|
114
|
+
* object. MCP tools do not have positional arguments.
|
|
115
|
+
*/
|
|
116
|
+
const enforceParams = (props: { method: ts.MethodDeclaration }): void => {
|
|
117
|
+
const decorated = props.method.parameters
|
|
118
|
+
.map((param, index) => ({ param, index }))
|
|
119
|
+
.filter(({ param }) => hasParamsDecorator(param));
|
|
120
|
+
if (decorated.length !== 1)
|
|
121
|
+
throw new Error(
|
|
122
|
+
`[@nestia.core.McpRoute] method ${props.method.name.getText()} must declare exactly one @McpRoute.Params() parameter.`,
|
|
123
|
+
);
|
|
124
|
+
if (props.method.parameters.length !== 1 || decorated[0]!.index !== 0)
|
|
125
|
+
throw new Error(
|
|
126
|
+
`[@nestia.core.McpRoute] method ${props.method.name.getText()} must have exactly one parameter, and it must be decorated by @McpRoute.Params().`,
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const findParamsType = (props: {
|
|
131
|
+
context: INestiaTransformContext;
|
|
132
|
+
method: ts.MethodDeclaration;
|
|
133
|
+
}): ts.Type => {
|
|
134
|
+
for (const param of props.method.parameters) {
|
|
135
|
+
if (hasParamsDecorator(param))
|
|
136
|
+
return props.context.checker.getTypeAtLocation(param);
|
|
137
|
+
}
|
|
138
|
+
throw new Error(
|
|
139
|
+
`[@nestia.core.McpRoute] method ${props.method.name.getText()} must declare exactly one @McpRoute.Params() parameter.`,
|
|
140
|
+
);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const hasParamsDecorator = (param: ts.ParameterDeclaration): boolean => {
|
|
144
|
+
const decos = (param.modifiers ?? []).filter((m) =>
|
|
145
|
+
ts.isDecorator(m),
|
|
146
|
+
) as ts.Decorator[];
|
|
147
|
+
return decos.some((d) =>
|
|
148
|
+
d.expression.getText().includes("McpRoute.Params"),
|
|
149
|
+
);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const getReturnType = (props: {
|
|
153
|
+
context: INestiaTransformContext;
|
|
154
|
+
method: ts.MethodDeclaration;
|
|
155
|
+
}): ts.Type | undefined => {
|
|
156
|
+
const sig = props.context.checker.getSignatureFromDeclaration(props.method);
|
|
157
|
+
if (!sig) return undefined;
|
|
158
|
+
return props.context.checker.getReturnTypeOfSignature(sig);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Convert a string literal argument (`@McpRoute("name")`) into the canonical
|
|
163
|
+
* object form (`@McpRoute({ name: "name" })`). If the argument is already an
|
|
164
|
+
* object literal it is returned unchanged. Anything else (computed
|
|
165
|
+
* expressions etc.) is rejected via TransformerError so the wire metadata
|
|
166
|
+
* stays statically analyzable.
|
|
167
|
+
*/
|
|
168
|
+
const normalizeConfig = (
|
|
169
|
+
arg: ts.Expression | undefined,
|
|
170
|
+
): ts.ObjectLiteralExpression => {
|
|
171
|
+
if (arg === undefined)
|
|
172
|
+
return ts.factory.createObjectLiteralExpression([], true);
|
|
173
|
+
if (ts.isStringLiteralLike(arg))
|
|
174
|
+
return ts.factory.createObjectLiteralExpression(
|
|
175
|
+
[
|
|
176
|
+
ts.factory.createPropertyAssignment(
|
|
177
|
+
"name",
|
|
178
|
+
ts.factory.createStringLiteral(arg.text),
|
|
179
|
+
),
|
|
180
|
+
],
|
|
181
|
+
true,
|
|
182
|
+
);
|
|
183
|
+
if (ts.isObjectLiteralExpression(arg)) return arg;
|
|
184
|
+
throw new Error(
|
|
185
|
+
"[@nestia.core.McpRoute] argument must be a string literal or object literal so the tool name can be statically resolved.",
|
|
186
|
+
);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Parse the JSDoc block attached to `method` and emit the property
|
|
191
|
+
* assignments needed to set `description` / `title` when the existing config
|
|
192
|
+
* object does not already define them.
|
|
193
|
+
*/
|
|
194
|
+
const jsDocFields = (
|
|
195
|
+
method: ts.MethodDeclaration,
|
|
196
|
+
config: ts.ObjectLiteralExpression,
|
|
197
|
+
): ts.PropertyAssignment[] => {
|
|
198
|
+
const docs = ts.getJSDocCommentsAndTags(method);
|
|
199
|
+
let description: string | undefined;
|
|
200
|
+
let title: string | undefined;
|
|
201
|
+
|
|
202
|
+
for (const d of docs) {
|
|
203
|
+
if (!ts.isJSDoc(d)) continue;
|
|
204
|
+
const raw =
|
|
205
|
+
typeof d.comment === "string"
|
|
206
|
+
? d.comment
|
|
207
|
+
: (d.comment?.map((n) => n.text).join("") ?? undefined);
|
|
208
|
+
if (raw !== undefined && raw.trim().length !== 0)
|
|
209
|
+
description = raw.trim();
|
|
210
|
+
|
|
211
|
+
for (const tag of d.tags ?? []) {
|
|
212
|
+
const name = tag.tagName.getText();
|
|
213
|
+
const text =
|
|
214
|
+
typeof tag.comment === "string"
|
|
215
|
+
? tag.comment
|
|
216
|
+
: tag.comment?.map((n) => n.text).join("");
|
|
217
|
+
if (name === "title" && text !== undefined && text.trim().length !== 0)
|
|
218
|
+
title = text.trim();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const out: ts.PropertyAssignment[] = [];
|
|
223
|
+
if (description !== undefined && !hasField(config, "description"))
|
|
224
|
+
out.push(
|
|
225
|
+
ts.factory.createPropertyAssignment(
|
|
226
|
+
"description",
|
|
227
|
+
ts.factory.createStringLiteral(description),
|
|
228
|
+
),
|
|
229
|
+
);
|
|
230
|
+
if (title !== undefined && !hasField(config, "title"))
|
|
231
|
+
out.push(
|
|
232
|
+
ts.factory.createPropertyAssignment(
|
|
233
|
+
"title",
|
|
234
|
+
ts.factory.createStringLiteral(title),
|
|
235
|
+
),
|
|
236
|
+
);
|
|
237
|
+
return out;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const hasField = (
|
|
241
|
+
config: ts.ObjectLiteralExpression,
|
|
242
|
+
name: string,
|
|
243
|
+
): boolean =>
|
|
244
|
+
config.properties.some(
|
|
245
|
+
(p) =>
|
|
246
|
+
ts.isPropertyAssignment(p) && !!p.name && propertyName(p.name) === name,
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const propertyName = (name: ts.PropertyName): string | undefined =>
|
|
250
|
+
ts.isIdentifier(name)
|
|
251
|
+
? name.escapedText.toString()
|
|
252
|
+
: ts.isStringLiteralLike(name)
|
|
253
|
+
? name.text
|
|
254
|
+
: undefined;
|
|
255
|
+
|
|
256
|
+
const injectFields = (
|
|
257
|
+
config: ts.ObjectLiteralExpression,
|
|
258
|
+
additions: ts.PropertyAssignment[],
|
|
259
|
+
): ts.ObjectLiteralExpression =>
|
|
260
|
+
additions.length === 0
|
|
261
|
+
? config
|
|
262
|
+
: ts.factory.updateObjectLiteralExpression(config, [
|
|
263
|
+
...config.properties,
|
|
264
|
+
...additions,
|
|
265
|
+
]);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const LIB_PATHS = [
|
|
269
|
+
path.join("@nestia", "core", "lib", "decorators", "McpRoute.d.ts"),
|
|
270
|
+
path.join("packages", "core", "src", "decorators", "McpRoute.ts"),
|
|
271
|
+
];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
2
|
|
|
3
3
|
import { INestiaTransformContext } from "../options/INestiaTransformProject";
|
|
4
|
+
import { McpRouteTransformer } from "./McpRouteTransformer";
|
|
4
5
|
import { TypedRouteTransformer } from "./TypedRouteTransformer";
|
|
5
6
|
import { WebSocketRouteTransformer } from "./WebSocketRouteTransformer";
|
|
6
7
|
|
|
@@ -34,6 +35,11 @@ export namespace MethodTransformer {
|
|
|
34
35
|
method: props.method,
|
|
35
36
|
decorator,
|
|
36
37
|
});
|
|
38
|
+
decorator = McpRouteTransformer.transform({
|
|
39
|
+
context: props.context,
|
|
40
|
+
decorator,
|
|
41
|
+
method: props.method,
|
|
42
|
+
});
|
|
37
43
|
return decorator;
|
|
38
44
|
};
|
|
39
45
|
if (ts.getDecorators !== undefined)
|
|
@@ -124,6 +124,11 @@ const FUNCTORS: Record<string, Programmer> = {
|
|
|
124
124
|
props.arguments.length
|
|
125
125
|
? props.arguments
|
|
126
126
|
: [TypedQueryProgrammer.generate(props)],
|
|
127
|
+
|
|
128
|
+
"McpRoute.Params": (props) =>
|
|
129
|
+
props.arguments.length
|
|
130
|
+
? props.arguments
|
|
131
|
+
: [TypedBodyProgrammer.generate(props)],
|
|
127
132
|
};
|
|
128
133
|
|
|
129
134
|
const LIB_PATH = path.join("@nestia", "core", "lib", "decorators");
|