@nestia/core 14.0.1 → 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/lib/adaptors/McpAdaptor.d.ts +24 -3
- package/lib/adaptors/McpAdaptor.js +72 -34
- package/lib/adaptors/McpAdaptor.js.map +1 -1
- package/lib/adaptors/WebSocketAdaptor.d.ts +37 -0
- package/lib/adaptors/WebSocketAdaptor.js +60 -16
- package/lib/adaptors/WebSocketAdaptor.js.map +1 -1
- package/lib/adaptors/internal/external_context.js +18 -5
- package/lib/adaptors/internal/external_context.js.map +1 -1
- 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 +37 -14
- package/lib/decorators/EncryptedBody.js.map +1 -1
- package/lib/decorators/EncryptedController.d.ts +6 -1
- package/lib/decorators/EncryptedController.js +6 -1
- package/lib/decorators/EncryptedController.js.map +1 -1
- package/lib/decorators/EncryptedModule.d.ts +18 -3
- package/lib/decorators/EncryptedModule.js +18 -3
- package/lib/decorators/EncryptedModule.js.map +1 -1
- package/lib/decorators/EncryptedRoute.d.ts +20 -11
- package/lib/decorators/EncryptedRoute.js +20 -11
- 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 +9 -2
- package/lib/decorators/TypedBody.js +19 -9
- package/lib/decorators/TypedBody.js.map +1 -1
- package/lib/decorators/TypedException.d.ts +10 -0
- package/lib/decorators/TypedFormData.d.ts +59 -1
- package/lib/decorators/TypedFormData.js +5 -0
- 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 +9 -0
- package/lib/decorators/TypedParam.js +9 -0
- package/lib/decorators/TypedParam.js.map +1 -1
- package/lib/decorators/TypedQuery.d.ts +16 -0
- package/lib/decorators/TypedQuery.js +16 -0
- 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/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_path_and_querify.js +3 -7
- 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/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/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 +120 -0
- 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 +40 -0
- package/lib/utils/ExceptionManager.js +34 -0
- package/lib/utils/ExceptionManager.js.map +1 -1
- package/lib/utils/SourceFinder.d.ts +22 -0
- package/lib/utils/SourceFinder.js +34 -7
- package/lib/utils/SourceFinder.js.map +1 -1
- package/lib/utils/VersioningStrategy.d.ts +36 -1
- package/lib/utils/VersioningStrategy.js +30 -9
- 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 +19 -1
- 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 +37 -3
- package/native/transform/core_transform.go +105 -98
- package/native/transform/core_websocket.go +21 -5
- package/native/transform/node_transform.go +11 -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 +21 -0
- package/package.json +5 -4
- package/src/adaptors/McpAdaptor.ts +86 -26
- package/src/adaptors/WebSocketAdaptor.ts +66 -14
- package/src/adaptors/internal/external_context.ts +37 -6
- package/src/decorators/DynamicModule.ts +12 -0
- package/src/decorators/EncryptedBody.ts +37 -13
- package/src/decorators/EncryptedController.ts +6 -1
- package/src/decorators/EncryptedModule.ts +18 -3
- package/src/decorators/EncryptedRoute.ts +20 -11
- 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 +17 -8
- package/src/decorators/TypedException.ts +10 -0
- package/src/decorators/TypedFormData.ts +59 -1
- package/src/decorators/TypedHeaders.ts +5 -0
- package/src/decorators/TypedParam.ts +9 -0
- package/src/decorators/TypedQuery.ts +16 -0
- 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/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_path_and_querify.ts +3 -9
- package/src/decorators/internal/get_path_and_stringify.ts +3 -9
- 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/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 +120 -0
- 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 +40 -0
- package/src/utils/SourceFinder.ts +38 -7
- package/src/utils/VersioningStrategy.ts +43 -13
|
@@ -159,7 +159,7 @@ func nestiaCoreCollectParameterReplacements(
|
|
|
159
159
|
}
|
|
160
160
|
canonical := nestiaCoreCanonicalSegments(context, segments)
|
|
161
161
|
kind := nestiaCoreParameterKind(canonical)
|
|
162
|
-
if kind == "" ||
|
|
162
|
+
if kind == "" || IsNestiaCoreCall(state.prog, nestiaCoreOriginalNode(state.ec, call.AsNode())) == false {
|
|
163
163
|
continue
|
|
164
164
|
}
|
|
165
165
|
candidates = append(candidates, candidate{call: call, segments: segments, kind: kind})
|
|
@@ -210,7 +210,7 @@ func nestiaCoreCollectMethodReplacements(
|
|
|
210
210
|
continue
|
|
211
211
|
}
|
|
212
212
|
canonical := nestiaCoreCanonicalSegments(context, segments)
|
|
213
|
-
if
|
|
213
|
+
if IsNestiaCoreCall(state.prog, nestiaCoreOriginalNode(state.ec, call.AsNode())) == false {
|
|
214
214
|
continue
|
|
215
215
|
}
|
|
216
216
|
// @WebSocketRoute carries no injected validator (it is not a method kind),
|
|
@@ -230,7 +230,7 @@ func nestiaCoreCollectMethodReplacements(
|
|
|
230
230
|
if nestiaCoreMcpRouteAlreadyTransformed(call) {
|
|
231
231
|
continue
|
|
232
232
|
}
|
|
233
|
-
} else if nestiaCoreShouldSkipMethodDecorator(state.prog, call) {
|
|
233
|
+
} else if nestiaCoreShouldSkipMethodDecorator(state.prog, nestiaCoreOriginalNode(state.ec, call.AsNode()).AsCallExpression()) {
|
|
234
234
|
continue
|
|
235
235
|
}
|
|
236
236
|
candidates = append(candidates, candidate{call: call, segments: segments, kind: kind})
|
|
@@ -264,13 +264,16 @@ func nestiaCoreCollectMethodReplacements(
|
|
|
264
264
|
}
|
|
265
265
|
|
|
266
266
|
// nestiaCoreOriginalNode returns the parse-tree node a (possibly synthetic) node
|
|
267
|
-
// was rebuilt from, so checker queries that need binder symbols or
|
|
268
|
-
//
|
|
267
|
+
// was rebuilt from, so checker queries that need binder symbols, signatures or
|
|
268
|
+
// types and source-text decisions resolve against the original. The rewritten
|
|
269
|
+
// call remains the emission target so typia's generated arguments are retained.
|
|
270
|
+
// core runs after typia in the shared emit pass: when typia
|
|
269
271
|
// lowers a `typia.random<T>()` call that sits in a decorator argument or method
|
|
270
272
|
// body, it rebuilds the enclosing method (and its parameters) into synthetic
|
|
271
|
-
// nodes that carry no binder symbol
|
|
272
|
-
//
|
|
273
|
-
//
|
|
273
|
+
// nodes that carry no binder symbol or checker parent context.
|
|
274
|
+
// GetResolvedSignature / GetSignatureFromDeclaration / GetTypeAtLocation can
|
|
275
|
+
// nil-panic on them, and source lookup on a synthetic call or callee has no
|
|
276
|
+
// original source span. ec.MostOriginal walks the original-link chain back
|
|
274
277
|
// to the parse node; it is a no-op for nodes core sees before typia touched them.
|
|
275
278
|
func nestiaCoreOriginalNode(ec *shimprinter.EmitContext, node *shimast.Node) *shimast.Node {
|
|
276
279
|
if ec == nil || node == nil {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
package transform
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
|
|
7
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
8
|
+
"github.com/samchon/ttsc/packages/ttsc/driver"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// SourceFilePackageName returns the nearest package-scope manifest's name for a
|
|
12
|
+
// resolved declaration source. A node_modules boundary ends the scope before
|
|
13
|
+
// its manifest is read. Missing, unreadable or malformed ownership
|
|
14
|
+
// returns an empty name; a foreign or invalid nearest manifest never falls
|
|
15
|
+
// through to an enclosing package.
|
|
16
|
+
//
|
|
17
|
+
// The loaded program filesystem owns path resolution, including its overlays
|
|
18
|
+
// and dependency observations. This performs one ancestor walk without retaining
|
|
19
|
+
// programs or caching an owner across unrelated compilation requests.
|
|
20
|
+
//
|
|
21
|
+
// @evidence contracts/common.md#principled-implementation Node's LOOKUP_PACKAGE_SCOPE ends at the node_modules path component before reading a manifest there. Within that scope the nearest manifest's decoded name decides declaration identity. A present unreadable or invalid manifest also stops resolution, so neither a manifestless dependency nor an invalid foreign owner inherits an enclosing core, rxjs, tgrid or typia owner.
|
|
22
|
+
// @evidence contracts/common.md#clear-and-simple-design One ancestor loop returns one decoded name, with nil, package-scope boundary and filesystem-root termination. All declaration provenance consumers share this operation rather than duplicating ownership policies.
|
|
23
|
+
// @evidence contracts/common.md#prohibited-implementation-shortcuts The literal node_modules component establishes Node's documented scope boundary, not package identity. No library-directory spelling, fixture name or type name substitutes for the actual scoped manifest name. Reads use the program filesystem.
|
|
24
|
+
// @evidence contracts/common.md#meaningful-documentation The comment defines scoped nearest-manifest semantics, boundary-before-read termination, empty results, overlay ownership and the absence of cross-program retention. The rule follows Node's ESM resolution LOOKUP_PACKAGE_SCOPE specification.
|
|
25
|
+
// @evidence contracts/portability.md#os-neutral-implementation filepath.Dir, Base and Join represent native path components, and the driver's filesystem performs reads/existence checks. Exact node_modules component comparison follows Node's scope algorithm; no shell path or slash-substring membership test is composed.
|
|
26
|
+
// @evidence contracts/performance.md#efficient-algorithms One read per ancestor and an existence check only after a failed read cost O(directory depth plus the first manifest's bytes). No source graph or dependency tree is recursively searched.
|
|
27
|
+
// @evidence contracts/performance.md#reuse-equivalent-work The program filesystem supplies its own coherent read/overlay observation model. This operation shares no result across programs because manifest inputs and overlays may differ between requests.
|
|
28
|
+
// @evidence contracts/performance.md#bound-retention-and-release-resources Only the current ancestor string and one manifest buffer/decoded name are retained; no program pointer, global owner map, descriptor or filesystem handle survives the call.
|
|
29
|
+
func SourceFilePackageName(prog *driver.Program, source *shimast.SourceFile) string {
|
|
30
|
+
if prog == nil || prog.FS == nil || source == nil {
|
|
31
|
+
return ""
|
|
32
|
+
}
|
|
33
|
+
for directory := filepath.Dir(source.FileName()); ; {
|
|
34
|
+
if filepath.Base(directory) == "node_modules" {
|
|
35
|
+
return ""
|
|
36
|
+
}
|
|
37
|
+
manifest := filepath.Join(directory, "package.json")
|
|
38
|
+
if contents, ok := prog.FS.ReadFile(manifest); ok {
|
|
39
|
+
var pack struct {
|
|
40
|
+
Name string `json:"name"`
|
|
41
|
+
}
|
|
42
|
+
if json.Unmarshal([]byte(contents), &pack) != nil {
|
|
43
|
+
return ""
|
|
44
|
+
}
|
|
45
|
+
return pack.Name
|
|
46
|
+
} else if prog.FS.FileExists(manifest) {
|
|
47
|
+
return ""
|
|
48
|
+
}
|
|
49
|
+
parent := filepath.Dir(directory)
|
|
50
|
+
if parent == directory {
|
|
51
|
+
return ""
|
|
52
|
+
}
|
|
53
|
+
directory = parent
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -4,8 +4,6 @@ import (
|
|
|
4
4
|
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
5
5
|
)
|
|
6
6
|
|
|
7
|
-
const nestiaKindColonToken = shimast.KindQuestionToken + 1
|
|
8
|
-
|
|
9
7
|
func normalizeNestiaSyntheticTokens(node *shimast.Node) {
|
|
10
8
|
if node == nil {
|
|
11
9
|
return
|
|
@@ -17,7 +15,7 @@ func normalizeNestiaSyntheticTokens(node *shimast.Node) {
|
|
|
17
15
|
conditional.QuestionToken = factory.NewToken(shimast.KindQuestionToken)
|
|
18
16
|
}
|
|
19
17
|
if conditional.ColonToken == nil {
|
|
20
|
-
conditional.ColonToken = factory.NewToken(
|
|
18
|
+
conditional.ColonToken = factory.NewToken(shimast.KindColonToken)
|
|
21
19
|
}
|
|
22
20
|
}
|
|
23
21
|
node.ForEachChild(func(child *shimast.Node) bool {
|
package/native/transform/run.go
CHANGED
|
@@ -25,6 +25,15 @@ var (
|
|
|
25
25
|
// decorator rewrites, then runs any statically linked contributor rewrite
|
|
26
26
|
// collectors such as the SDK metadata pass when the caller explicitly enables
|
|
27
27
|
// them.
|
|
28
|
+
//
|
|
29
|
+
// @evidence contracts/common.md#principled-implementation The command runs inside a `recover`, so an unexpected panic becomes one diagnostic line with the same shape as every other diagnostic and exit code 3 instead of a raw Go stack, and the stack is printed only when `NESTIA_NATIVE_DEBUG_STACK` is set.
|
|
30
|
+
// @evidence contracts/common.md#clear-and-simple-design One deferred handler around the private dispatcher.
|
|
31
|
+
// @evidence contracts/common.md#prohibited-implementation-shortcuts The recovery reports the failure; it does not retry or hide it, and the exit code stays non-zero.
|
|
32
|
+
// @evidence contracts/common.md#meaningful-documentation The comment explains the recovery envelope, the diagnostic shape, and the debug variable.
|
|
33
|
+
// @evidence contracts/portability.md#os-neutral-implementation Argument vectors are passed as strings to Go flag parsing without shell composition. Native working directories and file outputs are handled by os/filepath and the loaded program filesystem in the dispatched commands.
|
|
34
|
+
// @evidence contracts/performance.md#efficient-algorithms Dispatch itself is constant work; build/transform cost is owned by their program load, diagnostics and per-file emit passes, not by the recovery wrapper.
|
|
35
|
+
// @evidenceExclude contracts/performance.md#reuse-equivalent-work This operation does not coordinate equivalent requests; its result is derived from the supplied value or the current command and compiler program.
|
|
36
|
+
// @evidence contracts/performance.md#bound-retention-and-release-resources Commands defer program and publication cleanup; linked collectors remain process-owned. The debug stack is allocated only after a panic.
|
|
28
37
|
func Run(args []string) (code int) {
|
|
29
38
|
defer func() {
|
|
30
39
|
if exp := recover(); exp != nil {
|
|
@@ -57,6 +66,15 @@ func Run(args []string) (code int) {
|
|
|
57
66
|
// writers are package state, so concurrent calls would interleave; the host runs
|
|
58
67
|
// one command per process and Go tests in one package run sequentially unless
|
|
59
68
|
// they opt into t.Parallel.
|
|
69
|
+
//
|
|
70
|
+
// @evidence contracts/common.md#principled-implementation The two package writers are replaced for the duration of one call and restored by a `defer`, so a caller can observe the JSON envelope of the project-mode transform, which an exit code alone cannot show; a nil writer leaves its stream unchanged.
|
|
71
|
+
// @evidence contracts/common.md#clear-and-simple-design One function that swaps the writers, calls `Run`, and restores them.
|
|
72
|
+
// @evidence contracts/common.md#prohibited-implementation-shortcuts This is a test seam kept as an exported function because the external test module cannot reach the private writers; it changes no behavior of the command, but it mutates package state and is therefore not safe for concurrent calls, as its comment says, and it is used only by the Go test modules.
|
|
73
|
+
// @evidence contracts/common.md#meaningful-documentation The comment explains why the seam exists, its restore behavior, and the concurrency limit.
|
|
74
|
+
// @evidence contracts/portability.md#os-neutral-implementation Streams use caller supplied io.Writer values, with native file and process boundaries handled by Run and its dispatcher.
|
|
75
|
+
// @evidence contracts/performance.md#efficient-algorithms Swapping two stream references is constant work; the dispatched command determines compilation cost.
|
|
76
|
+
// @evidenceExclude contracts/performance.md#reuse-equivalent-work This operation does not coordinate equivalent requests; its result is derived from the supplied value or the current command and compiler program.
|
|
77
|
+
// @evidence contracts/performance.md#bound-retention-and-release-resources The call borrows writers and restores previous references on return or panic; it neither closes caller writers nor supports concurrent calls.
|
|
60
78
|
func RunWithOutput(args []string, out io.Writer, errOut io.Writer) int {
|
|
61
79
|
previousOut, previousErr := stdout, stderr
|
|
62
80
|
defer func() {
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
package schemaprojection
|
|
2
|
+
|
|
3
|
+
import metadata "github.com/samchon/typia/packages/typia/native/core/schemas/metadata"
|
|
4
|
+
|
|
5
|
+
// Project gives the schema writer an isolated representation
|
|
6
|
+
// of an absorbed metadata graph. Recursive tuples occupy named alias slots
|
|
7
|
+
// so the writer registers their component before visiting its children. Arrays
|
|
8
|
+
// retain their native recursive representation and every name remains the
|
|
9
|
+
// identifier allocated by the original metadata collection.
|
|
10
|
+
//
|
|
11
|
+
// The returned graph shares read-only scalar annotations but no metadata or
|
|
12
|
+
// component node whose traversal or name cache the writer can mutate. The raw
|
|
13
|
+
// graph remains the source of reflected interop data. Callers must use the
|
|
14
|
+
// returned root for both the main schema and decomposed property schemas.
|
|
15
|
+
// This type-only metadata foundation lives in the stable core host module:
|
|
16
|
+
// contributor packages are relocated by ttsc, while their host imports keep
|
|
17
|
+
// this module identity. The SDK owns baking and calls this operation across
|
|
18
|
+
// that production boundary; this package has no SDK module dependency.
|
|
19
|
+
//
|
|
20
|
+
// @evidence contracts/common.md#principled-implementation SDK analysis absorbs aliases, while the schema writer only breaks tuple cycles through named alias components. Replacing recursive tuple uses by aliases of the same collection-allocated semantic slot retains tuple elements and recursive edges without creating names or changing raw reflection. Memoized copies are registered before visiting edges, preserving shared and mutually recursive identities.
|
|
21
|
+
// @evidence contracts/common.md#clear-and-simple-design One graph projection owns the writer representation; a private visitor copies metadata and component edges, and one tuple helper separates a named use from its inline component body.
|
|
22
|
+
// @evidence contracts/common.md#prohibited-implementation-shortcuts The mapping follows recursive tuple metadata and original collection names for every input. It does not mutate foreign implementations, change absorption options, recognize fixture names, or catch schema failures.
|
|
23
|
+
// @evidence contracts/common.md#meaningful-documentation The comment explains absorbed input, recursive tuple representation, original name ownership, isolation and the requirement to share the returned root with property schema generation.
|
|
24
|
+
// @evidence contracts/performance.md#efficient-algorithms Pointer maps visit each metadata and component once, and copy each outgoing edge once, for O(V+E+annotation payload) work and space. The projection adds no checker analysis, compiler or host operation.
|
|
25
|
+
// @evidence contracts/performance.md#reuse-equivalent-work Pointer memoization reuses each original metadata and component's projection when several edges reach the same identity within this invocation. The immutable supplied graph establishes equivalence, and the caller reuses that one returned graph for root and property schema writes against shared components. Each different invocation owns fresh maps and does not reuse a projection across changed inputs.
|
|
26
|
+
// @evidence contracts/performance.md#bound-retention-and-release-resources Memo maps and copied nodes belong to one bake invocation and become reclaimable with its result; no global cache, handle or background work is retained.
|
|
27
|
+
// @evidenceExclude contracts/portability.md#os-neutral-implementation The projection visits an in-memory typia metadata graph with no native file identity or subprocess boundary.
|
|
28
|
+
func Project(input *metadata.MetadataSchema) *metadata.MetadataSchema {
|
|
29
|
+
projection := &projection{
|
|
30
|
+
metadatas: map[*metadata.MetadataSchema]*metadata.MetadataSchema{},
|
|
31
|
+
arrays: map[*metadata.MetadataArrayType]*metadata.MetadataArrayType{},
|
|
32
|
+
tuples: map[*metadata.MetadataTupleType]*metadata.MetadataTupleType{},
|
|
33
|
+
objects: map[*metadata.MetadataObjectType]*metadata.MetadataObjectType{},
|
|
34
|
+
aliases: map[*metadata.MetadataAliasType]*metadata.MetadataAliasType{},
|
|
35
|
+
tupleAliases: map[*metadata.MetadataTupleType]*metadata.MetadataAliasType{},
|
|
36
|
+
}
|
|
37
|
+
return projection.visit(input)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// projection retains invocation-local graph identities, including
|
|
41
|
+
// separate tuple component bodies and their named recursive use sites.
|
|
42
|
+
type projection struct {
|
|
43
|
+
metadatas map[*metadata.MetadataSchema]*metadata.MetadataSchema
|
|
44
|
+
arrays map[*metadata.MetadataArrayType]*metadata.MetadataArrayType
|
|
45
|
+
tuples map[*metadata.MetadataTupleType]*metadata.MetadataTupleType
|
|
46
|
+
objects map[*metadata.MetadataObjectType]*metadata.MetadataObjectType
|
|
47
|
+
aliases map[*metadata.MetadataAliasType]*metadata.MetadataAliasType
|
|
48
|
+
tupleAliases map[*metadata.MetadataTupleType]*metadata.MetadataAliasType
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// visit copies every metadata edge before a writer can reach it. Shared scalar
|
|
52
|
+
// tag and documentation payloads are read-only to the schema writer.
|
|
53
|
+
func (p *projection) visit(input *metadata.MetadataSchema) *metadata.MetadataSchema {
|
|
54
|
+
if input == nil {
|
|
55
|
+
return nil
|
|
56
|
+
}
|
|
57
|
+
if output, ok := p.metadatas[input]; ok {
|
|
58
|
+
return output
|
|
59
|
+
}
|
|
60
|
+
output := input.ShallowClone()
|
|
61
|
+
p.metadatas[input] = output
|
|
62
|
+
output.Rest = p.visit(input.Rest)
|
|
63
|
+
if input.Escaped != nil {
|
|
64
|
+
copy := *input.Escaped
|
|
65
|
+
output.Escaped = ©
|
|
66
|
+
copy.Original, copy.Returns = p.visit(input.Escaped.Original), p.visit(input.Escaped.Returns)
|
|
67
|
+
}
|
|
68
|
+
output.Atomics = copyLeaves(input.Atomics)
|
|
69
|
+
output.Constants = copyLeaves(input.Constants)
|
|
70
|
+
output.Natives = copyLeaves(input.Natives)
|
|
71
|
+
output.Templates = copyLeaves(input.Templates)
|
|
72
|
+
for i, entry := range input.Templates {
|
|
73
|
+
if entry == nil {
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
output.Templates[i].Row = make([]*metadata.MetadataSchema, len(entry.Row))
|
|
77
|
+
for j, child := range entry.Row {
|
|
78
|
+
output.Templates[i].Row[j] = p.visit(child)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
output.Functions = copyLeaves(input.Functions)
|
|
82
|
+
for i, entry := range input.Functions {
|
|
83
|
+
if entry == nil {
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
copy := output.Functions[i]
|
|
87
|
+
copy.Output = p.visit(entry.Output)
|
|
88
|
+
copy.Parameters = copyLeaves(entry.Parameters)
|
|
89
|
+
for j, parameter := range entry.Parameters {
|
|
90
|
+
if parameter != nil {
|
|
91
|
+
copy.Parameters[j].Type = p.visit(parameter.Type)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
output.Sets = copyLeaves(input.Sets)
|
|
96
|
+
for i, entry := range input.Sets {
|
|
97
|
+
if entry != nil {
|
|
98
|
+
output.Sets[i].Value = p.visit(entry.Value)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
output.Maps = copyLeaves(input.Maps)
|
|
102
|
+
for i, entry := range input.Maps {
|
|
103
|
+
if entry != nil {
|
|
104
|
+
output.Maps[i].Key, output.Maps[i].Value = p.visit(entry.Key), p.visit(entry.Value)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
output.Arrays = copyLeaves(input.Arrays)
|
|
108
|
+
for i, entry := range input.Arrays {
|
|
109
|
+
if entry == nil || entry.Type == nil {
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
component, ok := p.arrays[entry.Type]
|
|
113
|
+
if !ok {
|
|
114
|
+
copy := *entry.Type
|
|
115
|
+
component = ©
|
|
116
|
+
p.arrays[entry.Type] = component
|
|
117
|
+
component.Value = p.visit(entry.Type.Value)
|
|
118
|
+
}
|
|
119
|
+
output.Arrays[i].Type = component
|
|
120
|
+
}
|
|
121
|
+
output.Objects = copyLeaves(input.Objects)
|
|
122
|
+
for i, entry := range input.Objects {
|
|
123
|
+
if entry == nil || entry.Type == nil {
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
component, ok := p.objects[entry.Type]
|
|
127
|
+
if !ok {
|
|
128
|
+
copy := *entry.Type
|
|
129
|
+
component = ©
|
|
130
|
+
p.objects[entry.Type] = component
|
|
131
|
+
component.Properties = copyLeaves(entry.Type.Properties)
|
|
132
|
+
for j, property := range entry.Type.Properties {
|
|
133
|
+
if property != nil {
|
|
134
|
+
component.Properties[j].Key, component.Properties[j].Value = p.visit(property.Key), p.visit(property.Value)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
output.Objects[i].Type = component
|
|
139
|
+
}
|
|
140
|
+
output.Aliases = copyLeaves(input.Aliases)
|
|
141
|
+
for i, entry := range input.Aliases {
|
|
142
|
+
if entry == nil || entry.Type == nil {
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
145
|
+
component, ok := p.aliases[entry.Type]
|
|
146
|
+
if !ok {
|
|
147
|
+
copy := *entry.Type
|
|
148
|
+
component = ©
|
|
149
|
+
p.aliases[entry.Type] = component
|
|
150
|
+
component.Value = p.visit(entry.Type.Value)
|
|
151
|
+
}
|
|
152
|
+
output.Aliases[i].Type = component
|
|
153
|
+
}
|
|
154
|
+
output.Tuples = make([]*metadata.MetadataTuple, 0, len(input.Tuples))
|
|
155
|
+
for _, entry := range input.Tuples {
|
|
156
|
+
if entry == nil || entry.Type == nil {
|
|
157
|
+
output.Tuples = append(output.Tuples, entry)
|
|
158
|
+
continue
|
|
159
|
+
}
|
|
160
|
+
if entry.Type.Recursive {
|
|
161
|
+
alias, ok := p.tupleAliases[entry.Type]
|
|
162
|
+
if !ok {
|
|
163
|
+
alias = metadata.MetadataAliasType_create(metadata.MetadataAliasType{
|
|
164
|
+
Name: entry.Type.Name, DisplayName: entry.Type.DisplayName, Recursive: true, Nullables: entry.Type.Nullables,
|
|
165
|
+
})
|
|
166
|
+
p.tupleAliases[entry.Type] = alias
|
|
167
|
+
body := metadata.MetadataSchema_initialize()
|
|
168
|
+
alias.Value = body
|
|
169
|
+
copy := *entry
|
|
170
|
+
copy.Type = p.tuple(entry.Type)
|
|
171
|
+
body.Tuples = []*metadata.MetadataTuple{©}
|
|
172
|
+
}
|
|
173
|
+
output.Aliases = append(output.Aliases, metadata.MetadataAlias_create(metadata.MetadataAlias{Type: alias, Tags: entry.Tags}))
|
|
174
|
+
} else {
|
|
175
|
+
copy := *entry
|
|
176
|
+
copy.Type = p.tuple(entry.Type)
|
|
177
|
+
output.Tuples = append(output.Tuples, ©)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return output
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// tuple copies a structural component after registering its identity, allowing
|
|
184
|
+
// its child metadata to return through the already registered named alias.
|
|
185
|
+
func (p *projection) tuple(input *metadata.MetadataTupleType) *metadata.MetadataTupleType {
|
|
186
|
+
if output, ok := p.tuples[input]; ok {
|
|
187
|
+
return output
|
|
188
|
+
}
|
|
189
|
+
copy := *input
|
|
190
|
+
output := ©
|
|
191
|
+
p.tuples[input] = output
|
|
192
|
+
output.Elements = make([]*metadata.MetadataSchema, len(input.Elements))
|
|
193
|
+
for i, element := range input.Elements {
|
|
194
|
+
output.Elements[i] = p.visit(element)
|
|
195
|
+
}
|
|
196
|
+
return output
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// copyLeaves isolates node-local name caches. Scalar annotation
|
|
200
|
+
// slices remain shared because the public schema writer only reads them.
|
|
201
|
+
func copyLeaves[T any](input []*T) []*T {
|
|
202
|
+
if input == nil {
|
|
203
|
+
return nil
|
|
204
|
+
}
|
|
205
|
+
output := make([]*T, len(input))
|
|
206
|
+
for i, item := range input {
|
|
207
|
+
if item != nil {
|
|
208
|
+
copy := *item
|
|
209
|
+
output[i] = ©
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return output
|
|
213
|
+
}
|
|
@@ -7,6 +7,7 @@ import (
|
|
|
7
7
|
"fmt"
|
|
8
8
|
"os"
|
|
9
9
|
"path/filepath"
|
|
10
|
+
"reflect"
|
|
10
11
|
"strings"
|
|
11
12
|
|
|
12
13
|
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
@@ -251,6 +252,19 @@ func writeSingleOutput(text, outPath string) int {
|
|
|
251
252
|
return 0
|
|
252
253
|
}
|
|
253
254
|
|
|
255
|
+
// SourceFileText returns the text supplied by a present Text() provider.
|
|
256
|
+
// Missing values, including typed nil receivers, and values without Text()
|
|
257
|
+
// return empty text and false without invoking a method. Errors from a present
|
|
258
|
+
// provider's Text() method propagate to its caller.
|
|
259
|
+
//
|
|
260
|
+
// @evidence contracts/common.md#principled-implementation Structural Text() support establishes the provider operation, while a guarded reflect nil-identity check establishes that a receiver exists before dispatch. Every nil-capable kind follows the same absent-source rule; non-nil providers return their own text unchanged.
|
|
261
|
+
// @evidence contracts/common.md#clear-and-simple-design One structural type assertion, one constant-time receiver-presence check restricted to the kinds for which Go permits IsNil, and one method dispatch separate absent sources from actual provider behavior.
|
|
262
|
+
// @evidence contracts/common.md#prohibited-implementation-shortcuts The nil rule applies to all structural providers without compiler-type names or recovery. It does not invoke missing receivers, mutate foreign providers or conceal errors raised by present providers.
|
|
263
|
+
// @evidence contracts/common.md#meaningful-documentation The comment states exact absent-value behavior, typed nil receiver treatment and propagation of present-provider errors.
|
|
264
|
+
// @evidenceExclude contracts/portability.md#os-neutral-implementation The Text provider is an in-memory structural interface; the helper does not read a native source file.
|
|
265
|
+
// @evidence contracts/performance.md#efficient-algorithms Type and nil checks are constant work; obtaining the string has the provider-defined cost.
|
|
266
|
+
// @evidenceExclude contracts/performance.md#reuse-equivalent-work This operation does not coordinate equivalent requests; its result is derived from the supplied value or the current command and compiler program.
|
|
267
|
+
// @evidence contracts/performance.md#bound-retention-and-release-resources The provider owns source text and the returned string may retain those bytes; no global provider reference is stored.
|
|
254
268
|
func SourceFileText(target any) (string, bool) {
|
|
255
269
|
type sourceText interface {
|
|
256
270
|
Text() string
|
|
@@ -259,6 +273,13 @@ func SourceFileText(target any) (string, bool) {
|
|
|
259
273
|
if !ok {
|
|
260
274
|
return "", false
|
|
261
275
|
}
|
|
276
|
+
value := reflect.ValueOf(file)
|
|
277
|
+
switch value.Kind() {
|
|
278
|
+
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice, reflect.UnsafePointer:
|
|
279
|
+
if value.IsNil() {
|
|
280
|
+
return "", false
|
|
281
|
+
}
|
|
282
|
+
}
|
|
262
283
|
return file.Text(), true
|
|
263
284
|
}
|
|
264
285
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nestia/core",
|
|
3
|
-
"version": "14.0.
|
|
3
|
+
"version": "14.0.2",
|
|
4
4
|
"description": "Super-fast validation decorators of NestJS",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"tgrid": "^1.1.0",
|
|
54
54
|
"typia": "15.0.0",
|
|
55
55
|
"ws": "^7.5.3",
|
|
56
|
-
"@nestia/fetcher": "^14.0.
|
|
56
|
+
"@nestia/fetcher": "^14.0.2"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
59
|
"@modelcontextprotocol/sdk": "^1.18.0",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"rxjs": ">=7.1.0",
|
|
64
64
|
"ttsc": ">=0.19.2",
|
|
65
65
|
"typia": "15.0.0",
|
|
66
|
-
"@nestia/fetcher": "^14.0.
|
|
66
|
+
"@nestia/fetcher": "^14.0.2"
|
|
67
67
|
},
|
|
68
68
|
"peerDependenciesMeta": {
|
|
69
69
|
"@modelcontextprotocol/sdk": {
|
|
@@ -107,7 +107,8 @@
|
|
|
107
107
|
"scripts": {
|
|
108
108
|
"build": "rimraf lib && ttsc",
|
|
109
109
|
"dev": "ttsc --watch",
|
|
110
|
-
"test:go": "cd test && go test -count=1 ./..."
|
|
110
|
+
"test:go": "cd test && go test -count=1 ./...",
|
|
111
|
+
"evidence": "evidence"
|
|
111
112
|
},
|
|
112
113
|
"types": "lib/index.d.ts"
|
|
113
114
|
}
|
|
@@ -11,6 +11,7 @@ import { InstanceWrapper } from "@nestjs/core/injector/instance-wrapper";
|
|
|
11
11
|
import { Module } from "@nestjs/core/injector/module";
|
|
12
12
|
|
|
13
13
|
import { IMcpRouteReflect } from "../decorators/internal/IMcpRouteReflect";
|
|
14
|
+
import { resolve_request_body } from "../decorators/internal/validate_request_body";
|
|
14
15
|
import {
|
|
15
16
|
create_external_context_creator,
|
|
16
17
|
get_request_context_id,
|
|
@@ -47,14 +48,12 @@ import {
|
|
|
47
48
|
*
|
|
48
49
|
* @author wildduck - https://github.com/wildduck2
|
|
49
50
|
* @example
|
|
50
|
-
* ```typescript
|
|
51
51
|
* import core from "@nestia/core";
|
|
52
52
|
* import { NestFactory } from "@nestjs/core";
|
|
53
53
|
*
|
|
54
54
|
* const app = await NestFactory.create(AppModule);
|
|
55
55
|
* await core.McpAdaptor.upgrade(app, { path: "/mcp" });
|
|
56
56
|
* await app.listen(3000);
|
|
57
|
-
* ```;
|
|
58
57
|
*/
|
|
59
58
|
export class McpAdaptor {
|
|
60
59
|
/**
|
|
@@ -71,6 +70,11 @@ export class McpAdaptor {
|
|
|
71
70
|
*
|
|
72
71
|
* @param app Running Nest application instance.
|
|
73
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.
|
|
74
78
|
*/
|
|
75
79
|
public static async upgrade(
|
|
76
80
|
app: INestApplication,
|
|
@@ -101,7 +105,8 @@ export class McpAdaptor {
|
|
|
101
105
|
for (const key of Object.getOwnPropertyNames(proto)) {
|
|
102
106
|
if (key === "constructor" || visited.has(key)) continue;
|
|
103
107
|
visited.add(key);
|
|
104
|
-
|
|
108
|
+
// Route discovery inspects data descriptors without invoking accessors.
|
|
109
|
+
const method = Object.getOwnPropertyDescriptor(proto, key)?.value;
|
|
105
110
|
if (typeof method !== "function") continue;
|
|
106
111
|
|
|
107
112
|
const meta: IMcpRouteReflect | undefined = Reflect.getMetadata(
|
|
@@ -154,6 +159,7 @@ export class McpAdaptor {
|
|
|
154
159
|
capabilities: { tools: {} },
|
|
155
160
|
});
|
|
156
161
|
const server = mcp.server;
|
|
162
|
+
const relay = transportResponse(res.raw ?? res);
|
|
157
163
|
|
|
158
164
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
159
165
|
tools: tools.map((t) => ({
|
|
@@ -181,7 +187,7 @@ export class McpAdaptor {
|
|
|
181
187
|
response: res,
|
|
182
188
|
args,
|
|
183
189
|
});
|
|
184
|
-
if (
|
|
190
|
+
if (relay.takeOver()) return { content: [] };
|
|
185
191
|
if (result instanceof Error) throw result;
|
|
186
192
|
if (result === undefined) return { content: [] };
|
|
187
193
|
return {
|
|
@@ -194,7 +200,7 @@ export class McpAdaptor {
|
|
|
194
200
|
],
|
|
195
201
|
};
|
|
196
202
|
} catch (e) {
|
|
197
|
-
if (
|
|
203
|
+
if (relay.takeOver()) return { content: [] };
|
|
198
204
|
if (INVALID_ARGUMENTS.has(e as object)) {
|
|
199
205
|
const body = (e as BadRequestException).getResponse() as any;
|
|
200
206
|
throw new McpError(ErrorCode.InvalidParams, (e as Error).message, {
|
|
@@ -224,7 +230,7 @@ export class McpAdaptor {
|
|
|
224
230
|
});
|
|
225
231
|
try {
|
|
226
232
|
await mcp.connect(transport);
|
|
227
|
-
await transport.handleRequest(req.raw ?? req,
|
|
233
|
+
await transport.handleRequest(req.raw ?? req, relay.response, req.body);
|
|
228
234
|
} finally {
|
|
229
235
|
await transport.close().catch(() => {});
|
|
230
236
|
await mcp.close().catch(() => {});
|
|
@@ -242,7 +248,9 @@ export class McpAdaptor {
|
|
|
242
248
|
* The arguments reach the method through the pipes as its body would, validated
|
|
243
249
|
* by typia at that stage: after the guards, as `@TypedBody()` is. An invalid
|
|
244
250
|
* argument is thrown as the validator's `BadRequestException`, which an
|
|
245
|
-
* exception filter may map; unmapped, it becomes JSON-RPC `-32602`.
|
|
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.
|
|
246
254
|
*
|
|
247
255
|
* A controller that is request-scoped, itself or through an enhancer or a
|
|
248
256
|
* dependency, is built per request with its enhancers, as NestJS builds it for
|
|
@@ -275,10 +283,11 @@ const createHandler = (props: {
|
|
|
275
283
|
{
|
|
276
284
|
// [request, response, next] as an HTTP route has, then the arguments
|
|
277
285
|
exchangeKeyForValue: (_type, _data, [, , , args]) => {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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;
|
|
282
291
|
},
|
|
283
292
|
},
|
|
284
293
|
contextId,
|
|
@@ -307,22 +316,55 @@ const createHandler = (props: {
|
|
|
307
316
|
};
|
|
308
317
|
|
|
309
318
|
/**
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
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.
|
|
313
323
|
*/
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
+
},
|
|
324
367
|
};
|
|
325
|
-
return true;
|
|
326
368
|
};
|
|
327
369
|
|
|
328
370
|
const PARAMS_METADATA = "nestia/McpRoute/ExternalParameters";
|
|
@@ -374,8 +416,26 @@ const loadMcpSdk = async () => {
|
|
|
374
416
|
}
|
|
375
417
|
};
|
|
376
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
|
+
*/
|
|
377
429
|
export namespace McpAdaptor {
|
|
378
|
-
/**
|
|
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
|
+
*/
|
|
379
439
|
export interface IOptions {
|
|
380
440
|
/**
|
|
381
441
|
* HTTP path where the MCP endpoint will be mounted.
|