@nestia/core 14.0.0 → 14.0.2
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/MIGRATION.md +20 -0
- package/README.md +1 -1
- package/lib/adaptors/McpAdaptor.d.ts +29 -3
- package/lib/adaptors/McpAdaptor.js +150 -24
- package/lib/adaptors/McpAdaptor.js.map +1 -1
- package/lib/adaptors/WebSocketAdaptor.d.ts +37 -0
- package/lib/adaptors/WebSocketAdaptor.js +184 -48
- package/lib/adaptors/WebSocketAdaptor.js.map +1 -1
- package/lib/adaptors/internal/RoutePathMatcher.d.ts +1 -0
- package/lib/adaptors/internal/RoutePathMatcher.js +88 -0
- package/lib/adaptors/internal/RoutePathMatcher.js.map +1 -0
- package/lib/adaptors/internal/external_context.d.ts +1 -0
- package/lib/adaptors/internal/external_context.js +56 -0
- package/lib/adaptors/internal/external_context.js.map +1 -0
- package/lib/decorators/DynamicModule.d.ts +12 -0
- package/lib/decorators/DynamicModule.js +12 -0
- package/lib/decorators/DynamicModule.js.map +1 -1
- package/lib/decorators/EncryptedBody.d.ts +14 -6
- package/lib/decorators/EncryptedBody.js +39 -16
- package/lib/decorators/EncryptedBody.js.map +1 -1
- package/lib/decorators/EncryptedController.d.ts +6 -1
- package/lib/decorators/EncryptedController.js +7 -2
- package/lib/decorators/EncryptedController.js.map +1 -1
- package/lib/decorators/EncryptedModule.d.ts +25 -6
- package/lib/decorators/EncryptedModule.js +87 -18
- package/lib/decorators/EncryptedModule.js.map +1 -1
- package/lib/decorators/EncryptedRoute.d.ts +20 -11
- package/lib/decorators/EncryptedRoute.js +27 -13
- package/lib/decorators/EncryptedRoute.js.map +1 -1
- package/lib/decorators/HumanRoute.d.ts +5 -0
- package/lib/decorators/HumanRoute.js +5 -0
- package/lib/decorators/HumanRoute.js.map +1 -1
- package/lib/decorators/McpRoute.d.ts +31 -19
- package/lib/decorators/McpRoute.js +12 -1
- package/lib/decorators/McpRoute.js.map +1 -1
- package/lib/decorators/PlainBody.d.ts +7 -1
- package/lib/decorators/PlainBody.js +16 -8
- package/lib/decorators/PlainBody.js.map +1 -1
- package/lib/decorators/SwaggerCustomizer.d.ts +15 -0
- package/lib/decorators/SwaggerCustomizer.js +5 -0
- package/lib/decorators/SwaggerCustomizer.js.map +1 -1
- package/lib/decorators/SwaggerExample.d.ts +51 -22
- package/lib/decorators/SwaggerExample.js +35 -22
- package/lib/decorators/SwaggerExample.js.map +1 -1
- package/lib/decorators/TypedBody.d.ts +10 -3
- package/lib/decorators/TypedBody.js +20 -10
- package/lib/decorators/TypedBody.js.map +1 -1
- package/lib/decorators/TypedException.d.ts +10 -0
- package/lib/decorators/TypedFormData.d.ts +81 -9
- package/lib/decorators/TypedFormData.js +121 -31
- package/lib/decorators/TypedFormData.js.map +1 -1
- package/lib/decorators/TypedHeaders.d.ts +5 -0
- package/lib/decorators/TypedHeaders.js +5 -0
- package/lib/decorators/TypedHeaders.js.map +1 -1
- package/lib/decorators/TypedParam.d.ts +12 -3
- package/lib/decorators/TypedParam.js +12 -3
- package/lib/decorators/TypedParam.js.map +1 -1
- package/lib/decorators/TypedQuery.d.ts +17 -1
- package/lib/decorators/TypedQuery.js +25 -4
- package/lib/decorators/TypedQuery.js.map +1 -1
- package/lib/decorators/TypedRoute.d.ts +25 -10
- package/lib/decorators/TypedRoute.js +19 -10
- package/lib/decorators/TypedRoute.js.map +1 -1
- package/lib/decorators/WebSocketRoute.d.ts +18 -1
- package/lib/decorators/WebSocketRoute.js +18 -1
- package/lib/decorators/WebSocketRoute.js.map +1 -1
- package/lib/decorators/doNotThrowTransformError.d.ts +14 -0
- package/lib/decorators/doNotThrowTransformError.js +14 -0
- package/lib/decorators/doNotThrowTransformError.js.map +1 -1
- package/lib/decorators/internal/EncryptedConstant.js +13 -2
- package/lib/decorators/internal/EncryptedConstant.js.map +1 -1
- package/lib/decorators/internal/IRequestBodyValidation.d.ts +66 -0
- package/lib/decorators/internal/IRequestBodyValidation.js +3 -0
- package/lib/decorators/internal/IRequestBodyValidation.js.map +1 -0
- package/lib/decorators/internal/IWebSocketRouteReflect.d.ts +93 -1
- package/lib/decorators/internal/get_encryption_password.d.ts +1 -0
- package/lib/decorators/internal/get_encryption_password.js +14 -0
- package/lib/decorators/internal/get_encryption_password.js.map +1 -0
- package/lib/decorators/internal/get_path_and_querify.js +23 -9
- package/lib/decorators/internal/get_path_and_querify.js.map +1 -1
- package/lib/decorators/internal/get_path_and_stringify.js +3 -7
- package/lib/decorators/internal/get_path_and_stringify.js.map +1 -1
- package/lib/decorators/internal/get_text_body.js +9 -3
- package/lib/decorators/internal/get_text_body.js.map +1 -1
- package/lib/decorators/internal/group_entries.d.ts +1 -0
- package/lib/decorators/internal/group_entries.js +19 -0
- package/lib/decorators/internal/group_entries.js.map +1 -0
- package/lib/decorators/internal/load_controller.d.ts +14 -0
- package/lib/decorators/internal/load_controller.js +14 -0
- package/lib/decorators/internal/load_controller.js.map +1 -1
- package/lib/decorators/internal/split_route_arguments.d.ts +1 -0
- package/lib/decorators/internal/split_route_arguments.js +22 -0
- package/lib/decorators/internal/split_route_arguments.js.map +1 -0
- package/lib/decorators/internal/validate_request_body.js +68 -25
- package/lib/decorators/internal/validate_request_body.js.map +1 -1
- package/lib/decorators/internal/validate_request_form_data.js +3 -1
- package/lib/decorators/internal/validate_request_form_data.js.map +1 -1
- package/lib/decorators/internal/validate_request_headers.js +2 -1
- package/lib/decorators/internal/validate_request_headers.js.map +1 -1
- package/lib/decorators/internal/validate_request_query.js +3 -1
- package/lib/decorators/internal/validate_request_query.js.map +1 -1
- package/lib/options/IRequestBodyValidator.d.ts +71 -0
- package/lib/options/IRequestFormDataProps.d.ts +80 -0
- package/lib/options/IRequestHeadersValidator.d.ts +67 -0
- package/lib/options/IRequestQueryValidator.d.ts +68 -0
- package/lib/options/IResponseBodyQuerifier.d.ts +125 -1
- package/lib/options/IResponseBodyStringifier.d.ts +107 -0
- package/lib/typings/Creator.d.ts +10 -0
- package/lib/utils/ArrayUtil.d.ts +21 -0
- package/lib/utils/ArrayUtil.js +22 -1
- package/lib/utils/ArrayUtil.js.map +1 -1
- package/lib/utils/ExceptionManager.d.ts +43 -4
- package/lib/utils/ExceptionManager.js +44 -6
- package/lib/utils/ExceptionManager.js.map +1 -1
- package/lib/utils/SourceFinder.d.ts +22 -0
- package/lib/utils/SourceFinder.js +60 -10
- package/lib/utils/SourceFinder.js.map +1 -1
- package/lib/utils/VersioningStrategy.d.ts +45 -2
- package/lib/utils/VersioningStrategy.js +40 -7
- package/lib/utils/VersioningStrategy.js.map +1 -1
- package/native/go.mod +1 -1
- package/native/go.sum +2 -2
- package/native/plugin/plan.go +68 -2
- package/native/transform/ast.go +23 -3
- package/native/transform/build.go +51 -57
- package/native/transform/buildoutput/output.go +147 -0
- package/native/transform/contributor.go +9 -0
- package/native/transform/core_querify.go +63 -9
- package/native/transform/core_transform.go +180 -98
- package/native/transform/core_websocket.go +111 -14
- package/native/transform/node_transform.go +17 -8
- package/native/transform/package.go +55 -0
- package/native/transform/printer.go +1 -3
- package/native/transform/run.go +18 -0
- package/native/transform/schemaprojection/project.go +213 -0
- package/native/transform/transform.go +29 -18
- package/native/transform/typia_fast.go +6 -0
- package/native/transform.cjs +49 -0
- package/package.json +13 -15
- package/src/adaptors/McpAdaptor.ts +215 -33
- package/src/adaptors/WebSocketAdaptor.ts +202 -52
- package/src/adaptors/internal/RoutePathMatcher.ts +91 -0
- package/src/adaptors/internal/external_context.ts +81 -0
- package/src/decorators/DynamicModule.ts +12 -0
- package/src/decorators/EncryptedBody.ts +39 -15
- package/src/decorators/EncryptedController.ts +12 -3
- package/src/decorators/EncryptedModule.ts +89 -18
- package/src/decorators/EncryptedRoute.ts +27 -16
- package/src/decorators/HumanRoute.ts +5 -0
- package/src/decorators/McpRoute.ts +31 -19
- package/src/decorators/PlainBody.ts +20 -10
- package/src/decorators/SwaggerCustomizer.ts +15 -0
- package/src/decorators/SwaggerExample.ts +51 -22
- package/src/decorators/TypedBody.ts +18 -9
- package/src/decorators/TypedException.ts +10 -0
- package/src/decorators/TypedFormData.ts +207 -45
- package/src/decorators/TypedHeaders.ts +5 -0
- package/src/decorators/TypedParam.ts +12 -3
- package/src/decorators/TypedQuery.ts +35 -7
- package/src/decorators/TypedRoute.ts +25 -10
- package/src/decorators/WebSocketRoute.ts +18 -1
- package/src/decorators/doNotThrowTransformError.ts +14 -0
- package/src/decorators/internal/EncryptedConstant.ts +14 -1
- package/src/decorators/internal/IMcpRouteReflect.ts +7 -1
- package/src/decorators/internal/IRequestBodyValidation.ts +65 -0
- package/src/decorators/internal/IWebSocketRouteReflect.ts +95 -1
- package/src/decorators/internal/get_encryption_password.ts +19 -0
- package/src/decorators/internal/get_path_and_querify.ts +31 -10
- package/src/decorators/internal/get_path_and_stringify.ts +3 -9
- package/src/decorators/internal/get_text_body.ts +8 -4
- package/src/decorators/internal/group_entries.ts +17 -0
- package/src/decorators/internal/load_controller.ts +14 -0
- package/src/decorators/internal/split_route_arguments.ts +20 -0
- package/src/decorators/internal/validate_request_body.ts +87 -34
- package/src/decorators/internal/validate_request_form_data.ts +3 -1
- package/src/decorators/internal/validate_request_headers.ts +2 -2
- package/src/decorators/internal/validate_request_query.ts +3 -2
- package/src/options/IRequestBodyValidator.ts +71 -0
- package/src/options/IRequestFormDataProps.ts +80 -0
- package/src/options/IRequestHeadersValidator.ts +67 -0
- package/src/options/IRequestQueryValidator.ts +68 -0
- package/src/options/IResponseBodyQuerifier.ts +126 -1
- package/src/options/IResponseBodyStringifier.ts +107 -0
- package/src/typings/Creator.ts +10 -0
- package/src/utils/ArrayUtil.ts +22 -3
- package/src/utils/ExceptionManager.ts +50 -6
- package/src/utils/SourceFinder.ts +62 -10
- package/src/utils/VersioningStrategy.ts +55 -11
- package/lib/transform.d.ts +0 -10
- package/lib/transform.js +0 -25
- package/lib/transform.js.map +0 -1
- package/src/transform.ts +0 -26
|
@@ -3,9 +3,19 @@ import {
|
|
|
3
3
|
HttpException,
|
|
4
4
|
INestApplication,
|
|
5
5
|
} from "@nestjs/common";
|
|
6
|
-
import {
|
|
6
|
+
import { RouteParamtypes } from "@nestjs/common/enums/route-paramtypes.enum";
|
|
7
|
+
import { ContextId, NestContainer } from "@nestjs/core";
|
|
8
|
+
import { ExternalContextCreator } from "@nestjs/core/helpers/external-context-creator";
|
|
9
|
+
import { Injector } from "@nestjs/core/injector/injector";
|
|
10
|
+
import { InstanceWrapper } from "@nestjs/core/injector/instance-wrapper";
|
|
11
|
+
import { Module } from "@nestjs/core/injector/module";
|
|
7
12
|
|
|
8
13
|
import { IMcpRouteReflect } from "../decorators/internal/IMcpRouteReflect";
|
|
14
|
+
import { resolve_request_body } from "../decorators/internal/validate_request_body";
|
|
15
|
+
import {
|
|
16
|
+
create_external_context_creator,
|
|
17
|
+
get_request_context_id,
|
|
18
|
+
} from "./internal/external_context";
|
|
9
19
|
|
|
10
20
|
/**
|
|
11
21
|
* MCP (Model Context Protocol) adaptor.
|
|
@@ -23,6 +33,11 @@ import { IMcpRouteReflect } from "../decorators/internal/IMcpRouteReflect";
|
|
|
23
33
|
* registration API of `McpServer` is bypassed by accessing the low-level
|
|
24
34
|
* `.server` handler.
|
|
25
35
|
*
|
|
36
|
+
* A tool call passes the guards, interceptors, pipes, and exception filters
|
|
37
|
+
* NestJS applies to an HTTP route on the same method, with the MCP HTTP request
|
|
38
|
+
* as the execution context. The endpoint is mounted at `path` as given, outside
|
|
39
|
+
* the application's global prefix.
|
|
40
|
+
*
|
|
26
41
|
* Error mapping follows the MCP specification:
|
|
27
42
|
*
|
|
28
43
|
* - Unknown tool name: JSON-RPC `-32601`.
|
|
@@ -33,14 +48,12 @@ import { IMcpRouteReflect } from "../decorators/internal/IMcpRouteReflect";
|
|
|
33
48
|
*
|
|
34
49
|
* @author wildduck - https://github.com/wildduck2
|
|
35
50
|
* @example
|
|
36
|
-
* ```typescript
|
|
37
51
|
* import core from "@nestia/core";
|
|
38
52
|
* import { NestFactory } from "@nestjs/core";
|
|
39
53
|
*
|
|
40
54
|
* const app = await NestFactory.create(AppModule);
|
|
41
55
|
* await core.McpAdaptor.upgrade(app, { path: "/mcp" });
|
|
42
56
|
* await app.listen(3000);
|
|
43
|
-
* ```;
|
|
44
57
|
*/
|
|
45
58
|
export class McpAdaptor {
|
|
46
59
|
/**
|
|
@@ -57,6 +70,11 @@ export class McpAdaptor {
|
|
|
57
70
|
*
|
|
58
71
|
* @param app Running Nest application instance.
|
|
59
72
|
* @param options Transport and identity overrides.
|
|
73
|
+
* @evidence contracts/common.md#principled-implementation Tools are collected once from the prototype chain of every controller instance by the `nestia/McpRoute` metadata, with the first definition of a property name winning and accessors skipped without execution, duplicates by tool name refused before serving, and each call runs through NestJS's `ExternalContextCreator` so guards, interceptors, pipes, and exception filters apply as for an HTTP route; every request gets a fresh MCP server and Streamable HTTP transport, which stateless mode requires, and both are closed in a `finally`.
|
|
74
|
+
* @evidence contracts/common.md#clear-and-simple-design One entry point does discovery and registration; the per-tool call (`createHandler`), the duplicate check, the SDK loader, and the per-request response facade are separate module-private functions.
|
|
75
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts Reading the module graph goes through the internal `app.container`, because `INestApplication` exposes no public accessor for it, `create_external_context_creator` constructs an owned subclass through the Nest constructor and overrides module selection on that type, because the default looks for provider classes while controllers belong to the controller collection, and `get_request_context_id` registers the request provider on the request object as NestJS's router does for an HTTP route, and a per-request response facade forwards native state, events and method receivers without replacing raw methods. After a tool or its exception filter has sent headers, only the facade suppresses subsequent transport writes and preserves callbacks and chaining. Native transport writes do not themselves activate takeover. The remaining foreign-internal reads are the application container and Nest request context registration; the MCP SDK uses its public Node response injection boundary.
|
|
76
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states the stateless mode, what is discovered, and that the SDK is an optional dependency; the options documentation states the path and server information.
|
|
77
|
+
* @evidenceExclude contracts/portability.md#os-neutral-implementation MCP tool discovery and HTTP transport callbacks carry protocol values; optional SDK imports use Node module resolution and no native file identity or child process is defined here.
|
|
60
78
|
*/
|
|
61
79
|
public static async upgrade(
|
|
62
80
|
app: INestApplication,
|
|
@@ -69,7 +87,12 @@ export class McpAdaptor {
|
|
|
69
87
|
|
|
70
88
|
const tools: McpAdaptor.ITool[] = [];
|
|
71
89
|
const container = (app as any).container as NestContainer;
|
|
72
|
-
|
|
90
|
+
const injector: Injector = new Injector();
|
|
91
|
+
for (const [moduleKey, module] of container.getModules()) {
|
|
92
|
+
const creator: ExternalContextCreator = create_external_context_creator(
|
|
93
|
+
container,
|
|
94
|
+
moduleKey,
|
|
95
|
+
);
|
|
73
96
|
for (const wrapper of module.controllers.values()) {
|
|
74
97
|
const instance = wrapper.instance;
|
|
75
98
|
if (!instance) continue;
|
|
@@ -82,7 +105,8 @@ export class McpAdaptor {
|
|
|
82
105
|
for (const key of Object.getOwnPropertyNames(proto)) {
|
|
83
106
|
if (key === "constructor" || visited.has(key)) continue;
|
|
84
107
|
visited.add(key);
|
|
85
|
-
|
|
108
|
+
// Route discovery inspects data descriptors without invoking accessors.
|
|
109
|
+
const method = Object.getOwnPropertyDescriptor(proto, key)?.value;
|
|
86
110
|
if (typeof method !== "function") continue;
|
|
87
111
|
|
|
88
112
|
const meta: IMcpRouteReflect | undefined = Reflect.getMetadata(
|
|
@@ -94,15 +118,18 @@ export class McpAdaptor {
|
|
|
94
118
|
const params: IMcpRouteReflect.IArgument[] =
|
|
95
119
|
Reflect.getMetadata("nestia/McpRoute/Parameters", proto, key) ??
|
|
96
120
|
[];
|
|
97
|
-
const paramValidator = params.find(
|
|
98
|
-
(p) => p.category === "params",
|
|
99
|
-
)?.validate;
|
|
100
|
-
|
|
101
121
|
tools.push({
|
|
102
122
|
meta,
|
|
103
123
|
source: `${wrapper.metatype?.name ?? proto.constructor?.name ?? "UnknownController"}.${String(key)}`,
|
|
104
|
-
|
|
105
|
-
|
|
124
|
+
handler: createHandler({
|
|
125
|
+
container,
|
|
126
|
+
injector,
|
|
127
|
+
creator,
|
|
128
|
+
module,
|
|
129
|
+
wrapper,
|
|
130
|
+
key,
|
|
131
|
+
argument: params.find((p) => p.category === "params"),
|
|
132
|
+
}),
|
|
106
133
|
});
|
|
107
134
|
}
|
|
108
135
|
}
|
|
@@ -132,6 +159,7 @@ export class McpAdaptor {
|
|
|
132
159
|
capabilities: { tools: {} },
|
|
133
160
|
});
|
|
134
161
|
const server = mcp.server;
|
|
162
|
+
const relay = transportResponse(res.raw ?? res);
|
|
135
163
|
|
|
136
164
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
137
165
|
tools: tools.map((t) => ({
|
|
@@ -153,25 +181,14 @@ export class McpAdaptor {
|
|
|
153
181
|
);
|
|
154
182
|
|
|
155
183
|
const args = reqMsg.params.arguments ?? {};
|
|
156
|
-
if (tool.validateArgs) {
|
|
157
|
-
const err: Error | null = tool.validateArgs(args);
|
|
158
|
-
if (err !== null) {
|
|
159
|
-
const body =
|
|
160
|
-
err instanceof BadRequestException
|
|
161
|
-
? (err.getResponse() as any)
|
|
162
|
-
: undefined;
|
|
163
|
-
throw new McpError(ErrorCode.InvalidParams, err.message, {
|
|
164
|
-
errors: body?.errors,
|
|
165
|
-
path: body?.path,
|
|
166
|
-
expected: body?.expected,
|
|
167
|
-
value: body?.value,
|
|
168
|
-
reason: body?.reason,
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
184
|
try {
|
|
174
|
-
const result = await tool.handler(
|
|
185
|
+
const result = await tool.handler({
|
|
186
|
+
request: req,
|
|
187
|
+
response: res,
|
|
188
|
+
args,
|
|
189
|
+
});
|
|
190
|
+
if (relay.takeOver()) return { content: [] };
|
|
191
|
+
if (result instanceof Error) throw result;
|
|
175
192
|
if (result === undefined) return { content: [] };
|
|
176
193
|
return {
|
|
177
194
|
content: [
|
|
@@ -183,6 +200,17 @@ export class McpAdaptor {
|
|
|
183
200
|
],
|
|
184
201
|
};
|
|
185
202
|
} catch (e) {
|
|
203
|
+
if (relay.takeOver()) return { content: [] };
|
|
204
|
+
if (INVALID_ARGUMENTS.has(e as object)) {
|
|
205
|
+
const body = (e as BadRequestException).getResponse() as any;
|
|
206
|
+
throw new McpError(ErrorCode.InvalidParams, (e as Error).message, {
|
|
207
|
+
errors: body?.errors,
|
|
208
|
+
path: body?.path,
|
|
209
|
+
expected: body?.expected,
|
|
210
|
+
value: body?.value,
|
|
211
|
+
reason: body?.reason,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
186
214
|
if (e instanceof HttpException) {
|
|
187
215
|
return {
|
|
188
216
|
content: [{ type: "text" as const, text: e.message }],
|
|
@@ -202,7 +230,7 @@ export class McpAdaptor {
|
|
|
202
230
|
});
|
|
203
231
|
try {
|
|
204
232
|
await mcp.connect(transport);
|
|
205
|
-
await transport.handleRequest(req.raw ?? req,
|
|
233
|
+
await transport.handleRequest(req.raw ?? req, relay.response, req.body);
|
|
206
234
|
} finally {
|
|
207
235
|
await transport.close().catch(() => {});
|
|
208
236
|
await mcp.close().catch(() => {});
|
|
@@ -211,6 +239,139 @@ export class McpAdaptor {
|
|
|
211
239
|
}
|
|
212
240
|
}
|
|
213
241
|
|
|
242
|
+
/**
|
|
243
|
+
* The tool's call through the enhancers NestJS applies to a route of the same
|
|
244
|
+
* method: guards, interceptors, pipes, and exception filters, bound to the
|
|
245
|
+
* method, its controller, or globally. The MCP HTTP request is the execution
|
|
246
|
+
* context, so a guard reading `switchToHttp().getRequest()` works unchanged.
|
|
247
|
+
*
|
|
248
|
+
* The arguments reach the method through the pipes as its body would, validated
|
|
249
|
+
* by typia at that stage: after the guards, as `@TypedBody()` is. An invalid
|
|
250
|
+
* argument is thrown as the validator's `BadRequestException`, which an
|
|
251
|
+
* exception filter may map; unmapped, it becomes JSON-RPC `-32602`. Successful
|
|
252
|
+
* resolver data becomes the argument before pipes execute. Legacy error-only
|
|
253
|
+
* metadata callbacks retain raw input on null success.
|
|
254
|
+
*
|
|
255
|
+
* A controller that is request-scoped, itself or through an enhancer or a
|
|
256
|
+
* dependency, is built per request with its enhancers, as NestJS builds it for
|
|
257
|
+
* an HTTP route; the static instance is only a placeholder without them.
|
|
258
|
+
*/
|
|
259
|
+
const createHandler = (props: {
|
|
260
|
+
container: NestContainer;
|
|
261
|
+
injector: Injector;
|
|
262
|
+
creator: ExternalContextCreator;
|
|
263
|
+
module: Module;
|
|
264
|
+
wrapper: InstanceWrapper;
|
|
265
|
+
key: string;
|
|
266
|
+
argument: IMcpRouteReflect.IArgument | undefined;
|
|
267
|
+
}): McpAdaptor.ITool["handler"] => {
|
|
268
|
+
// the arguments stand at the params' position, or first when undecorated
|
|
269
|
+
const index: number = props.argument?.index ?? 0;
|
|
270
|
+
Reflect.defineMetadata(
|
|
271
|
+
PARAMS_METADATA,
|
|
272
|
+
{ [`${RouteParamtypes.BODY}:${index}`]: { index, data: undefined } },
|
|
273
|
+
props.wrapper.instance.constructor,
|
|
274
|
+
props.key,
|
|
275
|
+
);
|
|
276
|
+
const validate = props.argument?.validate;
|
|
277
|
+
const create = (instance: any, contextId?: ContextId) =>
|
|
278
|
+
props.creator.create(
|
|
279
|
+
instance,
|
|
280
|
+
instance[props.key],
|
|
281
|
+
props.key,
|
|
282
|
+
PARAMS_METADATA,
|
|
283
|
+
{
|
|
284
|
+
// [request, response, next] as an HTTP route has, then the arguments
|
|
285
|
+
exchangeKeyForValue: (_type, _data, [, , , args]) => {
|
|
286
|
+
if (!validate) return args;
|
|
287
|
+
const result = resolve_request_body(validate, args);
|
|
288
|
+
if (result.success) return result.data;
|
|
289
|
+
INVALID_ARGUMENTS.add(result.error);
|
|
290
|
+
throw result.error;
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
contextId,
|
|
294
|
+
contextId && props.wrapper.id,
|
|
295
|
+
);
|
|
296
|
+
let target: ((...args: any[]) => Promise<unknown>) | undefined;
|
|
297
|
+
return async (input) => {
|
|
298
|
+
const call = (fn: (...args: any[]) => Promise<unknown>) =>
|
|
299
|
+
fn(input.request, input.response, undefined, input.args);
|
|
300
|
+
// resolved at the first call, so global enhancers init() registers count
|
|
301
|
+
if (props.wrapper.isDependencyTreeStatic())
|
|
302
|
+
return call((target ??= create(props.wrapper.instance)));
|
|
303
|
+
const contextId: ContextId = get_request_context_id(
|
|
304
|
+
props.container,
|
|
305
|
+
input.request as object,
|
|
306
|
+
props.wrapper.isDependencyTreeDurable(),
|
|
307
|
+
);
|
|
308
|
+
const instance: object = await props.injector.loadPerContext(
|
|
309
|
+
props.wrapper.instance,
|
|
310
|
+
props.module,
|
|
311
|
+
props.module.controllers,
|
|
312
|
+
contextId,
|
|
313
|
+
);
|
|
314
|
+
return call(create(instance, contextId));
|
|
315
|
+
};
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* The transport's response view. Native state, events and method receivers stay
|
|
320
|
+
* on the real response; only this view's writes stop after a tool's exception
|
|
321
|
+
* filter has already answered. Normal transport writes never activate
|
|
322
|
+
* takeover.
|
|
323
|
+
*/
|
|
324
|
+
const transportResponse = (
|
|
325
|
+
raw: any,
|
|
326
|
+
): { response: any; takeOver: () => boolean } => {
|
|
327
|
+
let takenOver = false;
|
|
328
|
+
const respond = (key: string, args: unknown[]): unknown => {
|
|
329
|
+
if (takenOver) {
|
|
330
|
+
const callback = args.find((arg) => typeof arg === "function");
|
|
331
|
+
if ((key === "write" || key === "end") && typeof callback === "function")
|
|
332
|
+
queueMicrotask(() => callback());
|
|
333
|
+
return key === "write"
|
|
334
|
+
? true
|
|
335
|
+
: key === "flushHeaders"
|
|
336
|
+
? undefined
|
|
337
|
+
: response;
|
|
338
|
+
}
|
|
339
|
+
const result = Reflect.apply(raw[key], raw, args);
|
|
340
|
+
return result === raw ? response : result;
|
|
341
|
+
};
|
|
342
|
+
const writes = new Map<PropertyKey, (...args: unknown[]) => unknown>(
|
|
343
|
+
["writeHead", "flushHeaders", "write", "end"].map((key) => [
|
|
344
|
+
key,
|
|
345
|
+
(...args: unknown[]) => respond(key, args),
|
|
346
|
+
]),
|
|
347
|
+
);
|
|
348
|
+
const response: any = new Proxy(raw, {
|
|
349
|
+
get: (target, key) => {
|
|
350
|
+
const write = writes.get(key);
|
|
351
|
+
if (write !== undefined) return write;
|
|
352
|
+
const value = Reflect.get(target, key, target);
|
|
353
|
+
if (typeof value !== "function") return value;
|
|
354
|
+
return (...args: unknown[]) => {
|
|
355
|
+
const result = Reflect.apply(value, target, args);
|
|
356
|
+
return result === target ? response : result;
|
|
357
|
+
};
|
|
358
|
+
},
|
|
359
|
+
set: (target, key, value) => Reflect.set(target, key, value, target),
|
|
360
|
+
});
|
|
361
|
+
return {
|
|
362
|
+
response,
|
|
363
|
+
takeOver: () => {
|
|
364
|
+
takenOver ||= raw.headersSent === true;
|
|
365
|
+
return takenOver;
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const PARAMS_METADATA = "nestia/McpRoute/ExternalParameters";
|
|
371
|
+
|
|
372
|
+
/** The validators' exceptions, told apart from a handler's own. */
|
|
373
|
+
const INVALID_ARGUMENTS: WeakSet<object> = new WeakSet();
|
|
374
|
+
|
|
214
375
|
const assertUniqueTools = (tools: McpAdaptor.ITool[]): void => {
|
|
215
376
|
const dict: Map<string, McpAdaptor.ITool[]> = new Map();
|
|
216
377
|
for (const tool of tools) {
|
|
@@ -255,8 +416,26 @@ const loadMcpSdk = async () => {
|
|
|
255
416
|
}
|
|
256
417
|
};
|
|
257
418
|
|
|
419
|
+
/**
|
|
420
|
+
* Types of {@link McpAdaptor}: the options of `upgrade` and the record of a
|
|
421
|
+
* registered tool.
|
|
422
|
+
*
|
|
423
|
+
* @evidence contracts/common.md#principled-implementation The class exposes one static entry point, so the adapter has no instance state: the tool list is closed over by the registered route handler.
|
|
424
|
+
* @evidence contracts/common.md#clear-and-simple-design One static method and two nested types.
|
|
425
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts It has no fixture or tool name, and the MCP SDK is loaded lazily so an application that never enables MCP does not need it.
|
|
426
|
+
* @evidence contracts/common.md#meaningful-documentation The class comment explains what an MCP route is and how to enable it.
|
|
427
|
+
* @evidenceExclude contracts/portability.md#os-neutral-implementation MCP tool discovery and HTTP transport callbacks carry protocol values; optional SDK imports use Node module resolution and no native file identity or child process is defined here.
|
|
428
|
+
*/
|
|
258
429
|
export namespace McpAdaptor {
|
|
259
|
-
/**
|
|
430
|
+
/**
|
|
431
|
+
* Configuration options for {@link McpAdaptor.upgrade}.
|
|
432
|
+
*
|
|
433
|
+
* @evidence contracts/common.md#principled-implementation The two options are the route path, with `/mcp` as the default, and the server information reported to clients, with a default name and version.
|
|
434
|
+
* @evidence contracts/common.md#clear-and-simple-design A flat record of two optional values.
|
|
435
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts It is a type; the defaults are stated in the code that reads them.
|
|
436
|
+
* @evidence contracts/common.md#meaningful-documentation Each option documents its meaning.
|
|
437
|
+
* @evidenceExclude contracts/portability.md#os-neutral-implementation MCP tool discovery and HTTP transport callbacks carry protocol values; optional SDK imports use Node module resolution and no native file identity or child process is defined here.
|
|
438
|
+
*/
|
|
260
439
|
export interface IOptions {
|
|
261
440
|
/**
|
|
262
441
|
* HTTP path where the MCP endpoint will be mounted.
|
|
@@ -278,7 +457,10 @@ export namespace McpAdaptor {
|
|
|
278
457
|
export interface ITool {
|
|
279
458
|
meta: IMcpRouteReflect;
|
|
280
459
|
source: string;
|
|
281
|
-
handler: (
|
|
282
|
-
|
|
460
|
+
handler: (input: {
|
|
461
|
+
request: unknown;
|
|
462
|
+
response: unknown;
|
|
463
|
+
args: unknown;
|
|
464
|
+
}) => Promise<unknown>;
|
|
283
465
|
}
|
|
284
466
|
}
|