@adhisang/minecraft-modding-mcp 6.3.0 → 7.0.0-rc.1
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/CHANGELOG.md +90 -0
- package/README.md +13 -3
- package/dist/cache-policy.d.ts +71 -0
- package/dist/cache-policy.js +83 -0
- package/dist/cache-registry.js +45 -8
- package/dist/cli.js +74 -3
- package/dist/compat-stdio-transport.d.ts +1 -1
- package/dist/compat-stdio-transport.js +13 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.js +8 -2
- package/dist/decompiler/vineflower.d.ts +1 -0
- package/dist/decompiler/vineflower.js +8 -5
- package/dist/entry-tools/analyze-mod-service.d.ts +70 -136
- package/dist/entry-tools/analyze-symbol-service.d.ts +112 -150
- package/dist/entry-tools/compare-minecraft-service.d.ts +59 -145
- package/dist/entry-tools/entry-tool-schema.d.ts +38 -4
- package/dist/entry-tools/entry-tool-schema.js +4 -1
- package/dist/entry-tools/inspect-minecraft/internal.d.ts +235 -799
- package/dist/entry-tools/inspect-minecraft/internal.js +50 -13
- package/dist/entry-tools/inspect-minecraft-service.d.ts +372 -1736
- package/dist/entry-tools/manage-cache-service.d.ts +81 -91
- package/dist/entry-tools/validate-project/cases/mixin.js +26 -6
- package/dist/entry-tools/validate-project/cases/project-summary.d.ts +7 -7
- package/dist/entry-tools/validate-project-service.d.ts +164 -592
- package/dist/entry-tools/verify-mixin-target-service.d.ts +3 -19
- package/dist/entry-tools/verify-mixin-target-service.js +19 -1
- package/dist/era-classifier.d.ts +161 -0
- package/dist/era-classifier.js +292 -0
- package/dist/error-mapping.d.ts +76 -0
- package/dist/error-mapping.js +116 -8
- package/dist/index.d.ts +42 -4
- package/dist/index.js +636 -473
- package/dist/java-process.d.ts +2 -0
- package/dist/java-process.js +22 -2
- package/dist/json-rpc-framing.d.ts +77 -1
- package/dist/json-rpc-framing.js +249 -13
- package/dist/mapping/loaders/tiny-loom-selection.d.ts +88 -0
- package/dist/mapping/loaders/tiny-loom-selection.js +223 -0
- package/dist/mapping/loaders/tiny-loom.js +45 -33
- package/dist/mapping/loaders/tiny-maven.js +6 -11
- package/dist/mapping/parsers/tiny.d.ts +57 -0
- package/dist/mapping/parsers/tiny.js +99 -22
- package/dist/mapping-service.d.ts +19 -0
- package/dist/mapping-service.js +93 -9
- package/dist/maven-resolver.d.ts +18 -0
- package/dist/maven-resolver.js +20 -0
- package/dist/mcp-helpers.d.ts +19 -2
- package/dist/mcp-helpers.js +58 -9
- package/dist/minecraft-explorer-service.d.ts +1 -1
- package/dist/mixin/types.d.ts +8 -0
- package/dist/mod-analyzer.js +7 -7
- package/dist/mod-decompile-service.js +1 -0
- package/dist/nbt/java-nbt-codec.js +12 -2
- package/dist/nbt/json-patch.js +14 -3
- package/dist/nbt/pipeline.js +40 -3
- package/dist/nbt/typed-json.js +26 -1
- package/dist/registration-adapter.d.ts +32 -0
- package/dist/registration-adapter.js +52 -0
- package/dist/repo-downloader.d.ts +165 -0
- package/dist/repo-downloader.js +568 -10
- package/dist/request-context.d.ts +7 -0
- package/dist/request-context.js +9 -0
- package/dist/resources.d.ts +1 -1
- package/dist/resources.js +25 -19
- package/dist/server-identity.d.ts +27 -0
- package/dist/server-identity.js +26 -0
- package/dist/source/access-validate.js +53 -0
- package/dist/source/artifact-resolver.d.ts +81 -2
- package/dist/source/artifact-resolver.js +227 -15
- package/dist/source/class-source.d.ts +36 -0
- package/dist/source/class-source.js +222 -38
- package/dist/source/did-you-mean.d.ts +12 -1
- package/dist/source/did-you-mean.js +6 -2
- package/dist/source/file-access.js +150 -46
- package/dist/source/indexer.js +1 -0
- package/dist/source/shared-utils.d.ts +21 -0
- package/dist/source/shared-utils.js +23 -0
- package/dist/source-resolver.js +224 -57
- package/dist/source-service.d.ts +12 -1
- package/dist/stdio-supervisor.d.ts +357 -2
- package/dist/stdio-supervisor.js +1031 -80
- package/dist/storage/db.d.ts +2 -1
- package/dist/storage/db.js +15 -8
- package/dist/synthetic-decorator.d.ts +24 -0
- package/dist/synthetic-decorator.js +48 -0
- package/dist/tool-guidance.d.ts +17 -1
- package/dist/tool-guidance.js +323 -18
- package/dist/tool-schema-registry.d.ts +2 -0
- package/dist/tool-schema-registry.js +4 -0
- package/dist/tool-schemas.d.ts +2212 -3919
- package/dist/tool-schemas.js +33 -7
- package/dist/types.d.ts +35 -0
- package/dist/v1-parity-schemas.d.ts +7 -0
- package/dist/v1-parity-schemas.js +5584 -0
- package/dist/version-diff-service.d.ts +33 -0
- package/dist/version-diff-service.js +148 -3
- package/dist/version-service.js +36 -14
- package/dist/warning-details.js +18 -1
- package/docs/README-ja.md +5 -3
- package/docs/tool-reference.md +196 -19
- package/package.json +13 -8
|
@@ -137,29 +137,13 @@ export declare class VerifyMixinTargetService {
|
|
|
137
137
|
*/
|
|
138
138
|
private applyAutoRemap;
|
|
139
139
|
}
|
|
140
|
-
export declare const verifyMixinTargetMemberSchemaExport: z.ZodDiscriminatedUnion<
|
|
140
|
+
export declare const verifyMixinTargetMemberSchemaExport: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
141
141
|
kind: z.ZodLiteral<"method">;
|
|
142
142
|
name: z.ZodString;
|
|
143
143
|
descriptor: z.ZodOptional<z.ZodString>;
|
|
144
|
-
},
|
|
145
|
-
name: string;
|
|
146
|
-
kind: "method";
|
|
147
|
-
descriptor?: string | undefined;
|
|
148
|
-
}, {
|
|
149
|
-
name: string;
|
|
150
|
-
kind: "method";
|
|
151
|
-
descriptor?: string | undefined;
|
|
152
|
-
}>, z.ZodObject<{
|
|
144
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
153
145
|
kind: z.ZodLiteral<"field">;
|
|
154
146
|
name: z.ZodString;
|
|
155
147
|
descriptor: z.ZodOptional<z.ZodString>;
|
|
156
|
-
},
|
|
157
|
-
name: string;
|
|
158
|
-
kind: "field";
|
|
159
|
-
descriptor?: string | undefined;
|
|
160
|
-
}, {
|
|
161
|
-
name: string;
|
|
162
|
-
kind: "field";
|
|
163
|
-
descriptor?: string | undefined;
|
|
164
|
-
}>]>;
|
|
148
|
+
}, z.core.$strip>], "kind">;
|
|
165
149
|
export {};
|
|
@@ -221,7 +221,25 @@ export class VerifyMixinTargetService {
|
|
|
221
221
|
throw createError({
|
|
222
222
|
code: ERROR_CODES.CONTEXT_UNRESOLVED,
|
|
223
223
|
message: `verify-mixin-target requires a binary jar but artifact "${resolved.artifactId}" has none.`,
|
|
224
|
-
details: {
|
|
224
|
+
details: {
|
|
225
|
+
artifactId: resolved.artifactId,
|
|
226
|
+
owner,
|
|
227
|
+
// `ERR_CONTEXT_UNRESOLVED` classifies as `code_issue` by code, which
|
|
228
|
+
// is right for the sibling case (a caller naming a version no
|
|
229
|
+
// artifact carries) and for a `target: { kind: "jar", ... }` request:
|
|
230
|
+
// there the caller named the exact jar, and having none for its own
|
|
231
|
+
// sources is exactly the input the caller can change. It is wrong
|
|
232
|
+
// only when the artifact was picked by the TOOL from a non-jar target
|
|
233
|
+
// (version/coordinate/workspace/dependency), where whether it ships a
|
|
234
|
+
// binary jar is not something the request can express. Published as
|
|
235
|
+
// caller-fixable in that case it invites an endless retry of an input
|
|
236
|
+
// that was never at fault, so this site overrides the default only
|
|
237
|
+
// then.
|
|
238
|
+
issueOrigin: input.target?.kind === "jar" ? undefined : "tool_issue",
|
|
239
|
+
nextAction: `verify-mixin-target reads the target's members from bytecode, so it needs an artifact with a binary jar. `
|
|
240
|
+
+ `Re-target with target: { kind: "jar", value: "<path to the jar>" }, or with a version whose artifact carries one `
|
|
241
|
+
+ `(resolve-artifact reports binaryJarPath for the target you pass).`
|
|
242
|
+
}
|
|
225
243
|
});
|
|
226
244
|
}
|
|
227
245
|
let signature;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { type JSONRPCMessage, type JSONRPCResponse } from "@modelcontextprotocol/server";
|
|
2
|
+
/**
|
|
3
|
+
* Era classification for the dual-era stdio supervisor.
|
|
4
|
+
*
|
|
5
|
+
* The process serves TWO protocol eras: the legacy
|
|
6
|
+
* initialize/notifications/initialized handshake (five protocol versions) and
|
|
7
|
+
* the modern stateless era (protocol revision 2026-07-28) whose requests carry
|
|
8
|
+
* a per-request `_meta` envelope. The SUPERVISOR is the sole era gatekeeper at
|
|
9
|
+
* admission — the worker's own era classification is unreliable for gating (a
|
|
10
|
+
* claim-less frame silently pins the worker connection legacy) — so the
|
|
11
|
+
* shallow envelope check, the era types, and the machine-readable rejection
|
|
12
|
+
* builders live here.
|
|
13
|
+
*/
|
|
14
|
+
/** Reserved `_meta` key carrying the per-request protocol version claim. */
|
|
15
|
+
export declare const PROTOCOL_VERSION_META_KEY = "io.modelcontextprotocol/protocolVersion";
|
|
16
|
+
/** Reserved `_meta` key carrying the per-request client capabilities. */
|
|
17
|
+
export declare const CLIENT_CAPABILITIES_META_KEY = "io.modelcontextprotocol/clientCapabilities";
|
|
18
|
+
/**
|
|
19
|
+
* Reserved `_meta` key carrying the OPTIONAL per-request client
|
|
20
|
+
* implementation info. NOT part of the era-signal check; only captured (as-is)
|
|
21
|
+
* into request snapshots when present on a shallow-valid modern signal.
|
|
22
|
+
*/
|
|
23
|
+
export declare const CLIENT_INFO_META_KEY = "io.modelcontextprotocol/clientInfo";
|
|
24
|
+
/** The five legacy handshake protocol versions the worker supports. */
|
|
25
|
+
export declare const LEGACY_PROTOCOL_VERSIONS: readonly ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05", "2024-10-07"];
|
|
26
|
+
/** The modern per-request `_meta` protocol revision. */
|
|
27
|
+
export declare const MODERN_PROTOCOL_VERSION = "2026-07-28";
|
|
28
|
+
/** All protocol versions this process serves, across both eras. */
|
|
29
|
+
export declare const ERA_SUPPORTED_PROTOCOL_VERSIONS: readonly string[];
|
|
30
|
+
/** Process-lifetime era state. The lock is one-way and survives worker restarts. */
|
|
31
|
+
export type Era = "unselected" | "legacy" | "modern";
|
|
32
|
+
export type EraSignalClassification = "modern-signal" | "claim-less" | "claim-shaped-invalid";
|
|
33
|
+
export type EraSignal = {
|
|
34
|
+
classification: EraSignalClassification;
|
|
35
|
+
/** Absent required io.modelcontextprotocol/* keys. */
|
|
36
|
+
missing: string[];
|
|
37
|
+
/** Present-but-wrong-shallow-type required io.modelcontextprotocol/* keys. */
|
|
38
|
+
invalid: string[];
|
|
39
|
+
};
|
|
40
|
+
type RequestId = string | number;
|
|
41
|
+
/**
|
|
42
|
+
* Shallow era-signal classification of a request's (or notification's)
|
|
43
|
+
* params. Mirrors the SDK RequestMetaEnvelopeSchema's REQUIREDNESS without
|
|
44
|
+
* deep value validation: `params._meta` must be an object carrying a string
|
|
45
|
+
* `io.modelcontextprotocol/protocolVersion` and a non-null non-array object
|
|
46
|
+
* `io.modelcontextprotocol/clientCapabilities`. `io.modelcontextprotocol/clientInfo`
|
|
47
|
+
* is optional and NOT part of the check.
|
|
48
|
+
*
|
|
49
|
+
* A shallow-VALID envelope is the modern era signal even when the version
|
|
50
|
+
* VALUE is unsupported (e.g. "2027-01-01") — deep value validation belongs to
|
|
51
|
+
* the worker, which answers -32022 with {supported, requested}.
|
|
52
|
+
*
|
|
53
|
+
* claim-less: params/_meta missing, `_meta` not a plain object, or `_meta`
|
|
54
|
+
* carrying NEITHER required key. claim-shaped-invalid: at least one required
|
|
55
|
+
* key present but the shallow check fails (`missing` = absent required keys,
|
|
56
|
+
* `invalid` = present-but-wrong-shallow-type keys).
|
|
57
|
+
*/
|
|
58
|
+
export declare function classifyEraSignal(params: unknown): EraSignal;
|
|
59
|
+
/**
|
|
60
|
+
* Per-request modern protocol context, captured SHALLOW from `params._meta`
|
|
61
|
+
* at supervisor admission. Values are copied as-is (no deep validation — the
|
|
62
|
+
* worker validates values, e.g. answering -32022 for an unsupported
|
|
63
|
+
* protocolVersion). `clientInfo` is present only when the optional
|
|
64
|
+
* io.modelcontextprotocol/clientInfo key is present, and is passed through
|
|
65
|
+
* verbatim even when it is not Implementation-shaped.
|
|
66
|
+
*/
|
|
67
|
+
export type ModernRequestContext = {
|
|
68
|
+
protocolVersion: string;
|
|
69
|
+
clientCapabilities: Record<string, unknown>;
|
|
70
|
+
clientInfo?: unknown;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Shallow extraction of the {@link ModernRequestContext} from a request's
|
|
74
|
+
* params. Returns undefined unless the params classify as a shallow-valid
|
|
75
|
+
* modern signal — the guard is the same pure {@link classifyEraSignal} check
|
|
76
|
+
* the admission gate runs, so it cannot diverge from an admission-time
|
|
77
|
+
* classification of the same params object. The returned values ALIAS the
|
|
78
|
+
* inbound params._meta objects (no clone): callers must never mutate an
|
|
79
|
+
* inbound params graph after admission, or captured snapshots would change
|
|
80
|
+
* retroactively.
|
|
81
|
+
*/
|
|
82
|
+
export declare function extractModernRequestContext(params: unknown): ModernRequestContext | undefined;
|
|
83
|
+
/**
|
|
84
|
+
* Strips the modern era-claim keys from a request's `params._meta` IN PLACE,
|
|
85
|
+
* deleting `_meta` entirely when nothing else remains. Used by the supervisor
|
|
86
|
+
* for accepted legacy `initialize` frames: the admission rule classifies every
|
|
87
|
+
* initialize as the legacy era signal regardless of its envelope, but the
|
|
88
|
+
* SDK's opening classifier treats an initialize carrying a valid modern claim
|
|
89
|
+
* as MODERN — the strip keeps the worker's classification aligned with the
|
|
90
|
+
* admission decision. Mutation is in place deliberately: the framing-mode
|
|
91
|
+
* registry is keyed by the exact inbound frame object, and no
|
|
92
|
+
* {@link ModernRequestContext} ever aliases an initialize's `_meta` (an
|
|
93
|
+
* initialize never classifies as a modern signal at admission).
|
|
94
|
+
*/
|
|
95
|
+
export declare function stripModernEraClaimInPlace(params: unknown): void;
|
|
96
|
+
export declare const ERA_CONFLICT_MODERN_MESSAGE: string;
|
|
97
|
+
export declare const ERA_CONFLICT_LEGACY_MESSAGE: string;
|
|
98
|
+
export declare const MISSING_META_UNSELECTED_MESSAGE: string;
|
|
99
|
+
export declare const MISSING_META_MODERN_MESSAGE: string;
|
|
100
|
+
/**
|
|
101
|
+
* One-way era conflict rejection. selectedEra "modern" rejects a legacy
|
|
102
|
+
* initialize with -32601; selectedEra "legacy" rejects a modern-signal
|
|
103
|
+
* request with -32600. Machine-readable discrimination is by code +
|
|
104
|
+
* data.kind/selectedEra/requestedEra/supported only.
|
|
105
|
+
*/
|
|
106
|
+
export declare function buildEraConflictRejection(id: RequestId, selectedEra: "legacy" | "modern"): JSONRPCResponse;
|
|
107
|
+
/**
|
|
108
|
+
* Rejection for a request carrying no valid era signal: in the unselected
|
|
109
|
+
* state (era stays UNSELECTED; the message names both recovery paths) or in
|
|
110
|
+
* the modern era (the message names the required envelope). `invalid` is
|
|
111
|
+
* included only when non-empty; `missing` is always present.
|
|
112
|
+
*/
|
|
113
|
+
export declare function buildMissingMetaRejection(id: RequestId, signal: EraSignal, era: "unselected" | "modern"): JSONRPCResponse;
|
|
114
|
+
/**
|
|
115
|
+
* Whether a frame is a COMPLETE, schema-valid MCP `initialize` request.
|
|
116
|
+
*
|
|
117
|
+
* Admission classifies any frame whose method is `initialize` as the legacy
|
|
118
|
+
* era signal, and that lock is one-way for the process lifetime. Generic
|
|
119
|
+
* JSON-RPC parsing is far too weak a basis for a permanent decision: a frame
|
|
120
|
+
* as empty as `{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}`
|
|
121
|
+
* passes it while carrying none of the fields the handshake needs. Burning the
|
|
122
|
+
* lock on such a frame left the process legacy-locked with NO valid era
|
|
123
|
+
* opening ever completed, and the modern era permanently unreachable.
|
|
124
|
+
*
|
|
125
|
+
* The check is the SDK's own public `isInitializeRequest` — the exact
|
|
126
|
+
* `InitializeRequestSchema` the worker would apply — so admission and the
|
|
127
|
+
* worker can never disagree about what an initialize is. Extra params
|
|
128
|
+
* (including a `_meta` envelope) are tolerated; `protocolVersion` (string),
|
|
129
|
+
* `capabilities` (object) and `clientInfo` ({name, version}) are required.
|
|
130
|
+
* Version VALUES are not judged here: an unknown legacy version negotiates
|
|
131
|
+
* down at the worker, exactly as documented.
|
|
132
|
+
*/
|
|
133
|
+
export declare function isCompleteInitializeRequest(message: JSONRPCMessage): boolean;
|
|
134
|
+
export declare const INVALID_INITIALIZE_MESSAGE: string;
|
|
135
|
+
/**
|
|
136
|
+
* Rejection for a frame that claims to be an `initialize` but fails the MCP
|
|
137
|
+
* initialize schema. Answered at admission, BEFORE the legacy lock is
|
|
138
|
+
* committed, so the era stays UNSELECTED and every recovery path — including
|
|
139
|
+
* the modern era — remains open.
|
|
140
|
+
*/
|
|
141
|
+
export declare function buildInvalidInitializeRejection(id: RequestId): JSONRPCResponse;
|
|
142
|
+
/**
|
|
143
|
+
* Unsupported modern `protocolVersion` VALUE rejection (`-32022` with
|
|
144
|
+
* `data.supported` / `data.requested`).
|
|
145
|
+
*
|
|
146
|
+
* Built from the SDK's own {@link UnsupportedProtocolVersionError} so the
|
|
147
|
+
* code, message and data stay byte-identical to the worker-produced answer
|
|
148
|
+
* clients already receive on the pre-pin path — the supervisor merely makes
|
|
149
|
+
* the check apply to EVERY modern request instead of only the one that
|
|
150
|
+
* happened to open the connection.
|
|
151
|
+
*/
|
|
152
|
+
export declare function buildUnsupportedProtocolVersionRejection(id: RequestId, requested: string): JSONRPCResponse;
|
|
153
|
+
/**
|
|
154
|
+
* Plain -32601 admission rejection for subscriptions/listen in the modern era
|
|
155
|
+
* (no data.kind — deliberately indistinguishable from an ordinary unknown
|
|
156
|
+
* method). Defense in depth: if admission were ever bypassed, the worker
|
|
157
|
+
* (maxSubscriptions: 0) would answer -32603 "Subscription limit reached";
|
|
158
|
+
* intercepting at admission keeps that internal detail off the wire.
|
|
159
|
+
*/
|
|
160
|
+
export declare function buildMethodNotFoundRejection(id: RequestId): JSONRPCResponse;
|
|
161
|
+
export {};
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { UnsupportedProtocolVersionError, isInitializeRequest } from "@modelcontextprotocol/server";
|
|
2
|
+
/**
|
|
3
|
+
* Era classification for the dual-era stdio supervisor.
|
|
4
|
+
*
|
|
5
|
+
* The process serves TWO protocol eras: the legacy
|
|
6
|
+
* initialize/notifications/initialized handshake (five protocol versions) and
|
|
7
|
+
* the modern stateless era (protocol revision 2026-07-28) whose requests carry
|
|
8
|
+
* a per-request `_meta` envelope. The SUPERVISOR is the sole era gatekeeper at
|
|
9
|
+
* admission — the worker's own era classification is unreliable for gating (a
|
|
10
|
+
* claim-less frame silently pins the worker connection legacy) — so the
|
|
11
|
+
* shallow envelope check, the era types, and the machine-readable rejection
|
|
12
|
+
* builders live here.
|
|
13
|
+
*/
|
|
14
|
+
/** Reserved `_meta` key carrying the per-request protocol version claim. */
|
|
15
|
+
export const PROTOCOL_VERSION_META_KEY = "io.modelcontextprotocol/protocolVersion";
|
|
16
|
+
/** Reserved `_meta` key carrying the per-request client capabilities. */
|
|
17
|
+
export const CLIENT_CAPABILITIES_META_KEY = "io.modelcontextprotocol/clientCapabilities";
|
|
18
|
+
/**
|
|
19
|
+
* Reserved `_meta` key carrying the OPTIONAL per-request client
|
|
20
|
+
* implementation info. NOT part of the era-signal check; only captured (as-is)
|
|
21
|
+
* into request snapshots when present on a shallow-valid modern signal.
|
|
22
|
+
*/
|
|
23
|
+
export const CLIENT_INFO_META_KEY = "io.modelcontextprotocol/clientInfo";
|
|
24
|
+
/** The five legacy handshake protocol versions the worker supports. */
|
|
25
|
+
export const LEGACY_PROTOCOL_VERSIONS = [
|
|
26
|
+
"2025-11-25",
|
|
27
|
+
"2025-06-18",
|
|
28
|
+
"2025-03-26",
|
|
29
|
+
"2024-11-05",
|
|
30
|
+
"2024-10-07"
|
|
31
|
+
];
|
|
32
|
+
/** The modern per-request `_meta` protocol revision. */
|
|
33
|
+
export const MODERN_PROTOCOL_VERSION = "2026-07-28";
|
|
34
|
+
/** All protocol versions this process serves, across both eras. */
|
|
35
|
+
export const ERA_SUPPORTED_PROTOCOL_VERSIONS = [
|
|
36
|
+
...LEGACY_PROTOCOL_VERSIONS,
|
|
37
|
+
MODERN_PROTOCOL_VERSION
|
|
38
|
+
];
|
|
39
|
+
const REQUIRED_META_KEYS = [PROTOCOL_VERSION_META_KEY, CLIENT_CAPABILITIES_META_KEY];
|
|
40
|
+
function isPlainObject(value) {
|
|
41
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Shallow era-signal classification of a request's (or notification's)
|
|
45
|
+
* params. Mirrors the SDK RequestMetaEnvelopeSchema's REQUIREDNESS without
|
|
46
|
+
* deep value validation: `params._meta` must be an object carrying a string
|
|
47
|
+
* `io.modelcontextprotocol/protocolVersion` and a non-null non-array object
|
|
48
|
+
* `io.modelcontextprotocol/clientCapabilities`. `io.modelcontextprotocol/clientInfo`
|
|
49
|
+
* is optional and NOT part of the check.
|
|
50
|
+
*
|
|
51
|
+
* A shallow-VALID envelope is the modern era signal even when the version
|
|
52
|
+
* VALUE is unsupported (e.g. "2027-01-01") — deep value validation belongs to
|
|
53
|
+
* the worker, which answers -32022 with {supported, requested}.
|
|
54
|
+
*
|
|
55
|
+
* claim-less: params/_meta missing, `_meta` not a plain object, or `_meta`
|
|
56
|
+
* carrying NEITHER required key. claim-shaped-invalid: at least one required
|
|
57
|
+
* key present but the shallow check fails (`missing` = absent required keys,
|
|
58
|
+
* `invalid` = present-but-wrong-shallow-type keys).
|
|
59
|
+
*/
|
|
60
|
+
export function classifyEraSignal(params) {
|
|
61
|
+
const meta = isPlainObject(params) ? params._meta : undefined;
|
|
62
|
+
if (!isPlainObject(meta)) {
|
|
63
|
+
return { classification: "claim-less", missing: [...REQUIRED_META_KEYS], invalid: [] };
|
|
64
|
+
}
|
|
65
|
+
const versionPresent = PROTOCOL_VERSION_META_KEY in meta;
|
|
66
|
+
const capabilitiesPresent = CLIENT_CAPABILITIES_META_KEY in meta;
|
|
67
|
+
if (!versionPresent && !capabilitiesPresent) {
|
|
68
|
+
return { classification: "claim-less", missing: [...REQUIRED_META_KEYS], invalid: [] };
|
|
69
|
+
}
|
|
70
|
+
const missing = [];
|
|
71
|
+
const invalid = [];
|
|
72
|
+
if (!versionPresent) {
|
|
73
|
+
missing.push(PROTOCOL_VERSION_META_KEY);
|
|
74
|
+
}
|
|
75
|
+
else if (typeof meta[PROTOCOL_VERSION_META_KEY] !== "string") {
|
|
76
|
+
invalid.push(PROTOCOL_VERSION_META_KEY);
|
|
77
|
+
}
|
|
78
|
+
if (!capabilitiesPresent) {
|
|
79
|
+
missing.push(CLIENT_CAPABILITIES_META_KEY);
|
|
80
|
+
}
|
|
81
|
+
else if (!isPlainObject(meta[CLIENT_CAPABILITIES_META_KEY])) {
|
|
82
|
+
invalid.push(CLIENT_CAPABILITIES_META_KEY);
|
|
83
|
+
}
|
|
84
|
+
if (missing.length === 0 && invalid.length === 0) {
|
|
85
|
+
return { classification: "modern-signal", missing, invalid };
|
|
86
|
+
}
|
|
87
|
+
return { classification: "claim-shaped-invalid", missing, invalid };
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Shallow extraction of the {@link ModernRequestContext} from a request's
|
|
91
|
+
* params. Returns undefined unless the params classify as a shallow-valid
|
|
92
|
+
* modern signal — the guard is the same pure {@link classifyEraSignal} check
|
|
93
|
+
* the admission gate runs, so it cannot diverge from an admission-time
|
|
94
|
+
* classification of the same params object. The returned values ALIAS the
|
|
95
|
+
* inbound params._meta objects (no clone): callers must never mutate an
|
|
96
|
+
* inbound params graph after admission, or captured snapshots would change
|
|
97
|
+
* retroactively.
|
|
98
|
+
*/
|
|
99
|
+
export function extractModernRequestContext(params) {
|
|
100
|
+
if (classifyEraSignal(params).classification !== "modern-signal") {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
const meta = params._meta;
|
|
104
|
+
const context = {
|
|
105
|
+
protocolVersion: meta[PROTOCOL_VERSION_META_KEY],
|
|
106
|
+
clientCapabilities: meta[CLIENT_CAPABILITIES_META_KEY]
|
|
107
|
+
};
|
|
108
|
+
if (CLIENT_INFO_META_KEY in meta) {
|
|
109
|
+
context.clientInfo = meta[CLIENT_INFO_META_KEY];
|
|
110
|
+
}
|
|
111
|
+
return context;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Strips the modern era-claim keys from a request's `params._meta` IN PLACE,
|
|
115
|
+
* deleting `_meta` entirely when nothing else remains. Used by the supervisor
|
|
116
|
+
* for accepted legacy `initialize` frames: the admission rule classifies every
|
|
117
|
+
* initialize as the legacy era signal regardless of its envelope, but the
|
|
118
|
+
* SDK's opening classifier treats an initialize carrying a valid modern claim
|
|
119
|
+
* as MODERN — the strip keeps the worker's classification aligned with the
|
|
120
|
+
* admission decision. Mutation is in place deliberately: the framing-mode
|
|
121
|
+
* registry is keyed by the exact inbound frame object, and no
|
|
122
|
+
* {@link ModernRequestContext} ever aliases an initialize's `_meta` (an
|
|
123
|
+
* initialize never classifies as a modern signal at admission).
|
|
124
|
+
*/
|
|
125
|
+
export function stripModernEraClaimInPlace(params) {
|
|
126
|
+
if (!isPlainObject(params))
|
|
127
|
+
return;
|
|
128
|
+
const meta = params._meta;
|
|
129
|
+
if (!isPlainObject(meta))
|
|
130
|
+
return;
|
|
131
|
+
delete meta[PROTOCOL_VERSION_META_KEY];
|
|
132
|
+
delete meta[CLIENT_CAPABILITIES_META_KEY];
|
|
133
|
+
delete meta[CLIENT_INFO_META_KEY];
|
|
134
|
+
if (Object.keys(meta).length === 0) {
|
|
135
|
+
delete params._meta;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const SUPPORTED_VERSIONS_SENTENCE = `Supported protocol versions: ${LEGACY_PROTOCOL_VERSIONS.join(", ")} (legacy initialize handshake) ` +
|
|
139
|
+
`and ${MODERN_PROTOCOL_VERSION} (modern per-request _meta).`;
|
|
140
|
+
/** Launcher-neutral fresh-process recovery preamble shared by both era-conflict messages. */
|
|
141
|
+
const FRESH_PROCESS_RECOVERY = "start a fresh process: close this transport, terminate and respawn the configured server command " +
|
|
142
|
+
"as a fresh stdio process, discard or re-issue any pending request ids, then";
|
|
143
|
+
export const ERA_CONFLICT_MODERN_MESSAGE = `initialize rejected: this server process is era-locked to protocol revision ${MODERN_PROTOCOL_VERSION} ` +
|
|
144
|
+
"(modern per-request _meta era), so the legacy initialize handshake can no longer be accepted. " +
|
|
145
|
+
`${SUPPORTED_VERSIONS_SENTENCE} ` +
|
|
146
|
+
`To use the legacy handshake, ${FRESH_PROCESS_RECOVERY} send initialize followed by notifications/initialized.`;
|
|
147
|
+
// Claim-agnostic wording: ANY string protocolVersion value classifies as the
|
|
148
|
+
// modern signal, so the message names the envelope, not a specific revision.
|
|
149
|
+
export const ERA_CONFLICT_LEGACY_MESSAGE = "Modern per-request _meta request rejected: this server process is era-locked to the legacy initialize handshake, " +
|
|
150
|
+
"so requests carrying the modern per-request _meta envelope can no longer be accepted. " +
|
|
151
|
+
`${SUPPORTED_VERSIONS_SENTENCE} ` +
|
|
152
|
+
`To use the modern era, ${FRESH_PROCESS_RECOVERY} send a request carrying the required io.modelcontextprotocol/* _meta envelope.`;
|
|
153
|
+
const REQUIRED_KEYS_SENTENCE = `the required io.modelcontextprotocol/* keys (${PROTOCOL_VERSION_META_KEY} and ${CLIENT_CAPABILITIES_META_KEY}) in params._meta`;
|
|
154
|
+
export const MISSING_META_UNSELECTED_MESSAGE = "Request rejected: no protocol era is selected yet and this request carries no valid era signal. " +
|
|
155
|
+
"Either send initialize followed by notifications/initialized to select the legacy handshake, " +
|
|
156
|
+
`or include ${REQUIRED_KEYS_SENTENCE} to select protocol revision ${MODERN_PROTOCOL_VERSION}.`;
|
|
157
|
+
export const MISSING_META_MODERN_MESSAGE = `Request rejected: this server process is era-locked to protocol revision ${MODERN_PROTOCOL_VERSION} ` +
|
|
158
|
+
"and the request lacks the required per-request _meta envelope. " +
|
|
159
|
+
`Include ${REQUIRED_KEYS_SENTENCE}.`;
|
|
160
|
+
/**
|
|
161
|
+
* One-way era conflict rejection. selectedEra "modern" rejects a legacy
|
|
162
|
+
* initialize with -32601; selectedEra "legacy" rejects a modern-signal
|
|
163
|
+
* request with -32600. Machine-readable discrimination is by code +
|
|
164
|
+
* data.kind/selectedEra/requestedEra/supported only.
|
|
165
|
+
*/
|
|
166
|
+
export function buildEraConflictRejection(id, selectedEra) {
|
|
167
|
+
const modernSelected = selectedEra === "modern";
|
|
168
|
+
return {
|
|
169
|
+
jsonrpc: "2.0",
|
|
170
|
+
id,
|
|
171
|
+
error: {
|
|
172
|
+
code: modernSelected ? -32601 : -32600,
|
|
173
|
+
message: modernSelected ? ERA_CONFLICT_MODERN_MESSAGE : ERA_CONFLICT_LEGACY_MESSAGE,
|
|
174
|
+
data: {
|
|
175
|
+
kind: "era_conflict",
|
|
176
|
+
selectedEra,
|
|
177
|
+
requestedEra: modernSelected ? "legacy" : "modern",
|
|
178
|
+
supported: [...ERA_SUPPORTED_PROTOCOL_VERSIONS]
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Rejection for a request carrying no valid era signal: in the unselected
|
|
185
|
+
* state (era stays UNSELECTED; the message names both recovery paths) or in
|
|
186
|
+
* the modern era (the message names the required envelope). `invalid` is
|
|
187
|
+
* included only when non-empty; `missing` is always present.
|
|
188
|
+
*/
|
|
189
|
+
export function buildMissingMetaRejection(id, signal, era) {
|
|
190
|
+
const data = { kind: "missing_meta", missing: [...signal.missing] };
|
|
191
|
+
if (signal.invalid.length > 0) {
|
|
192
|
+
data.invalid = [...signal.invalid];
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
jsonrpc: "2.0",
|
|
196
|
+
id,
|
|
197
|
+
error: {
|
|
198
|
+
code: -32602,
|
|
199
|
+
message: era === "modern" ? MISSING_META_MODERN_MESSAGE : MISSING_META_UNSELECTED_MESSAGE,
|
|
200
|
+
data
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Whether a frame is a COMPLETE, schema-valid MCP `initialize` request.
|
|
206
|
+
*
|
|
207
|
+
* Admission classifies any frame whose method is `initialize` as the legacy
|
|
208
|
+
* era signal, and that lock is one-way for the process lifetime. Generic
|
|
209
|
+
* JSON-RPC parsing is far too weak a basis for a permanent decision: a frame
|
|
210
|
+
* as empty as `{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}`
|
|
211
|
+
* passes it while carrying none of the fields the handshake needs. Burning the
|
|
212
|
+
* lock on such a frame left the process legacy-locked with NO valid era
|
|
213
|
+
* opening ever completed, and the modern era permanently unreachable.
|
|
214
|
+
*
|
|
215
|
+
* The check is the SDK's own public `isInitializeRequest` — the exact
|
|
216
|
+
* `InitializeRequestSchema` the worker would apply — so admission and the
|
|
217
|
+
* worker can never disagree about what an initialize is. Extra params
|
|
218
|
+
* (including a `_meta` envelope) are tolerated; `protocolVersion` (string),
|
|
219
|
+
* `capabilities` (object) and `clientInfo` ({name, version}) are required.
|
|
220
|
+
* Version VALUES are not judged here: an unknown legacy version negotiates
|
|
221
|
+
* down at the worker, exactly as documented.
|
|
222
|
+
*/
|
|
223
|
+
export function isCompleteInitializeRequest(message) {
|
|
224
|
+
return isInitializeRequest(message);
|
|
225
|
+
}
|
|
226
|
+
const INITIALIZE_REQUIRED_FIELDS = ["protocolVersion", "capabilities", "clientInfo"];
|
|
227
|
+
export const INVALID_INITIALIZE_MESSAGE = "initialize rejected: the request is not a valid MCP initialize request. params must carry " +
|
|
228
|
+
"protocolVersion (string), capabilities (object) and clientInfo ({ name, version }). " +
|
|
229
|
+
"No protocol era has been selected, so this is fully recoverable: retry with a well-formed " +
|
|
230
|
+
`initialize, or select protocol revision ${MODERN_PROTOCOL_VERSION} by including ` +
|
|
231
|
+
"the required io.modelcontextprotocol/* keys in params._meta.";
|
|
232
|
+
/**
|
|
233
|
+
* Rejection for a frame that claims to be an `initialize` but fails the MCP
|
|
234
|
+
* initialize schema. Answered at admission, BEFORE the legacy lock is
|
|
235
|
+
* committed, so the era stays UNSELECTED and every recovery path — including
|
|
236
|
+
* the modern era — remains open.
|
|
237
|
+
*/
|
|
238
|
+
export function buildInvalidInitializeRejection(id) {
|
|
239
|
+
return {
|
|
240
|
+
jsonrpc: "2.0",
|
|
241
|
+
id,
|
|
242
|
+
error: {
|
|
243
|
+
code: -32602,
|
|
244
|
+
message: INVALID_INITIALIZE_MESSAGE,
|
|
245
|
+
data: {
|
|
246
|
+
kind: "invalid_initialize",
|
|
247
|
+
required: [...INITIALIZE_REQUIRED_FIELDS],
|
|
248
|
+
eraSelected: false
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Unsupported modern `protocolVersion` VALUE rejection (`-32022` with
|
|
255
|
+
* `data.supported` / `data.requested`).
|
|
256
|
+
*
|
|
257
|
+
* Built from the SDK's own {@link UnsupportedProtocolVersionError} so the
|
|
258
|
+
* code, message and data stay byte-identical to the worker-produced answer
|
|
259
|
+
* clients already receive on the pre-pin path — the supervisor merely makes
|
|
260
|
+
* the check apply to EVERY modern request instead of only the one that
|
|
261
|
+
* happened to open the connection.
|
|
262
|
+
*/
|
|
263
|
+
export function buildUnsupportedProtocolVersionRejection(id, requested) {
|
|
264
|
+
const error = new UnsupportedProtocolVersionError({
|
|
265
|
+
supported: [MODERN_PROTOCOL_VERSION],
|
|
266
|
+
requested
|
|
267
|
+
});
|
|
268
|
+
return {
|
|
269
|
+
jsonrpc: "2.0",
|
|
270
|
+
id,
|
|
271
|
+
error: {
|
|
272
|
+
code: error.code,
|
|
273
|
+
message: error.message,
|
|
274
|
+
data: error.data
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Plain -32601 admission rejection for subscriptions/listen in the modern era
|
|
280
|
+
* (no data.kind — deliberately indistinguishable from an ordinary unknown
|
|
281
|
+
* method). Defense in depth: if admission were ever bypassed, the worker
|
|
282
|
+
* (maxSubscriptions: 0) would answer -32603 "Subscription limit reached";
|
|
283
|
+
* intercepting at admission keeps that internal detail off the wire.
|
|
284
|
+
*/
|
|
285
|
+
export function buildMethodNotFoundRejection(id) {
|
|
286
|
+
return {
|
|
287
|
+
jsonrpc: "2.0",
|
|
288
|
+
id,
|
|
289
|
+
error: { code: -32601, message: "Method not found" }
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
//# sourceMappingURL=era-classifier.js.map
|
package/dist/error-mapping.d.ts
CHANGED
|
@@ -57,6 +57,14 @@ export type ProblemDetails = {
|
|
|
57
57
|
exampleCalls?: ExampleCall[];
|
|
58
58
|
/** Ranked near-miss candidates for a class/symbol that was not found. */
|
|
59
59
|
didYouMean?: DidYouMeanCandidate[];
|
|
60
|
+
/**
|
|
61
|
+
* Nested-jar inventory of the shell jar the lookup ran against, as jar-entry
|
|
62
|
+
* names (e.g. "META-INF/jars/api.jar"). A shell jar holds no classes of its
|
|
63
|
+
* own, so a class-not-found error on one is only actionable with the list of
|
|
64
|
+
* inner jars the caller can target next. Like `didYouMean`, it travels as a
|
|
65
|
+
* dedicated typed field: `context` is primitive-only and can never carry it.
|
|
66
|
+
*/
|
|
67
|
+
nestedJars?: string[];
|
|
60
68
|
failedStage?: string;
|
|
61
69
|
context?: Record<string, string | number | boolean>;
|
|
62
70
|
};
|
|
@@ -67,6 +75,34 @@ export type ProblemDetails = {
|
|
|
67
75
|
* whole rather than partially published.
|
|
68
76
|
*/
|
|
69
77
|
export declare function extractDidYouMean(details: unknown): DidYouMeanCandidate[] | undefined;
|
|
78
|
+
/**
|
|
79
|
+
* Validates and extracts a `nestedJars` inventory from error details.
|
|
80
|
+
* `buildClassSourceNotFoundError` records it whenever the lookup ran against a
|
|
81
|
+
* shell jar, but `context` is primitive-only, so without this the inventory
|
|
82
|
+
* never left the process. Malformed payloads are dropped whole rather than
|
|
83
|
+
* partially published, matching {@link extractDidYouMean}. An empty inventory
|
|
84
|
+
* is dropped too: the producing site omits the key entirely in that case, so an
|
|
85
|
+
* empty array carries no information a caller could act on.
|
|
86
|
+
*/
|
|
87
|
+
export declare function extractNestedJars(details: unknown): string[] | undefined;
|
|
88
|
+
/**
|
|
89
|
+
* Per-throw-site {@link IssueOrigin} override carried on an AppError's
|
|
90
|
+
* `details.issueOrigin`.
|
|
91
|
+
*
|
|
92
|
+
* The default classification is keyed purely on the error CODE, which is right
|
|
93
|
+
* for codes whose every throw site shares one origin. A few codes do not:
|
|
94
|
+
* `ERR_CONTEXT_UNRESOLVED` covers both a caller naming a version no artifact
|
|
95
|
+
* carries (genuinely `code_issue`) and the tool resolving an artifact with no
|
|
96
|
+
* binary jar behind the caller's back (`tool_issue`). Publishing the latter as
|
|
97
|
+
* caller-fixable sends an agent into a retry loop over input it cannot repair.
|
|
98
|
+
*
|
|
99
|
+
* This override is deliberately opt-in and per-error: `issueOriginForErrorCode`
|
|
100
|
+
* and its default sets stay untouched, so the exhaustive classification map in
|
|
101
|
+
* tests/runtime/error-mapping.test.ts keeps forcing a conscious decision for
|
|
102
|
+
* every new code. The key is not in `CONTEXT_ALLOWLIST`, so it cannot leak into
|
|
103
|
+
* the public `context` blob.
|
|
104
|
+
*/
|
|
105
|
+
export declare function extractIssueOriginOverride(details: unknown): IssueOrigin | undefined;
|
|
70
106
|
export declare function statusForErrorCode(code: string): number;
|
|
71
107
|
/**
|
|
72
108
|
* Single source of truth mapping an error code to its {@link RetryClass}. Used
|
|
@@ -85,6 +121,46 @@ export declare function retryClassForErrorCode(code: string): RetryClass;
|
|
|
85
121
|
* not satisfy).
|
|
86
122
|
*/
|
|
87
123
|
export declare function issueOriginForErrorCode(code: string): IssueOrigin;
|
|
124
|
+
/**
|
|
125
|
+
* The single source of truth for a ProblemDetails classification pair, and the
|
|
126
|
+
* ONLY supported way to compute one for an error the server caught. Every site
|
|
127
|
+
* that turns a caught error into a published envelope spreads this result — the
|
|
128
|
+
* tool envelope (`mapErrorToProblem`), batch entries, the batch-aborted
|
|
129
|
+
* sentinel, and the `mc://` resource read — so one failure always publishes the
|
|
130
|
+
* same `{ retryClass, issueOrigin }` no matter which access path the caller
|
|
131
|
+
* used.
|
|
132
|
+
*
|
|
133
|
+
* KNOWN EXCEPTION — the synthetic supervisor replies in `src/stdio-supervisor.ts`
|
|
134
|
+
* (`buildSupervisorQueueLimitReply`, `buildValidateProjectTimeoutReply`) write
|
|
135
|
+
* the pair as literals and never import this builder. They are not emission
|
|
136
|
+
* sites for a caught error at all: the supervisor answers a request the worker
|
|
137
|
+
* never got to run, so there is no source `AppError` whose `details` could carry
|
|
138
|
+
* an override and nothing to pass as the required `details` argument. Their
|
|
139
|
+
* literals are `transient` / `tool_issue`, which is exactly what this function
|
|
140
|
+
* returns for `ERR_LIMIT_EXCEEDED` and `ERR_TOOL_TIMEOUT` today — but that is a
|
|
141
|
+
* duplicated value held in agreement by hand, not a second call into this
|
|
142
|
+
* classifier. Reclassifying either code here must be mirrored there.
|
|
143
|
+
*
|
|
144
|
+
* It exists because that pair used to be assembled by hand at each site. The
|
|
145
|
+
* `issueOrigin` half has a per-throw-site override (see
|
|
146
|
+
* {@link extractIssueOriginOverride}), and honouring it was copy-pasted into
|
|
147
|
+
* some sites and forgotten in others — which is exactly how one identical
|
|
148
|
+
* AppError came to publish `tool_issue` through a tool call and `code_issue`
|
|
149
|
+
* through a resource read. Composing the pair here removes the opportunity:
|
|
150
|
+
* emission modules import this function and no longer reach the component
|
|
151
|
+
* classifiers at all.
|
|
152
|
+
*
|
|
153
|
+
* `details` is REQUIRED, deliberately without a default. A site that genuinely
|
|
154
|
+
* has no AppError behind it (a ZodError, a sanitized non-AppError, the
|
|
155
|
+
* batch-aborted sentinel) must pass `undefined` explicitly, so skipping the
|
|
156
|
+
* override can only ever be a written-down decision rather than an omission.
|
|
157
|
+
*
|
|
158
|
+
* Only `issueOrigin` is overridable per throw site. `retryClass` stays purely
|
|
159
|
+
* code-derived: it is a documented wire contract, and changing the value an
|
|
160
|
+
* existing code publishes is a Breaking change, so a `retryClass` override seam
|
|
161
|
+
* was deliberately deferred rather than added alongside this one.
|
|
162
|
+
*/
|
|
163
|
+
export declare function problemClassification(code: string, details: unknown): Pick<ProblemDetails, "retryClass" | "issueOrigin">;
|
|
88
164
|
/**
|
|
89
165
|
* Pick the allowlisted, primitive-valued fields out of an AppError's `details`
|
|
90
166
|
* so the public envelope can carry structured repair context without leaking
|