@adhisang/minecraft-modding-mcp 6.2.0 → 7.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +21 -5
  3. package/dist/cache-policy.d.ts +71 -0
  4. package/dist/cache-policy.js +83 -0
  5. package/dist/cache-registry.js +6 -6
  6. package/dist/cli.js +74 -3
  7. package/dist/compat-stdio-transport.d.ts +1 -1
  8. package/dist/compat-stdio-transport.js +13 -1
  9. package/dist/config.d.ts +3 -0
  10. package/dist/config.js +8 -2
  11. package/dist/decompiler/vineflower.d.ts +1 -0
  12. package/dist/decompiler/vineflower.js +8 -5
  13. package/dist/entry-tools/analyze-mod-service.d.ts +70 -136
  14. package/dist/entry-tools/analyze-symbol-service.d.ts +112 -150
  15. package/dist/entry-tools/compare-minecraft-service.d.ts +59 -145
  16. package/dist/entry-tools/entry-tool-schema.d.ts +38 -4
  17. package/dist/entry-tools/entry-tool-schema.js +4 -1
  18. package/dist/entry-tools/inspect-minecraft/internal.d.ts +235 -799
  19. package/dist/entry-tools/inspect-minecraft/internal.js +65 -28
  20. package/dist/entry-tools/inspect-minecraft-service.d.ts +372 -1736
  21. package/dist/entry-tools/inspect-minecraft-service.js +1 -1
  22. package/dist/entry-tools/manage-cache-service.d.ts +81 -91
  23. package/dist/entry-tools/validate-project/cases/project-summary.d.ts +7 -7
  24. package/dist/entry-tools/validate-project-service.d.ts +164 -592
  25. package/dist/entry-tools/verify-mixin-target-service.d.ts +3 -19
  26. package/dist/era-classifier.d.ts +161 -0
  27. package/dist/era-classifier.js +292 -0
  28. package/dist/error-mapping.js +9 -2
  29. package/dist/index.d.ts +42 -4
  30. package/dist/index.js +637 -475
  31. package/dist/java-process.d.ts +2 -0
  32. package/dist/java-process.js +22 -2
  33. package/dist/json-rpc-framing.d.ts +77 -1
  34. package/dist/json-rpc-framing.js +249 -13
  35. package/dist/mapping/loaders/tiny-loom-selection.d.ts +88 -0
  36. package/dist/mapping/loaders/tiny-loom-selection.js +223 -0
  37. package/dist/mapping/loaders/tiny-loom.js +45 -33
  38. package/dist/mapping/loaders/tiny-maven.js +6 -11
  39. package/dist/mapping/parsers/tiny.d.ts +57 -0
  40. package/dist/mapping/parsers/tiny.js +99 -22
  41. package/dist/mapping-service.d.ts +19 -0
  42. package/dist/mapping-service.js +93 -9
  43. package/dist/mcp-helpers.d.ts +19 -2
  44. package/dist/mcp-helpers.js +48 -6
  45. package/dist/minecraft-explorer-service.d.ts +1 -1
  46. package/dist/mixin/types.d.ts +8 -0
  47. package/dist/mod-analyzer.js +7 -7
  48. package/dist/mod-decompile-service.js +1 -0
  49. package/dist/nbt/java-nbt-codec.js +12 -2
  50. package/dist/nbt/json-patch.js +14 -3
  51. package/dist/nbt/pipeline.js +40 -3
  52. package/dist/nbt/typed-json.js +26 -1
  53. package/dist/registration-adapter.d.ts +32 -0
  54. package/dist/registration-adapter.js +52 -0
  55. package/dist/request-context.d.ts +7 -0
  56. package/dist/request-context.js +9 -0
  57. package/dist/resources.d.ts +1 -1
  58. package/dist/resources.js +25 -19
  59. package/dist/server-identity.d.ts +27 -0
  60. package/dist/server-identity.js +26 -0
  61. package/dist/source/access-validate.js +53 -0
  62. package/dist/source/artifact-resolver.d.ts +69 -1
  63. package/dist/source/artifact-resolver.js +215 -14
  64. package/dist/source/class-source.d.ts +22 -0
  65. package/dist/source/class-source.js +162 -29
  66. package/dist/source/did-you-mean.d.ts +12 -1
  67. package/dist/source/did-you-mean.js +6 -2
  68. package/dist/source/file-access.js +150 -46
  69. package/dist/source/indexer.js +1 -0
  70. package/dist/source/nested-jars.d.ts +19 -0
  71. package/dist/source/nested-jars.js +90 -21
  72. package/dist/source/shared-utils.d.ts +21 -0
  73. package/dist/source/shared-utils.js +23 -0
  74. package/dist/source-service.d.ts +12 -0
  75. package/dist/source-service.js +3 -0
  76. package/dist/stdio-supervisor.d.ts +357 -2
  77. package/dist/stdio-supervisor.js +1031 -80
  78. package/dist/storage/db.d.ts +2 -1
  79. package/dist/storage/db.js +15 -8
  80. package/dist/synthetic-decorator.d.ts +24 -0
  81. package/dist/synthetic-decorator.js +48 -0
  82. package/dist/tool-contract-manifest.js +1 -1
  83. package/dist/tool-guidance.d.ts +17 -1
  84. package/dist/tool-guidance.js +417 -13
  85. package/dist/tool-schema-registry.d.ts +2 -0
  86. package/dist/tool-schema-registry.js +4 -0
  87. package/dist/tool-schemas.d.ts +2214 -3915
  88. package/dist/tool-schemas.js +34 -7
  89. package/dist/types.d.ts +35 -0
  90. package/dist/v1-parity-schemas.d.ts +7 -0
  91. package/dist/v1-parity-schemas.js +5584 -0
  92. package/dist/version-diff-service.d.ts +33 -0
  93. package/dist/version-diff-service.js +148 -3
  94. package/dist/version-service.js +36 -14
  95. package/dist/warning-details.js +18 -1
  96. package/docs/README-ja.md +9 -3
  97. package/docs/tool-reference.md +267 -22
  98. package/package.json +12 -9
@@ -1,3 +1,4 @@
1
+ import { spawn } from "node:child_process";
1
2
  export declare const MAX_STDIO_SNAPSHOT = 6240;
2
3
  export interface JavaProcessOptions {
3
4
  jarPath: string;
@@ -18,6 +19,7 @@ export declare function isAbsolutePath(value: string): boolean;
18
19
  export declare function isOptionArg(value: string): boolean;
19
20
  export declare function normalizeArgs(args: string[]): string[];
20
21
  export declare function assertJavaAvailable(): Promise<void>;
22
+ export declare function resetJavaAvailabilityCacheForTests(spawnHook?: typeof spawn): void;
21
23
  export declare function runJavaProcess(options: JavaProcessOptions): Promise<JavaProcessResult>;
22
24
  export declare const javaRunner: {
23
25
  run: (options: JavaProcessOptions) => Promise<JavaProcessResult>;
@@ -3,6 +3,8 @@ import { createError, ERROR_CODES } from "./errors.js";
3
3
  import { normalizePathForHost } from "./path-converter.js";
4
4
  const JAVA_CHECK_TIMEOUT_MS = 2_000;
5
5
  export const MAX_STDIO_SNAPSHOT = 6_240;
6
+ let javaAvailabilitySpawn = spawn;
7
+ let javaAvailabilityPromise;
6
8
  export function limitStdio(text) {
7
9
  if (text.length <= MAX_STDIO_SNAPSHOT) {
8
10
  return text;
@@ -26,9 +28,9 @@ export function normalizeArgs(args) {
26
28
  return arg;
27
29
  });
28
30
  }
29
- export async function assertJavaAvailable() {
31
+ function probeJavaAvailability() {
30
32
  return new Promise((resolve, reject) => {
31
- const proc = spawn("java", ["-version"], { stdio: "ignore" });
33
+ const proc = javaAvailabilitySpawn("java", ["-version"], { stdio: "ignore" });
32
34
  const timer = setTimeout(() => {
33
35
  proc.kill();
34
36
  reject(createError({
@@ -56,6 +58,24 @@ export async function assertJavaAvailable() {
56
58
  });
57
59
  });
58
60
  }
61
+ export function assertJavaAvailable() {
62
+ if (javaAvailabilityPromise) {
63
+ return javaAvailabilityPromise;
64
+ }
65
+ const probePromise = Promise.resolve().then(() => probeJavaAvailability());
66
+ const cachedPromise = probePromise.catch((error) => {
67
+ if (javaAvailabilityPromise === cachedPromise) {
68
+ javaAvailabilityPromise = undefined;
69
+ }
70
+ throw error;
71
+ });
72
+ javaAvailabilityPromise = cachedPromise;
73
+ return cachedPromise;
74
+ }
75
+ export function resetJavaAvailabilityCacheForTests(spawnHook = spawn) {
76
+ javaAvailabilityPromise = undefined;
77
+ javaAvailabilitySpawn = spawnHook;
78
+ }
59
79
  export function runJavaProcess(options) {
60
80
  const { jarPath, args, cwd, timeoutMs = 120_000, maxMemoryMb, minMemoryMb, normalizePathArgs = false } = options;
61
81
  const jvmArgs = [];
@@ -1,18 +1,75 @@
1
- import { type JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
1
+ import { type JSONRPCMessage } from "@modelcontextprotocol/server";
2
2
  export type FramingMode = "unknown" | "line" | "content-length";
3
3
  export type ConcreteFramingMode = Exclude<FramingMode, "unknown">;
4
4
  export type ParsedJsonRpcFrame = {
5
5
  message: JSONRPCMessage;
6
6
  mode: ConcreteFramingMode;
7
7
  };
8
+ /**
9
+ * A framing violation the reader cannot provably recover from.
10
+ *
11
+ * The reader's framing invariant is: after ANY framing violation it either
12
+ * provably resynchronizes — resuming at a byte position the peer itself
13
+ * delimited — or it terminates the session with a diagnostic. It must never
14
+ * silently consume subsequent valid frames, and it must never wait on bytes
15
+ * an untrusted declared length says are coming.
16
+ *
17
+ * Recoverable violations (plain `Error`, reader keeps running):
18
+ * - a line-delimited frame that is oversized or unparseable — the newline
19
+ * that terminates it is a delimiter the reader can prove,
20
+ * - a Content-Length header block carrying no usable length at all
21
+ * (`Content-Length: nope`, a missing header, a malformed header line) —
22
+ * the CRLFCRLF boundary delimits the header block and no body length was
23
+ * ever declared, so only the header block is consumed,
24
+ * - an oversized Content-Length frame whose complete declared body is ALREADY
25
+ * buffered — dropping exactly those bytes lands on the byte the peer named
26
+ * as the next frame's first,
27
+ * - a Content-Length body that is valid JSON but not a valid JSON-RPC message
28
+ * — valid JSON of exactly the declared length proves the boundary was
29
+ * right, so this is a message-level error, not a framing one.
30
+ *
31
+ * Fatal violations (this class, reader stops permanently):
32
+ * - an oversized Content-Length whose declared body has NOT fully arrived
33
+ * (waiting on it is what let a single unanswerable header wedge the
34
+ * transport for the process lifetime),
35
+ * - a Content-Length body that is not valid JSON — under-declaration,
36
+ * over-declaration and an honestly-framed bad body are indistinguishable,
37
+ * and the first two have already desynchronized the stream,
38
+ * - duplicate Content-Length headers — the body length is ambiguous,
39
+ * - a Content-Length header block that never terminates within the header
40
+ * limit — there is no delimiter left to resynchronize on.
41
+ */
42
+ export declare class JsonRpcFramingFatalError extends Error {
43
+ readonly framingFatal = true;
44
+ constructor(message: string);
45
+ }
46
+ /**
47
+ * Whether an error reported through `processChunk`'s `onError` handler ends
48
+ * the session. Every transport that owns a {@link JsonRpcFrameReader} MUST
49
+ * check this and tear its session down: the reader has stopped accepting
50
+ * input, so ignoring the signal would leave a silently deaf transport.
51
+ */
52
+ export declare function isJsonRpcFramingFatalError(error: unknown): error is JsonRpcFramingFatalError;
53
+ export declare function loadMaxFrameBytes(value?: string | undefined): number;
8
54
  export declare function encodeJsonRpcMessage(message: JSONRPCMessage, mode: ConcreteFramingMode): Buffer;
9
55
  export declare class JsonRpcFrameReader {
56
+ private readonly maxFrameBytes;
10
57
  private mode;
11
58
  private buffer;
12
59
  private pendingChunks;
13
60
  private pendingBytes;
14
61
  private awaitedFrameEnd;
62
+ private fatal;
63
+ constructor(options?: {
64
+ maxFrameBytes?: number;
65
+ });
15
66
  get currentMode(): FramingMode;
67
+ /**
68
+ * Whether an unrecoverable framing violation has stopped this reader. No
69
+ * further input is examined and no further frame is ever emitted until
70
+ * `reset()` or `clear()` explicitly re-arms it.
71
+ */
72
+ get isFatal(): boolean;
16
73
  reset(): void;
17
74
  clear(): void;
18
75
  processChunk(chunk: Buffer, handlers: {
@@ -20,6 +77,25 @@ export declare class JsonRpcFrameReader {
20
77
  onError: (error: Error) => void;
21
78
  }): void;
22
79
  private canCompleteFrame;
80
+ private rejectOversizedIncompleteInput;
81
+ /**
82
+ * Rejects a Content-Length frame whose declared body can never be accepted
83
+ * (over the frame limit, or behind an over-limit header block).
84
+ *
85
+ * The body is skipped ONLY when every declared byte is already buffered. In
86
+ * that case the skip is a bounded operation on bytes in hand and it resumes
87
+ * at exactly the offset the peer itself named as the next frame's first
88
+ * byte — a resynchronization the reader can prove without extending trust to
89
+ * a single unarrived byte.
90
+ *
91
+ * When the body has NOT fully arrived the reader must not wait for it: the
92
+ * declared length is attacker-controlled, and arming a countdown with it is
93
+ * precisely what let a 29-byte header (`Content-Length: 999999999\r\n\r\n`
94
+ * with no body) silently swallow every later frame for the process lifetime.
95
+ * There is no delimiter to scan forward to either — an arbitrary binary body
96
+ * offers none — so the session is terminated instead.
97
+ */
98
+ private rejectDeclaredBody;
23
99
  private detectMode;
24
100
  private readLineDelimitedMessage;
25
101
  private readContentLengthMessage;
@@ -1,4 +1,7 @@
1
- import { JSONRPCMessageSchema } from "@modelcontextprotocol/sdk/types.js";
1
+ import { parseJSONRPCMessage } from "@modelcontextprotocol/server";
2
+ const DEFAULT_MAX_FRAME_BYTES = 64 * 1024 * 1024;
3
+ const MIN_MAX_FRAME_BYTES = 1024 * 1024;
4
+ const MAX_CONTENT_LENGTH_HEADER_BYTES = 8 * 1024;
2
5
  function findHeaderBoundary(buffer) {
3
6
  const crlfBoundary = buffer.indexOf("\r\n\r\n");
4
7
  if (crlfBoundary !== -1) {
@@ -11,11 +14,71 @@ function findHeaderBoundary(buffer) {
11
14
  return undefined;
12
15
  }
13
16
  function parseJsonRpcMessage(json) {
14
- return JSONRPCMessageSchema.parse(JSON.parse(json));
17
+ return parseJSONRPCMessage(JSON.parse(json));
15
18
  }
16
19
  function asError(value) {
17
20
  return value instanceof Error ? value : new Error(String(value));
18
21
  }
22
+ /**
23
+ * A framing violation the reader cannot provably recover from.
24
+ *
25
+ * The reader's framing invariant is: after ANY framing violation it either
26
+ * provably resynchronizes — resuming at a byte position the peer itself
27
+ * delimited — or it terminates the session with a diagnostic. It must never
28
+ * silently consume subsequent valid frames, and it must never wait on bytes
29
+ * an untrusted declared length says are coming.
30
+ *
31
+ * Recoverable violations (plain `Error`, reader keeps running):
32
+ * - a line-delimited frame that is oversized or unparseable — the newline
33
+ * that terminates it is a delimiter the reader can prove,
34
+ * - a Content-Length header block carrying no usable length at all
35
+ * (`Content-Length: nope`, a missing header, a malformed header line) —
36
+ * the CRLFCRLF boundary delimits the header block and no body length was
37
+ * ever declared, so only the header block is consumed,
38
+ * - an oversized Content-Length frame whose complete declared body is ALREADY
39
+ * buffered — dropping exactly those bytes lands on the byte the peer named
40
+ * as the next frame's first,
41
+ * - a Content-Length body that is valid JSON but not a valid JSON-RPC message
42
+ * — valid JSON of exactly the declared length proves the boundary was
43
+ * right, so this is a message-level error, not a framing one.
44
+ *
45
+ * Fatal violations (this class, reader stops permanently):
46
+ * - an oversized Content-Length whose declared body has NOT fully arrived
47
+ * (waiting on it is what let a single unanswerable header wedge the
48
+ * transport for the process lifetime),
49
+ * - a Content-Length body that is not valid JSON — under-declaration,
50
+ * over-declaration and an honestly-framed bad body are indistinguishable,
51
+ * and the first two have already desynchronized the stream,
52
+ * - duplicate Content-Length headers — the body length is ambiguous,
53
+ * - a Content-Length header block that never terminates within the header
54
+ * limit — there is no delimiter left to resynchronize on.
55
+ */
56
+ export class JsonRpcFramingFatalError extends Error {
57
+ framingFatal = true;
58
+ constructor(message) {
59
+ super(message);
60
+ this.name = "JsonRpcFramingFatalError";
61
+ }
62
+ }
63
+ /**
64
+ * Whether an error reported through `processChunk`'s `onError` handler ends
65
+ * the session. Every transport that owns a {@link JsonRpcFrameReader} MUST
66
+ * check this and tear its session down: the reader has stopped accepting
67
+ * input, so ignoring the signal would leave a silently deaf transport.
68
+ */
69
+ export function isJsonRpcFramingFatalError(error) {
70
+ return error instanceof JsonRpcFramingFatalError;
71
+ }
72
+ export function loadMaxFrameBytes(value = process.env.MCP_MAX_FRAME_BYTES) {
73
+ if (!/^[0-9]+$/.test(value ?? "")) {
74
+ return DEFAULT_MAX_FRAME_BYTES;
75
+ }
76
+ const parsed = Number(value);
77
+ if (!Number.isSafeInteger(parsed)) {
78
+ return DEFAULT_MAX_FRAME_BYTES;
79
+ }
80
+ return Math.max(parsed, MIN_MAX_FRAME_BYTES);
81
+ }
19
82
  export function encodeJsonRpcMessage(message, mode) {
20
83
  const json = JSON.stringify(message);
21
84
  return Buffer.from(mode === "content-length"
@@ -23,17 +86,31 @@ export function encodeJsonRpcMessage(message, mode) {
23
86
  : `${json}\n`, "utf8");
24
87
  }
25
88
  export class JsonRpcFrameReader {
89
+ maxFrameBytes;
26
90
  mode = "unknown";
27
91
  buffer = Buffer.alloc(0);
28
92
  pendingChunks = [];
29
93
  pendingBytes = 0;
30
94
  awaitedFrameEnd = -1;
95
+ fatal = false;
96
+ constructor(options = {}) {
97
+ this.maxFrameBytes = options.maxFrameBytes ?? loadMaxFrameBytes();
98
+ }
31
99
  get currentMode() {
32
100
  return this.mode;
33
101
  }
102
+ /**
103
+ * Whether an unrecoverable framing violation has stopped this reader. No
104
+ * further input is examined and no further frame is ever emitted until
105
+ * `reset()` or `clear()` explicitly re-arms it.
106
+ */
107
+ get isFatal() {
108
+ return this.fatal;
109
+ }
34
110
  reset() {
35
111
  this.mode = "unknown";
36
112
  this.awaitedFrameEnd = -1;
113
+ this.fatal = false;
37
114
  }
38
115
  clear() {
39
116
  this.mode = "unknown";
@@ -41,9 +118,10 @@ export class JsonRpcFrameReader {
41
118
  this.pendingChunks = [];
42
119
  this.pendingBytes = 0;
43
120
  this.awaitedFrameEnd = -1;
121
+ this.fatal = false;
44
122
  }
45
123
  processChunk(chunk, handlers) {
46
- if (chunk.length === 0) {
124
+ if (chunk.length === 0 || this.fatal) {
47
125
  return;
48
126
  }
49
127
  this.pendingChunks.push(chunk);
@@ -56,6 +134,7 @@ export class JsonRpcFrameReader {
56
134
  this.pendingBytes = 0;
57
135
  while (true) {
58
136
  try {
137
+ this.rejectOversizedIncompleteInput();
59
138
  if (this.mode === "unknown") {
60
139
  const detected = this.detectMode();
61
140
  if (!detected) {
@@ -80,16 +159,91 @@ export class JsonRpcFrameReader {
80
159
  });
81
160
  }
82
161
  catch (caughtError) {
162
+ const error = asError(caughtError);
83
163
  this.mode = "unknown";
84
- handlers.onError(asError(caughtError));
164
+ this.awaitedFrameEnd = -1;
165
+ if (error instanceof JsonRpcFramingFatalError) {
166
+ // Terminal: drop everything buffered and refuse all further input so
167
+ // no byte after the violation can be mistaken for a frame. The
168
+ // transport owns the teardown.
169
+ this.fatal = true;
170
+ this.buffer = Buffer.alloc(0);
171
+ this.pendingChunks = [];
172
+ this.pendingBytes = 0;
173
+ handlers.onError(error);
174
+ return;
175
+ }
176
+ handlers.onError(error);
85
177
  }
86
178
  }
87
179
  }
88
180
  canCompleteFrame(chunk) {
181
+ const bufferedBytes = this.buffer.length + this.pendingBytes;
89
182
  if (this.mode === "content-length" && this.awaitedFrameEnd >= 0) {
90
- return this.buffer.length + this.pendingBytes >= this.awaitedFrameEnd;
183
+ return bufferedBytes >= this.awaitedFrameEnd;
184
+ }
185
+ if (this.mode === "content-length" &&
186
+ bufferedBytes > MAX_CONTENT_LENGTH_HEADER_BYTES) {
187
+ return true;
188
+ }
189
+ return chunk.includes(0x0a) || bufferedBytes > this.maxFrameBytes;
190
+ }
191
+ rejectOversizedIncompleteInput() {
192
+ const headerBoundary = this.mode === "content-length" ? findHeaderBoundary(this.buffer) : undefined;
193
+ if (this.mode === "content-length" &&
194
+ !headerBoundary &&
195
+ this.buffer.length > MAX_CONTENT_LENGTH_HEADER_BYTES) {
196
+ // No header terminator anywhere in an over-limit header block: there is
197
+ // no delimiter left to resynchronize on (content-length mode has no
198
+ // newline delimiter, and a line frame at the head would already have
199
+ // switched the mode), so the session cannot continue.
200
+ throw new JsonRpcFramingFatalError(`Content-Length header is ${this.buffer.length} bytes with no header terminator, exceeding ` +
201
+ `the header limit of ${MAX_CONTENT_LENGTH_HEADER_BYTES} bytes; the stdio session is terminated.`);
202
+ }
203
+ if (this.buffer.length <= this.maxFrameBytes) {
204
+ return;
205
+ }
206
+ if (this.mode === "content-length" && headerBoundary) {
207
+ return;
208
+ }
209
+ if (this.mode !== "content-length" && this.buffer.includes(0x0a)) {
210
+ return;
211
+ }
212
+ const observedBytes = this.buffer.length;
213
+ const description = this.mode === "line" ? "Line-delimited JSON-RPC frame" : "Headerless JSON-RPC input";
214
+ this.buffer = Buffer.alloc(0);
215
+ throw new Error(`${description} is ${observedBytes} bytes, exceeding the configured frame limit of ` +
216
+ `${this.maxFrameBytes} bytes.`);
217
+ }
218
+ /**
219
+ * Rejects a Content-Length frame whose declared body can never be accepted
220
+ * (over the frame limit, or behind an over-limit header block).
221
+ *
222
+ * The body is skipped ONLY when every declared byte is already buffered. In
223
+ * that case the skip is a bounded operation on bytes in hand and it resumes
224
+ * at exactly the offset the peer itself named as the next frame's first
225
+ * byte — a resynchronization the reader can prove without extending trust to
226
+ * a single unarrived byte.
227
+ *
228
+ * When the body has NOT fully arrived the reader must not wait for it: the
229
+ * declared length is attacker-controlled, and arming a countdown with it is
230
+ * precisely what let a 29-byte header (`Content-Length: 999999999\r\n\r\n`
231
+ * with no body) silently swallow every later frame for the process lifetime.
232
+ * There is no delimiter to scan forward to either — an arbitrary binary body
233
+ * offers none — so the session is terminated instead.
234
+ */
235
+ rejectDeclaredBody(messageStart, contentLength, reason) {
236
+ const frameEnd = BigInt(messageStart) + contentLength;
237
+ if (BigInt(this.buffer.length) >= frameEnd) {
238
+ this.buffer = this.buffer.subarray(Number(frameEnd));
239
+ this.awaitedFrameEnd = -1;
240
+ this.mode = "unknown";
241
+ throw new Error(reason);
91
242
  }
92
- return chunk.includes(0x0a);
243
+ const arrivedBodyBytes = Math.max(0, this.buffer.length - messageStart);
244
+ throw new JsonRpcFramingFatalError(`${reason} Only ${arrivedBodyBytes} of the declared ${contentLength.toString()} body bytes have ` +
245
+ "arrived, so the reader cannot resynchronize without trusting bytes that may never be sent; " +
246
+ "the stdio session is terminated.");
93
247
  }
94
248
  detectMode() {
95
249
  while (this.buffer.length > 0) {
@@ -129,14 +283,26 @@ export class JsonRpcFrameReader {
129
283
  if (newlineIndex === -1) {
130
284
  return undefined;
131
285
  }
286
+ const lineBytes = newlineIndex > 0 && this.buffer[newlineIndex - 1] === 0x0d
287
+ ? newlineIndex - 1
288
+ : newlineIndex;
289
+ if (lineBytes > this.maxFrameBytes) {
290
+ this.buffer = this.buffer.subarray(newlineIndex + 1);
291
+ throw new Error(`Line-delimited JSON-RPC frame is ${lineBytes} bytes, exceeding the configured ` +
292
+ `frame limit of ${this.maxFrameBytes} bytes.`);
293
+ }
132
294
  const line = this.buffer.subarray(0, newlineIndex).toString("utf8").replace(/\r$/, "");
133
295
  this.buffer = this.buffer.subarray(newlineIndex + 1);
134
296
  if (line.trim().length === 0) {
135
297
  continue;
136
298
  }
137
299
  if (/^\s*content-length\s*:/i.test(line)) {
138
- const separator = this.buffer.length > 0 && this.buffer[0] === 0x0d ? "\r\n" : "\n";
139
- this.buffer = Buffer.concat([Buffer.from(`${line}${separator}`, "utf8"), this.buffer]);
300
+ // Always re-inject with CRLF: choosing the separator from the next
301
+ // buffered byte raced byte-granular chunking (the peer's \r may not
302
+ // have arrived yet, yielding a "\n\r\n" boundary findHeaderBoundary
303
+ // cannot see). With CRLF both peer styles stay recognizable:
304
+ // "...\r\n" + "\r\n…" → "\r\n\r\n", "...\r\n" + "\n…" → "\n\n".
305
+ this.buffer = Buffer.concat([Buffer.from(`${line}\r\n`, "utf8"), this.buffer]);
140
306
  this.mode = "content-length";
141
307
  return undefined;
142
308
  }
@@ -145,6 +311,42 @@ export class JsonRpcFrameReader {
145
311
  }
146
312
  readContentLengthMessage() {
147
313
  this.awaitedFrameEnd = -1;
314
+ // Skip blank separator lines between frames so the mid-stream mode check
315
+ // below sees the first byte of the next frame.
316
+ while (this.buffer.length > 0) {
317
+ if (this.buffer[0] === 0x0a) {
318
+ this.buffer = this.buffer.subarray(1);
319
+ continue;
320
+ }
321
+ if (this.buffer.length >= 2 && this.buffer[0] === 0x0d && this.buffer[1] === 0x0a) {
322
+ this.buffer = this.buffer.subarray(2);
323
+ continue;
324
+ }
325
+ break;
326
+ }
327
+ // Mirror of the line→content-length switch in readLineDelimitedMessage:
328
+ // a JSON object or array opener can never begin a Content-Length header block,
329
+ // so this is a line-delimited frame arriving after a Content-Length frame.
330
+ // Arrays are re-dispatched only to surface their JSON-RPC schema error, not
331
+ // accepted as batch messages. Peek past leading whitespace (pure line mode
332
+ // tolerates it: blank lines are skipped and JSON.parse accepts a
333
+ // whitespace-prefixed line) without consuming it, then switch modes and let
334
+ // processChunk re-dispatch the buffered bytes, so every frame is delivered
335
+ // with its own true mode.
336
+ let probeIndex = 0;
337
+ while (probeIndex < this.buffer.length &&
338
+ (this.buffer[probeIndex] === 0x20 ||
339
+ this.buffer[probeIndex] === 0x09 ||
340
+ this.buffer[probeIndex] === 0x0d ||
341
+ this.buffer[probeIndex] === 0x0a)) {
342
+ probeIndex += 1;
343
+ }
344
+ if (probeIndex < this.buffer.length &&
345
+ (this.buffer[probeIndex] === 0x7b /* '{' */ ||
346
+ this.buffer[probeIndex] === 0x5b /* '[' */)) {
347
+ this.mode = "line";
348
+ return undefined;
349
+ }
148
350
  const headerBoundary = findHeaderBoundary(this.buffer);
149
351
  if (!headerBoundary) {
150
352
  return undefined;
@@ -164,12 +366,19 @@ export class JsonRpcFrameReader {
164
366
  const headerName = headerLine.slice(0, separatorIndex).trim().toLowerCase();
165
367
  const headerValue = headerLine.slice(separatorIndex + 1).trim();
166
368
  if (headerName === "content-length") {
167
- const parsed = Number.parseInt(headerValue, 10);
168
- if (!Number.isFinite(parsed) || parsed < 0) {
369
+ if (contentLength !== undefined) {
370
+ // Two declarations, no way to tell which delimits the body: the
371
+ // classic frame-smuggling shape. Last-wins would hand an attacker
372
+ // the choice of where the reader thinks this frame ends.
373
+ throw new JsonRpcFramingFatalError(`Duplicate Content-Length header (${contentLength.toString()} then ${headerValue}): the ` +
374
+ "declared body length is ambiguous, so the reader cannot determine where this frame " +
375
+ "ends; the stdio session is terminated.");
376
+ }
377
+ if (!/^[0-9]+$/.test(headerValue)) {
169
378
  this.buffer = this.buffer.subarray(headerBoundary.index + headerBoundary.delimiterBytes);
170
379
  throw new Error(`Invalid Content-Length header value: ${headerValue}`);
171
380
  }
172
- contentLength = parsed;
381
+ contentLength = BigInt(headerValue);
173
382
  }
174
383
  }
175
384
  if (contentLength === undefined) {
@@ -177,14 +386,41 @@ export class JsonRpcFrameReader {
177
386
  throw new Error("Missing Content-Length header.");
178
387
  }
179
388
  const messageStart = headerBoundary.index + headerBoundary.delimiterBytes;
180
- const frameEnd = messageStart + contentLength;
389
+ if (contentLength > BigInt(this.maxFrameBytes)) {
390
+ this.rejectDeclaredBody(messageStart, contentLength, `Content-Length ${contentLength.toString()} exceeds the configured frame limit of ` +
391
+ `${this.maxFrameBytes} bytes.`);
392
+ }
393
+ if (messageStart > MAX_CONTENT_LENGTH_HEADER_BYTES) {
394
+ this.rejectDeclaredBody(messageStart, contentLength, `Content-Length header is ${messageStart} bytes, exceeding the header limit of ` +
395
+ `${MAX_CONTENT_LENGTH_HEADER_BYTES} bytes.`);
396
+ }
397
+ const frameEnd = messageStart + Number(contentLength);
181
398
  if (this.buffer.length < frameEnd) {
182
399
  this.awaitedFrameEnd = frameEnd;
183
400
  return undefined;
184
401
  }
185
402
  const body = this.buffer.subarray(messageStart, frameEnd).toString("utf8");
186
403
  this.buffer = this.buffer.subarray(frameEnd);
187
- return parseJsonRpcMessage(body);
404
+ let payload;
405
+ try {
406
+ payload = JSON.parse(body);
407
+ }
408
+ catch (parseError) {
409
+ // The declared length is the ONLY delimiter a Content-Length frame has,
410
+ // and it just failed its one verification: bytes cut at that offset are
411
+ // not a JSON value. An under-declared length (tail garbage left in the
412
+ // buffer, which used to corrupt the NEXT frame), an over-declared length
413
+ // (the next frame already swallowed into this body) and an honestly
414
+ // framed but malformed body are indistinguishable here — and the first
415
+ // two have already desynchronized the stream. Line framing keeps its
416
+ // recoverable parse errors; its newline proves the boundary.
417
+ throw new JsonRpcFramingFatalError(`Content-Length frame body of ${Number(contentLength)} bytes is not valid JSON ` +
418
+ `(${asError(parseError).message}); the declared length cannot be trusted to delimit the ` +
419
+ "next frame, so the stdio session is terminated.");
420
+ }
421
+ // Valid JSON of exactly the declared length: the frame boundary is proven,
422
+ // so a JSON-RPC schema violation is an ordinary message-level error.
423
+ return parseJSONRPCMessage(payload);
188
424
  }
189
425
  }
190
426
  //# sourceMappingURL=json-rpc-framing.js.map
@@ -0,0 +1,88 @@
1
+ import type { SourceMapping } from "../../types.js";
2
+ import type { PairKey } from "../internal-types.js";
3
+ /**
4
+ * Bytes of JS heap one index slot costs, measured on Minecraft 1.21.10 loom
5
+ * mappings (2,080 MB peak / 6,755,994 slots ~= 323 B; the Mojang proguard index
6
+ * lands at 330 B). Rounded up so the derived budget errs toward stopping early.
7
+ */
8
+ export declare const HEAP_BYTES_PER_INDEX_ENTRY = 340;
9
+ /**
10
+ * How many index slots one Loom tiny load may accumulate before it stops and
11
+ * warns instead of exhausting the heap.
12
+ *
13
+ * The default is derived from the live V8 heap limit, so raising
14
+ * `--max-old-space-size` raises the budget automatically. `MCP_LOOM_TINY_MAX_INDEX_ENTRIES`
15
+ * overrides it outright.
16
+ */
17
+ export declare function resolveTinyIndexEntryBudget(value?: string | undefined, heapStats?: {
18
+ heap_size_limit: number;
19
+ used_heap_size: number;
20
+ }): number;
21
+ export type TinyFileCandidate = {
22
+ path: string;
23
+ bytes: number;
24
+ /**
25
+ * Normalized first namespace column: the coordinates every descriptor in the file
26
+ * uses. `official` and `obfuscated` normalize to the same value.
27
+ */
28
+ descriptorNamespace: SourceMapping | string;
29
+ namespaces: SourceMapping[];
30
+ };
31
+ export type TinySelection = {
32
+ /** Files to merge, in the order they should be merged. */
33
+ selected: TinyFileCandidate[];
34
+ /** Files whose bytes duplicate an already-selected file exactly. */
35
+ duplicateOf: Map<string, string>;
36
+ /**
37
+ * Files skipped because they restate namespace pairs already covered by a
38
+ * selected file while declaring their descriptors in a different namespace.
39
+ */
40
+ descriptorConflicts: TinyFileCandidate[];
41
+ /** Files whose header is not a usable tiny v2 header. */
42
+ unsupported: string[];
43
+ };
44
+ /**
45
+ * Read just the first line of a tiny file and derive its namespace layout.
46
+ * Returns `undefined` for anything {@link parseTinyHeader} rejects, so the body
47
+ * of an unusable file is never read.
48
+ */
49
+ export declare function readTinyFileCandidate(path: string): Promise<TinyFileCandidate | undefined>;
50
+ /** Every ordered namespace pair a candidate's rows would populate. */
51
+ export declare function candidatePairKeys(candidate: TinyFileCandidate): PairKey[];
52
+ /**
53
+ * Richest first: more namespaces beats fewer; then a file whose descriptors are
54
+ * written in obfuscated coordinates beats one that is not, because that is the
55
+ * form callers query with and the form every other tiny source this server reads
56
+ * (Fabric intermediary and yarn artifacts, the Maven loader) produces; then larger
57
+ * beats smaller; then path order so the result never depends on filesystem
58
+ * enumeration order.
59
+ *
60
+ * The descriptor-coordinate rule also decides which rendering becomes the one the
61
+ * descriptor-conflict filter measures the rest against, so it must come before size:
62
+ * Loom's `mappings-base.tiny` is written with `intermediary` first AND is the larger
63
+ * file, and letting it win would leave the whole index carrying intermediary-coordinate
64
+ * descriptors.
65
+ */
66
+ export declare function compareCandidates(left: TinyFileCandidate, right: TinyFileCandidate): number;
67
+ /**
68
+ * Decide which of the discovered tiny files to merge.
69
+ *
70
+ * Loom keeps several renderings of one layered mapping set side by side in one
71
+ * output directory: the final `mappings.tiny`, a `-mojang` rendering with an extra
72
+ * column, `-migrated` renderings per loader build, and a `mappings-base.tiny`
73
+ * written with the namespace columns rotated. Two filters apply:
74
+ *
75
+ * 1. Byte-identical files are collapsed. Loom copies the same rendering into every
76
+ * layered variant directory, so re-reading them is pure repeated work.
77
+ * 2. A file is dropped as an alternative rendering when ALL of the following hold:
78
+ * it declares its descriptors in a different namespace than the rendering
79
+ * already chosen, it sits in a directory that already yielded a selected file,
80
+ * and every namespace pair it carries is already covered. Tiny v2 writes each
81
+ * descriptor once, in the file's first namespace; merging an `intermediary`-first
82
+ * rendering into an `official`-first index registers every method a second time
83
+ * under an incompatible descriptor, which turns unique lookups into ambiguous
84
+ * ones. All three conditions are required so that a genuinely different mapping
85
+ * file — a standalone yarn tiny, a cache in another Gradle root — is never
86
+ * mistaken for a rendering of data already loaded.
87
+ */
88
+ export declare function selectTinyFiles(paths: string[]): Promise<TinySelection>;