@adhisang/minecraft-modding-mcp 6.3.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.
- package/CHANGELOG.md +62 -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 +6 -6
- 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/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/era-classifier.d.ts +161 -0
- package/dist/era-classifier.js +292 -0
- package/dist/error-mapping.js +9 -2
- 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/mcp-helpers.d.ts +19 -2
- package/dist/mcp-helpers.js +48 -6
- 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/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 +69 -1
- package/dist/source/artifact-resolver.js +215 -14
- package/dist/source/class-source.d.ts +21 -0
- package/dist/source/class-source.js +125 -28
- 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-service.d.ts +11 -0
- 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 +309 -11
- 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 +194 -19
- package/package.json +8 -5
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,68 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project aims to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [7.0.0-rc.0] - 2026-08-22
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- MCP protocol revision `2026-07-28` (modern stateless era), served alongside the legacy initialize protocol in one process: `server/discover` returns `supportedVersions: ["2026-07-28"]`, capabilities, and the server identity; modern requests carry per-request `io.modelcontextprotocol/*` `_meta` (protocol version, client capabilities, optional client info) that survives the supervisor/worker boundary, queueing, and worker restarts; every modern result — including supervisor-synthesized overflow/restart/timeout replies — carries `resultType: "complete"` and the server-identity `_meta` echo; the cacheable methods (`tools/list`, `resources/list`, `resources/read`, `resources/templates/list`, `server/discover`) return `ttlMs`/`cacheScope` per the adopted private-cache policy; unsupported modern protocol versions answer `-32022` with `data.supported`/`data.requested`. Era selection is a supervisor-owned one-way lock with machine-readable rejections (`data.kind: "era_conflict"` / `"missing_meta"`); modern worker restarts never replay `initialize`. Contract details: `docs/tool-reference.md` → MCP Protocol Support. Verification: dual-era wire suites (`tests/stdio/stdio-supervisor-era-state.test.ts`, `stdio-supervisor-era-wire.test.ts`, `stdio-supervisor-era-lifecycle.test.ts`, `stdio-supervisor-era-context.test.ts`) plus modern-surface suites (`stdio-modern-*.test.ts`) in the full green suite.
|
|
15
|
+
- SQLite and decompiler tuning knobs: `MCP_SQLITE_CACHE_KB` (page cache in KiB, default `8000`) and `MCP_SQLITE_MMAP_SIZE` (bytes, default `268435456`, `0` disables) tune the artifact-index database, which now also uses `temp_store=MEMORY`; `MCP_DECOMPILE_MAX_MEMORY_MB` (default `4096`) caps the Vineflower JVM heap the same way `MCP_REMAP_MAX_MEMORY_MB` caps remapping. Defaults preserve existing behavior on typical installations.
|
|
16
|
+
- `MCP_MAX_FRAME_BYTES` bounds the JSON-RPC frame size accepted by the stdio supervisor and worker transport (default 64 MiB, clamped to at least 1 MiB). An oversized or `Content-Length`-abusive frame is rejected with a diagnostic naming the observed size and the configured limit, header sections are capped at 8 KiB, and line-delimited frames obey the same limit. A framing violation the reader cannot provably resynchronize past is FATAL: the session is terminated with a `supervisor.client_framing_fatal` diagnostic and exit code 1 rather than silently consuming later frames.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **Behavior change.** `get-class-members` (and `batch-class-members`) now inherit the resolved artifact's mapping when the call supplies no explicit `mapping`, matching what `get-class-source` already did. Previously an artifact resolved as mojang answered member reads in the OBFUSCATED namespace — `ownerFqn: "dlp"` with fields `e, f, g` — which silently broke the documented `find-class` → `get-class-source` → `get-class-members` flow; the two tools returned different namespaces for the same artifact and the same absent argument. `context.mappingNamespace` and `minecraftVersion` no longer contradict `returnedNamespace`. An explicit `mapping` is still never overridden. Note the `get-class-members.mapping` schema description still reads "default obfuscated"; the advertised `inputSchema` bytes are pinned for legacy wire parity, so the artifact-target qualifier is documented in `docs/tool-reference.md` instead.
|
|
21
|
+
- **Behavior change — result counts differ.** `compare-versions` now lifts both jars' class lists into the mojang namespace before diffing, instead of diffing raw obfuscated jar entries. Obfuscated names are not stable across Minecraft versions, so the previous diff compared unrelated symbols: on the measured 1.21.10→1.21.11 pair the unfiltered result moved from `added 236 / removed 0 / unchanged 6386` to `added 616 / removed 380 / unchanged 6006`. Results now carry `classes.namespace` and `classes.packageFilter { matchedFrom, matchedTo }`; a filter that matches nothing emits an explicit warning instead of returning a zeroed result that reads as "no changes"; and an unavailable mapping degrades to the obfuscated namespace with a loud warning rather than silently.
|
|
22
|
+
- **Behavior change — a previously successful call now fails closed.** In `validate-access-widener` and `validate-access-transformer`, a `scope: "merged"` runtime resolution that would serve a jar from a DIFFERENT loader than the workspace is now refused with `ERR_CONTEXT_UNRESOLVED` instead of being answered. The refusal sits on those two tools' runtime-evidence guard — the only path that carries the loader mismatch — so `resolve-artifact` and `get-class-source` resolve exactly as before. Previously the fallback could serve a NeoForge 1.21.10 jar for a Fabric 1.21.11 request, and `validate-access-widener` then reported `valid: true` for a Fabric access widener validated against that NeoForge jar — a false PASS. A same-loader version drift is still answered, now marked `approximate: true`. Fallback policy is otherwise unchanged.
|
|
23
|
+
- **Behavior change — a different artifact is resolved.** `inspect-minecraft` now hands a workspace subject to artifact resolution as `target.kind="workspace"`, exactly as `resolve-artifact` handles the same directory, instead of reading `minecraft_version` itself and resolving `target.kind="version"` with no mapping. The old path left `mapping` undefined, which normalizes to `obfuscated`; that flips the remap gate, changes `mappingVariant`, and therefore changes the hash the artifact id is built from, and it also skips Loom source-jar discovery — so `inspect-minecraft` and `resolve-artifact` returned DIFFERENT artifacts for the same project directory. The visible symptom: `task="class-members"` looked a mojang FQCN up verbatim in an obfuscated jar and failed, while `batch-class-members` — which forwards the workspace target untouched — succeeded on the same class. Workspace resolution now picks up the project's detected compile mapping and loader scope, so a call that previously resolved an obfuscated vanilla artifact can now resolve the merged, mojang-mapped one and report a different `artifactId`. An explicit `subject.mapping` / `subject.scope` is still never overridden, and `WORKSPACE_TARGET_OFF=1` keeps the previous routing.
|
|
24
|
+
- **Behavior change — a response field reports a different set.** `resolve-method-mapping-exact` now returns the strict candidates the verdict was actually computed from on `status: "resolved"` and `status: "ambiguous"` — the two verdicts the strict subset decides — instead of the wider name-matched list. `status: "not_found"` and `status: "mapping_unavailable"` deliberately keep reporting that wider name-matched list: no strict subset produced either verdict, and on a miss the near misses are the useful content. The verdict has always been decided by the strict subset, but the response was built from the raw lookup, so a caller could be shown a `confidence: 1, matchKind: "exact"` candidate — on another owner, under another descriptor — sitting beside `status: "ambiguous"` with nothing to mark it as one the tool had already rejected. `candidates` and `candidateCount` therefore shrink on any query where the simple-name index contributed extra matches. On `status: "resolved"` the reported set is the single match, which makes `candidates` redundant with `resolvedSymbol`, so the default response projection omits it and returns `resolvedSymbol` with `candidateCount: 1`. Rejected candidates are accounted for by a warning on the ambiguous verdict that reports the counts BY REASON — how many were rejected for their owner and how many for their descriptor — because a single cause would name the wrong one; a `resolved` answer whose candidate count shrank carries no such accounting, and `find-mapping` still returns the unfiltered list. The ambiguous result also populates `ambiguityReasons[]`, which the type declared but this path never set; because `resolve-workspace-symbol` spreads this result for `kind: "method"`, that field now appears on its method-ambiguous responses too, and it is present only on `status: "ambiguous"` where it always holds at least one reason. No short-circuit was added: the tool still refuses to pick a winner, which is the whole point of the strict variant.
|
|
25
|
+
- **Behavior change.** `resolve-method-mapping-exact` is now OWNER-strict: its strict filter requires the query's full advertised triple `owner + name + descriptor`, where it previously checked the descriptor alone. The tool takes a REQUIRED `owner` and sells itself as the strict variant of `find-mapping`, but the member index also keys every method under an OWNERLESS `<name><descriptor>` key, so a same-signature method on an unrelated class was pulled in at `matchKind: "simple-name"`, survived the descriptor filter, and forced `status: "ambiguous"`. On a two-owner fixture where the queried class declares exactly one matching method, the result moves from `ambiguous` with 2 candidates — one of them `matchKind: "exact", confidence: 1` — to `resolved` naming that one method. The owner is compared after being projected along the same mapping path the candidates travelled, since the query owner arrives in the source namespace while candidate owners are declaring classes in the target namespace. The accepted cost: a method the owner INHERITS rather than declares moves from `ambiguous` to `not_found`, because the mapping formats record declarations and carry no class hierarchy. That case is not silent — the result carries a warning naming the queried owner, the classes that do declare the member, and `find-mapping` as the owner-agnostic lookup. Two methods on the SAME owner remain `ambiguous`.
|
|
26
|
+
- **Behavior change — a different error code.** When a workspace subject's Minecraft version cannot be detected, `inspect-minecraft` now fails with `ERR_WORKSPACE_VERSION_UNRESOLVED`, which names the `projectPath` and carries `nextAction` plus a `suggestedCall`. Previously the tool degraded to an empty `artifactId` and the failure surfaced further down as `ERR_INVALID_INPUT` "Either artifactId or target must be provided." — an error that described neither the cause nor the fix. `task="class-overview"` and `task="file"`, which answered this case with a `blocked` summary, now surface the typed error too.
|
|
27
|
+
- **Wire contract change — both eras.** Advertised capabilities now carry `resources: { listChanged: false }` and `tools: { listChanged: false }`, where both were previously `true`. This changes the observable bytes of the legacy `initialize` result as well as the modern `server/discover` result; the key order (`resources` before `tools`) and the rest of the payload are unchanged, and no prompts, subscriptions, or logging capability is advertised. The `true` was the SDK's default for any registered tool or resource surface and was never true of this server: the tool and resource surface is fixed at process start by environment flags, `notifications/tools/list_changed` and `notifications/resources/list_changed` are never emitted, and `subscriptions/listen` answers `-32601` in both eras — so the flag advertised a stream no client could subscribe to and that would never have carried anything. A client that keyed list invalidation to the advertised flag should treat both lists as static for the lifetime of the process and re-read them only across a restart. `buildServer()` now passes an explicit `capabilities` option to override the SDK default; the suppression is deliberate and is NOT era-gated, so the two eras cannot disagree. Contract details: `docs/tool-reference.md` → MCP Protocol Support. Pinned by `tests/stdio/stdio-modern-discover-contents.test.ts` (modern `server/discover`) and `tests/stdio/stdio-dependency-method-inventory.test.ts` (legacy `initialize`, over the real wire).
|
|
28
|
+
- Runtime migrated from `@modelcontextprotocol/sdk` 1.27.1 (SDK v1) with Zod 3 to `@modelcontextprotocol/server` 2.0.0 / `@modelcontextprotocol/client` 2.0.0 (SDK v2) with Zod 4.4.3, both exact-pinned. Legacy clients keep byte-compatible wire behavior: advertised `inputSchema` bytes are pinned to the pre-migration snapshots (`src/v1-parity-schemas.ts`, `tests/fixtures/premigration/tool-contracts/`), `runTool()` remains the sole tool-argument validator through an identity registration adapter (`src/registration-adapter.ts`, no SDK-private patching — guarded by `tests/contracts/no-sdk-private-request-handler-access.test.ts`), Zod error bytes keep zod3 parity (`tests/utils/zod3-parity.test.ts`), and initialization capture/replay across worker restarts is unchanged. Five recorded legacy exceptions: (1) `tools/call` with omitted `arguments` reaches the application validator as `{}` — input-free tools succeed, required-field schemas answer per-field `ERR_INVALID_INPUT`, and a `{}`-accepting schema with an input-requiring handler answers that tool's handler-level ProblemDetails (`json-to-nbt` → `ERR_NBT_INVALID_TYPED_JSON`); (2) legacy `tools/list` entries omit the v1-only `execution: {"taskSupport":"forbidden"}` field (SDK v2 does not emit it; all other advertised fields identical); (3) unknown/disabled-tool replies are synthesized at the supervisor pre-queue with identical bytes but no queue-slot consumption, so reply ordering and overflow outcomes can differ from v1 under concurrent load; (4) the unmatched-resource-URI error keeps code `-32602` but its message changed from `MCP error -32602: Resource <uri> not found` to `Resource not found: <uri>` with a `data.uri` field (SDK v2 wording); (5) the `initialize` result advertises `resources: { listChanged: false }` and `tools: { listChanged: false }`, where the pre-migration server advertised `true` for both — the suppression is deliberate, and it is not era-gated because both eras read one `getCapabilities()` and a per-era split would leave one process advertising two contracts.
|
|
29
|
+
- Modern-era `tools/list` returns raw tool-name-ascending order; legacy processes keep the frozen pre-migration registration order (goldens: `tests/fixtures/premigration/tools-list-order.*.json`).
|
|
30
|
+
- **Breaking (Node package surface).** The exported `server` is now an SDK v2 `McpServer` with a different API surface than the v1 instance it replaces, `buildServer` takes an optional `McpRequestContext` and branches on `ctx?.era`, and the emitted `dist/index.d.ts` resolves Zod 4 types. Consumers importing this package as a library — rather than speaking to it over stdio — must migrate; the byte-compatibility guarantee above covers the WIRE surface only.
|
|
31
|
+
- **Breaking (minimum runtime).** `engines.node` is raised from `>=22` to `>=22.13.0`. `src/storage/sqlite.ts` imports `node:sqlite` unguarded at module load, on the static import path the `dist/cli.js` bin reaches before it serves anything, and `src/storage/symbols-repo.ts` calls `StatementSync.iterate`. **22.13.0** is the first release where either is usable unflagged: `node:sqlite` landed in 22.5.0 but stayed behind `--experimental-sqlite` until 22.13.0 unflagged it, and 22.13.0 is also where `StatementSync.prototype.iterate` was added. Nothing in this package passes that flag — the bin is a plain `#!/usr/bin/env node` — so on 22.0–22.12 the unguarded import throws at process start and the server never reaches a request, rather than degrading; on 22.13.0 and later both the module and the iterator are present. The previous `>=22` floor understated the real requirement.
|
|
32
|
+
- `provenance.mappingArtifact` for Loom-sourced mappings now names the file actually merged rather than the alphabetically first discovered file (for the measured 1.21.10 cache, `.../mappings-mojang.tiny` instead of `intermediary-v2.tiny`).
|
|
33
|
+
- Modern-era `resources/read` for a URI that matches a registered template but has no backing artifact now answers JSON-RPC `-32602` (the revision requires an error for a resource that does not exist) instead of a successful `resultType: "complete"` carrying a ProblemDetails document. Applies to the five 404-class AppError codes. Legacy replies keep the ProblemDetails envelope and its cache-field absence.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- The published tarball no longer risks shipping internal design documents. `files` listed `docs/**/*.md`, and because an npm `files` allowlist overrides `.gitignore`, a publish run from a working tree that contained `docs/plans/`, `docs/specs/`, or `docs/reports/` would have included them — 19 files and about 840 kB on the tree this was measured from, among them the implementation plan for this very release. The CI publish path was never exposed (it publishes from a clean `actions/checkout`), but a local `npm publish` was. `files` now names the three intended documents explicitly, so a newly added internal document cannot leak by default.
|
|
38
|
+
- The npm release path can no longer publish a prerelease under the `latest` dist-tag. The publish workflow derives the dist-tag from the package version — a SemVer prerelease publishes under `rc`, anything else under `latest` — fails before publishing when the pushed git tag and the package version disagree, and refuses to run when the frozen named-test set gate's escape hatch is present in the environment.
|
|
39
|
+
- Runtime provenance no longer claims a version or loader that was not served. `provenance` now describes the jar actually used — `version`, `requestedVersion`, `versionApproximated`, `servedLoader`, `expectedLoader`, `loaderMismatch` — where it previously echoed the REQUESTED version back to the caller, so a substituted artifact was indistinguishable from an exact match. `resolve-artifact` gains `provenance.versionApproximation` and corrects `resolvedFrom.version`.
|
|
40
|
+
- `get-artifact-file` serves root-level and `META-INF/**` jar entries. Delivery was gated on an `assets/`/`data/` prefix with no safety purpose, so `fabric.mod.json`, `META-INF/MANIFEST.MF`, `<mod>.mixins.json`, and license files all answered `ERR_FILE_NOT_FOUND` despite being present in the jar — precisely the files an agent inspecting a mod reads first. Extension-less entries are now classified by sniffing for valid UTF-8 rather than assumed binary. The 512 KiB cap, the `truncated` flag, the `contentOmittedReason` for binary entries, and `ERR_INVALID_INPUT` for traversal-shaped paths are unchanged.
|
|
41
|
+
- `validate-access-transformer` resolves its context from a NeoForge/ModDevGradle workspace. Candidate discovery required an exact Minecraft-version token in the artifact path, but ModDevGradle names artifacts after the LOADER version (`neoforge-21.11.38-beta-*.jar` for Minecraft 1.21.11), so every candidate was dropped and the canonical NeoForge example project answered `ERR_CONTEXT_UNRESOLVED` even with `projectPath` supplied. Loader-to-Minecraft version equivalence is now recognized and recorded as a provenance note, and resources-only `client-extra` jars are deprioritized.
|
|
42
|
+
- An error hint can no longer ask the caller to supply a parameter they already supplied. The `validate-access-transformer` hint was the observed instance; the fix is applied generically to execution-error guidance so any tool's hints drop asks the request already satisfied.
|
|
43
|
+
- The typed-NBT rejections say what is wrong and what to do about it. `ERR_NBT_INVALID_TYPED_JSON` published `code`, `detail`, `status`, `retryClass`, and `issueOrigin` and nothing else: its `jsonPointer` / `expectedType` / `actualType` were stripped by the context allowlist, and there were no `fieldErrors`, no `hints`, and no `suggestedCall` — so a caller was told "Invalid typed NBT JSON document." with no way to learn WHICH node, and the `typedJson` argument advertises an empty JSON Schema (`{}`, frozen for legacy parity) that cannot tell them either. The error now carries `fieldErrors[0].path` (the RFC6901 pointer into the document, or `typedJson` for a root-level failure) plus a `nextAction` naming the expected and received types and the document shape, and `json-to-nbt` / `nbt-apply-json-patch` attach an `exampleCalls` entry pointing at `nbt-to-json` as the reliable way to obtain a valid document. `ERR_NBT_PARSE_FAILED`, `ERR_NBT_ENCODE_FAILED`, `ERR_JSON_PATCH_INVALID`, `ERR_NBT_UNSUPPORTED_FEATURE`, and `ERR_JSON_PATCH_CONFLICT` gain default `nextAction` guidance the same way. No message string and no advertised schema changed; the required document shape is documented in `docs/tool-reference.md` instead.
|
|
44
|
+
- `docs/tool-reference.md` no longer states a condition on the suggested-call fallback hint that the code does not implement. It documented `"suggested call payload failed schema validation; using fallback examples"` as firing only when a primary suggestion is dropped AND no `exampleCalls[]` fallback exists; neither emitting branch checks that, and all six recorded tool-contract envelopes that carry the sentence also carry `exampleCalls`. The sentence also fires for a deliberate placeholder-template drop, where nothing was ever schema-validated. The wording itself is unchanged and now carries a note saying why: it is frozen byte-for-byte in ten pre-migration fixtures that cannot be re-recorded.
|
|
45
|
+
- `get-class-source`'s `ERR_CLASS_NOT_FOUND` names the artifact the caller asked about. When the class was missing, an internal binary fallback re-resolved a DIFFERENT artifact and the error was then written from it: `details.artifactId`, `error.context.artifactId` and `details.suggestedCall.params.artifactId` all named an artifact the caller never requested, and `details.didYouMean` was collected from that artifact's symbol index — so the near-miss candidates came from the wrong jar and a `find-class` retry built from the suggestion searched the wrong place. The requested id is reported again; the fallback id survives as an additive `details.fallbackArtifactId`, present only when the two differ. `details.didYouMean` is collected from the requested artifact's index FIRST and then unioned with the fallback artifact's (see the near-miss entry under Fixed). A SUCCESSFUL fallback still reports the fallback artifact, which is where the returned source actually came from. Separately, the fallback now honors `allowDecompile` instead of hardcoding `true`, so a caller who explicitly passed `allowDecompile: false` no longer pays a full Vineflower pass (measured on a first miss: 22.5 s decompile plus 29.3 s indexing). That cost is UNCHANGED for callers who did not decline decompilation — the fallback is not otherwise narrowed.
|
|
46
|
+
- `ERR_CLASS_NOT_FOUND` offers near-miss candidates in the one scenario they were built for. Collecting `details.didYouMean` from the requested artifact alone returned `[]` exactly when the partial-source binary fallback had fired, because the requested artifact is by construction the one WITHOUT `net.minecraft` symbols — which is why the fallback fired and indexed the other jar. A typo'd vanilla class name therefore lost its only recovery signal. Candidates are now taken from the requested artifact first and unioned with the artifact the lookup ended on, deduplicated by fully-qualified name; a candidate found outside the requested artifact carries an additional `artifactId` naming where it was found, so an entry without that field always means the artifact the caller asked about. The identity fields are unchanged: `details.artifactId`, `error.context.artifactId` and `suggestedCall.params.artifactId` still name the REQUESTED artifact.
|
|
47
|
+
- A `get-class-source` failure that followed a nested-jar redirect no longer describes two artifacts at once. The redirect into a shell jar's bundled inner jar replaced the active artifact id, mapping and quality flags together, so the resulting `ERR_CLASS_NOT_FOUND` reported the outer shell as `details.artifactId` while `details.mapping` and `details.qualityFlags` described the inner jar — and `mapping` is published through the context allowlist, so a client acting on it queried the wrong namespace, with a `suggestedCall` pointing `find-class` at an index holding neither the class nor its siblings. All three fields now describe the artifact the caller named; `details.fallbackArtifactId` continues to name where the lookup ended, for the nested-jar redirect as well as the binary fallback.
|
|
48
|
+
- `docs/tool-reference.md` no longer promises that `error.exampleCalls[]` entries are "always-valid" and "safe to re-call as-is". Examples are validated for schema SHAPE only and are explicitly permitted to be templates, and frozen envelopes already publish `<...>` placeholders that would be rejected if replayed verbatim. The guarantee is corrected to what the code provides — right tool, right argument names and types, placeholders possible — with no change to the emitted examples or to any fixture. The same section's `didYouMean` and `resolve-method-mapping-exact` descriptions are updated for the behavior changes above.
|
|
49
|
+
- `find-class` reports a nested type under its own name. A nested class or enum was reported under its OUTER fully-qualified name because the symbol extractor stores one `qualifiedName` per file, so searching `Block` returned `net.minecraft.world.level.ClipContext` (the outer class of `ClipContext.Block`) ranked above the real `net.minecraft.world.level.block.Block`. Nested matches now render as `<Outer>.<Nested>` with `nested` and `enclosingClass` fields, and top-level matches sort first. A zero-result response classifies its warning as `partial_coverage` and carries a `suggestedCall` to `get-class-source` in one specific case — when the resolved artifact carries the `partial-source-no-net-minecraft` quality flag AND the query looks like a deobfuscated class name, the combination where partial coverage is what makes the index unable to answer while the binary fallback still can. Every other empty result is unchanged.
|
|
50
|
+
- `find-mapping` and the other mapping lookups no longer exhaust the worker heap on a populated Fabric Loom cache. Every `.tiny` file discovered for a version under `<gradleUserHome>/caches/fabric-loom/<version>` was read whole and merged into one unbounded index; on a real 1.21.10 cache that is 13 files totalling 87,744,824 bytes, which drove the worker into `Ineffective mark-compacts near heap limit`, `SIGABRT`, and an `ERR_WORKER_RESTART` for the caller. The loader now selects files from a 64 KiB header probe, deduplicates identical contents (those 13 files are only 6 distinct contents), merges into a shared accumulator instead of building a per-file index first, and bounds the accumulation with a heap-derived budget overridable by `MCP_LOOM_TINY_MAX_INDEX_ENTRIES`. Measured on that cache: 4,256.7 MB peak and a crash at 86.5 s became 2,705.3 MB and a resolved answer at 26.1 s, reading 31.0 MB of the 83.7 MB.
|
|
51
|
+
- Loom mapping lookups no longer report phantom candidates from a descriptor-namespace mismatch. Tiny v2 stores each descriptor once in the file's FIRST namespace, and the parser stamped that column onto every namespace untranslated; because `mappings-base.tiny` is headed `intermediary named official` while the sibling renderings are official-first, merging it re-registered every method under a second, foreign-coordinate descriptor — 216,696 rows on the measured cache, all methods. A method query could therefore return two candidates for one real member, and the phantom could outrank the real record in candidate ordering. Selection now drops a rendering only when it uses a different descriptor namespace, sits in the same directory as an already-selected rendering, and adds no uncovered namespace pair. One measured `resolve-method-mapping-exact` query changed from `resolved: false, status: "ambiguous"` with 2 candidates to `resolved: true` with 1. That is the same shape as the owner-strict change but a different cause and a different second candidate: owner-strictness drops a real method declared by an unrelated class that the member index admitted under its OWNERLESS key, while this drops a phantom of the queried method itself, re-registered under a foreign-coordinate descriptor.
|
|
52
|
+
- Modern-era `subscriptions/listen` is rejected at supervisor admission with `-32601` `Method not found`, as the contract always documented, and `maxSubscriptions: 0` is passed to `serveStdio` as defense in depth. Previously a conformant listen carrying a valid `SubscriptionFilter` was SERVED: the SDK answered with an id-less `notifications/subscriptions/acknowledged` notification and never settled the request id, so the supervisor's pending entry survived forever — permanently blocking the `validate-project` barrier and head-of-line-blocking every queued request. Because `notifications/cancelled` released the SDK subscription but not the supervisor entry, repeated listen/cancel pairs also grew `pendingRequests` without bound while evading the SDK's own 1024-subscription cap.
|
|
53
|
+
- A cancelled request now releases its supervisor slot for every method, not only `validate-project`: `notifications/cancelled` settles the pending entry, clears its deadline timer, releases the `validate-project` barrier if it held one, and records an ordinary response-finality tombstone so a late worker answer is still discarded. An `initialize` in flight is excluded, since its handshake lifecycle owns that entry. Finality tombstones are capped with insertion-order eviction so the bookkeeping cannot itself grow without bound.
|
|
54
|
+
- An `initialize` request that is not a valid MCP initialize request no longer locks the process to the legacy era. Previously any generically valid JSON-RPC frame named `initialize` committed the one-way era lock BEFORE method-schema validation, so a malformed handshake permanently burned the era choice: the client received a `-32603` restart error implying a transient failure, and every later modern request answered `-32600` `era_conflict` until the process was respawned. The request is now validated against the SDK's own initialize schema first and rejected with `-32602` `data.kind: "invalid_initialize"` (carrying `data.required[]` and `data.eraSelected: false`), leaving the era unselected and the client free to retry either era.
|
|
55
|
+
- The modern `protocolVersion` VALUE is validated on every modern request instead of only on the one that pins the connection. Previously, once a connection was pinned by a valid request, later requests carrying an unsupported version were served normally — a `tools/call` with `"1999-12-31"` executed its handler — and whether `-32022` fired at all depended on which method the client happened to send first.
|
|
56
|
+
- A framing violation that the reader cannot provably resynchronize past now terminates the session with a `supervisor.client_framing_fatal` diagnostic and exit code 1, instead of silently consuming every later frame. Previously a 29-byte `Content-Length: 999999999\r\n\r\n` header with no body left the reader waiting on the attacker-declared count for the process lifetime, so all subsequent valid frames were swallowed with no further diagnostic; an under-declared `Content-Length` likewise corrupted the next valid request by prefixing it with the unread remainder. Duplicate `Content-Length` headers are rejected rather than resolved last-wins.
|
|
57
|
+
- Line framing recovers after a line-delimited JSON array frame. The Content-Length reader recognized only `{` as an opener that cannot begin a header block, so an array arriving after a Content-Length frame was neither consumed as a line nor parsed as a header, and every subsequent frame stayed buffered indefinitely with no parse error reported. Arrays are re-dispatched only to surface their JSON-RPC schema error; this does not add batch-message support.
|
|
58
|
+
- A `server/discover` admitted while the worker was starting is no longer drained into a `-32601` by a later `initialize`. The queued arrival-order prefix preceding an `initialize` is now forwarded ahead of it; the suffix stays gated until the initialize response.
|
|
59
|
+
- Legacy `tools/call` with a non-object `arguments` reproduces the v1 error bytes again. Method-shape validation now runs before the unknown-tool intercept on legacy connections, so a malformed call naming a missing tool no longer returns a successful `isError` "not found" envelope, and a registered tool answers the v1 raw `-32603` rather than the SDK v2 `-32602`. Modern calls keep the SDK's `-32602`.
|
|
60
|
+
- A supervisor synthetic terminal reply (queue overflow, cap-blocked restart, unknown-tool intercept, or queued-request terminalization) for a request that reuses a live JSON-RPC id no longer settles or tombstones the different live request sharing that id. Previously the live request's worker answer was tombstone-discarded and a running `validate-project` barrier could strand queued requests for the worker's lifetime. The same identity rule also drops an orphaned preserved initialize entry when a cap-blocked re-initialize discards the cached handshake, instead of leaking it into `pendingRequests`. Pinned by `tests/stdio/stdio-supervisor-finality-id-reuse.test.ts`.
|
|
61
|
+
- A legacy `initialize` carrying a valid modern `io.modelcontextprotocol/*` era claim in `params._meta` completes the legacy handshake: the supervisor strips the era-claim keys before the frame reaches the worker, whose opening classifier would otherwise treat a claim-bearing `initialize` as modern and fail the handshake with a worker restart. Non-era `_meta` keys pass through. Pinned by the enveloped-initialize wire test in `tests/stdio/stdio-supervisor-era-wire.test.ts`.
|
|
62
|
+
- Version manifest and version-detail fetches now abort after `MCP_FETCH_TIMEOUT_MS` with a typed `ERR_REPO_FETCH_FAILED` error instead of hanging indefinitely when a repository stops responding.
|
|
63
|
+
- `manage-cache` opens the artifact index through the integrity-checking recovery path: a corrupt SQLite file is backed up and rebuilt instead of crashing cache inspection, and a missing database file is still not created as a side effect of inspection.
|
|
64
|
+
- In supervised stdio mode, a worker that hits a fatal `uncaughtException`/`unhandledRejection` exits after logging so the supervisor restart/replay path replaces it; previously the faulted worker stayed alive with only `exitCode` set, bypassing recovery.
|
|
65
|
+
- The stdio supervisor retries unresolved process-tree cleanup tokens during normal operation with capped exponential backoff, emitting `supervisor.cleanup_token.retry`/`supervisor.cleanup_token.recovered` events and unblocking the 2-slot live cap without waiting for shutdown. Live-cap saturation that blocks a restart is surfaced as `supervisor.live_cap.saturated`.
|
|
66
|
+
- Tool responses keep their typed error envelopes when SQLite is unavailable: input validation still returns `ERR_INVALID_INPUT`, database-independent tools (such as the NBT utilities) still return their successful results, and a metrics-recording failure no longer replaces a completed response.
|
|
67
|
+
- A supervised worker now stands down when its supervisor goes away, instead of surviving as an orphan for the lifetime of the machine. The worker held an unconditional keep-alive `setInterval` whose only `clearInterval` ran from a process `"exit"` listener — which by definition fires once the process is already leaving — so stdin EOF closed the MCP server but never drained the event loop: any launcher that terminated the server without a cooperative shutdown left a resident node process behind — measured at ~125 MB on the reference host — and each restart added another. The worker now releases the keep-alive on stdin EOF, remembers an EOF that arrives while it is still starting (a fast-closing pipe could otherwise deliver `end` before the listener existed), and — as defense in depth for hosts whose stdin is not a pipe the parent solely owns — stands down when the parent pid it snapshotted at startup stops existing. Teardown never calls `process.exit()`, so replies still being written after a half-close are delivered in full. On the supervisor side, a generation that signals readiness and then stands down cleanly before it has been ready for a second no longer counts as a start that succeeded: adoption still clears the restart backoff, and the clean stand-down then raises it back to the level the repeated failure has earned without reserving a restart, so a host that closes every spawned child's stdin immediately backs off exponentially instead of respawning a worker ten times a second. A crash keeps its prompt replacement. Measured on the reference host, a single full `npm test` run each way: 20 resident orphaned workers were left before the fix and 0 after. Test-side teardown moved onto a shared stdin-close → `SIGTERM` → `SIGKILL` ladder (`tests/helpers/stdio-child-lifecycle.ts`), and `tests/contracts/no-direct-sigkill.test.ts` keeps it that way.
|
|
68
|
+
- The stdio supervisor now reaps its worker on `SIGHUP` and on its own fatal errors, instead of leaving the detached worker process group with nobody to collect it. `SIGHUP` — what a terminating launcher or a vanishing session sends — was never registered, so the OS default terminated the supervisor where it stood; an uncaught exception or unhandled rejection in the supervisor process was likewise left to node's default handler, which prints a stack and exits without any route to shutdown. Both now run the supervisor's ordinary shutdown path, which terminates the worker process group; a fatal error is reported as a `supervisor.fatal` event and then ends the process explicitly with a non-zero status, once the worker group has been collected or a bounded watchdog expires. Registering fatal handlers suppresses node's default abort, so that exit can no longer be left to the event loop draining on its own, and the handlers now stay registered until the teardown tail finishes rather than being withdrawn before it.
|
|
69
|
+
|
|
8
70
|
## [6.3.0] - 2026-07-18
|
|
9
71
|
|
|
10
72
|
### Added
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@adhisang/minecraft-modding-mcp)
|
|
4
4
|
[](LICENSE)
|
|
5
|
-
[](https://nodejs.org/)
|
|
5
|
+
[](https://nodejs.org/)
|
|
6
6
|
[](https://github.com/adhi-jp/minecraft-modding-mcp/actions/workflows/ci.yml)
|
|
7
7
|
|
|
8
8
|
**English** | [日本語](docs/README-ja.md)
|
|
@@ -27,13 +27,23 @@ It runs over stdio and works with Claude Desktop, Claude Code, VS Code, Codex CL
|
|
|
27
27
|
- **NBT, registry, cache, and diagnostics**: patch NBT payloads, inspect generated registry data, and manage cache/runtime state
|
|
28
28
|
- **MCP resources**: expose versions, class source, artifact metadata, and mappings through URI-based resources
|
|
29
29
|
|
|
30
|
+
## Protocol Support
|
|
31
|
+
|
|
32
|
+
The server implements MCP protocol revision `2026-07-28` and keeps the legacy initialize-based protocol (`2025-11-25` back through `2024-10-07`) fully supported in the same binary — no configuration needed:
|
|
33
|
+
|
|
34
|
+
- **Legacy clients** initialize as before and see the same tool names, input contracts, and response envelopes — byte-compatible except for a short list of recorded deviations (see the legacy-exceptions list in [docs/tool-reference.md → MCP Protocol Support](docs/tool-reference.md#mcp-protocol-support)).
|
|
35
|
+
- **Modern clients** (2026-07-28) skip `initialize`, probe with `server/discover`, and send per-request `io.modelcontextprotocol/*` `_meta`. Modern results carry `resultType` and the server identity echo; the cacheable methods additionally carry cache metadata (`ttlMs` / `cacheScope`).
|
|
36
|
+
- One stdio process serves one era, selected by the client's first signal; the selection persists across the server's internal worker restarts.
|
|
37
|
+
|
|
38
|
+
Wire-level details — era selection, the rejection and version-negotiation tables, cache values, and framing — are in [docs/tool-reference.md → MCP Protocol Support](docs/tool-reference.md#mcp-protocol-support).
|
|
39
|
+
|
|
30
40
|
## Quick Start
|
|
31
41
|
|
|
32
42
|
### Package Users
|
|
33
43
|
|
|
34
44
|
Requirements:
|
|
35
45
|
|
|
36
|
-
- Node.js 22+
|
|
46
|
+
- Node.js 22.13.0+
|
|
37
47
|
- Java is only required for `remap-mod-jar` and decompile or remap flows that need Vineflower or tiny-remapper
|
|
38
48
|
|
|
39
49
|
Start the server locally:
|
|
@@ -390,7 +400,7 @@ Detailed parameter constraints, migration notes, resource behavior, and the full
|
|
|
390
400
|
|
|
391
401
|
Repository requirements:
|
|
392
402
|
|
|
393
|
-
- Node.js 22+
|
|
403
|
+
- Node.js 22.13.0+
|
|
394
404
|
- `pnpm`
|
|
395
405
|
- Java when running remap or decompile flows locally
|
|
396
406
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { CacheHint } from "@modelcontextprotocol/server";
|
|
2
|
+
/**
|
|
3
|
+
* Adopted cache-hint policy for the 2026-07-28 protocol revision (approved
|
|
4
|
+
* values; single source of truth for every configured `ttlMs`/`cacheScope`).
|
|
5
|
+
*
|
|
6
|
+
* Everything is cacheScope "private": every result reflects this server
|
|
7
|
+
* process's local caches, workspace, and feature-flag configuration, so no
|
|
8
|
+
* shared cache may ever serve it across clients.
|
|
9
|
+
*
|
|
10
|
+
* ttlMs by surface, with the rationale each row stands on:
|
|
11
|
+
* - tools/list and server/discover: 0 — the SDK default; deliberately NOT
|
|
12
|
+
* configured. The advertised tool set depends on process feature flags
|
|
13
|
+
* (BATCH_TOOLS_OFF, VERIFY_MIXIN_TARGET_OFF), so caching would risk
|
|
14
|
+
* serving a stale flag-dependent tool set across a reconfiguration.
|
|
15
|
+
* - resources/list and resources/templates/list: 1 hour. The resource
|
|
16
|
+
* registry is process-stable registration metadata (fixed 2+7 set); a
|
|
17
|
+
* cached copy may briefly outlive a changed configuration, which is
|
|
18
|
+
* acceptable for pure discovery metadata.
|
|
19
|
+
* - resources/read of mc://versions/list: 5 minutes — permits up to five
|
|
20
|
+
* minutes of staleness against the upstream Mojang version manifest.
|
|
21
|
+
* - resources/read of mc://metrics: 0 — live runtime counters, never cache
|
|
22
|
+
* (default value, configured explicitly for self-documentation).
|
|
23
|
+
* - resources/read of the seven template resources (class-source,
|
|
24
|
+
* class-source-json, artifact-file, find-mapping, find-member-mapping,
|
|
25
|
+
* class-members, artifact-metadata): 1 minute — short-lived reuse of
|
|
26
|
+
* locally indexed data; local changes (re-index, re-resolve, remap) may
|
|
27
|
+
* be served stale for up to one minute.
|
|
28
|
+
* - ANY successful resource read whose content is a ProblemDetails envelope:
|
|
29
|
+
* 0, with UNCONDITIONAL precedence over the resource's class row —
|
|
30
|
+
* transient failures represented as successful reads must never be
|
|
31
|
+
* cached, or a recovered resource would keep serving its old error. The
|
|
32
|
+
* override is identified STRUCTURALLY on the errorResource(...) build
|
|
33
|
+
* path (never re-inferred from serialized text) and is emitted as
|
|
34
|
+
* handler-returned result fields, which the SDK's encode seam ranks above
|
|
35
|
+
* configured hints. Because the 2025-era codec serializes
|
|
36
|
+
* handler-returned fields verbatim, the override is era-gated in
|
|
37
|
+
* errorResource() — legacy results never carry cache fields.
|
|
38
|
+
*
|
|
39
|
+
* Configured hints ride the SDK's symbol-keyed carrier (never serialized), so
|
|
40
|
+
* none of the values below can ever surface on a 2025-era response.
|
|
41
|
+
*/
|
|
42
|
+
/** cacheScope for every surface this server emits cache hints for. */
|
|
43
|
+
export declare const APP_CACHE_SCOPE: "private";
|
|
44
|
+
/** resources/list and resources/templates/list: 1 hour. */
|
|
45
|
+
export declare const RESOURCE_LISTS_TTL_MS = 3600000;
|
|
46
|
+
/** resources/read of mc://versions/list: 5 minutes. */
|
|
47
|
+
export declare const VERSIONS_LIST_READ_TTL_MS = 300000;
|
|
48
|
+
/** resources/read of mc://metrics: live counters, never cache. */
|
|
49
|
+
export declare const METRICS_READ_TTL_MS = 0;
|
|
50
|
+
/** resources/read of the seven template resources (class-source, class-source-json, artifact-file, find-mapping, find-member-mapping, class-members, artifact-metadata): 1 minute. */
|
|
51
|
+
export declare const RESOURCE_READ_TTL_MS = 60000;
|
|
52
|
+
/** ProblemDetails resource reads: never cache, regardless of the class row. */
|
|
53
|
+
export declare const PROBLEM_DETAILS_READ_TTL_MS = 0;
|
|
54
|
+
/** Per-operation hints handed to the McpServer constructor (`cacheHints`). */
|
|
55
|
+
export declare const RESOURCE_LISTS_CACHE_HINT: Readonly<CacheHint>;
|
|
56
|
+
/** Per-registration hint for the mc://versions/list fixed resource. */
|
|
57
|
+
export declare const VERSIONS_LIST_READ_CACHE_HINT: Readonly<CacheHint>;
|
|
58
|
+
/** Per-registration hint for the mc://metrics fixed resource (explicit default). */
|
|
59
|
+
export declare const METRICS_READ_CACHE_HINT: Readonly<CacheHint>;
|
|
60
|
+
/** Per-registration hint for every template resource. */
|
|
61
|
+
export declare const RESOURCE_READ_CACHE_HINT: Readonly<CacheHint>;
|
|
62
|
+
/**
|
|
63
|
+
* Handler-returned result fields for the ProblemDetails-read override
|
|
64
|
+
* (precedence rank 1 at the SDK encode seam — beats the class row's
|
|
65
|
+
* configured hint). Spread onto the errorResource result ONLY on modern-era
|
|
66
|
+
* requests; see errorResource() for the era gate.
|
|
67
|
+
*/
|
|
68
|
+
export declare const PROBLEM_DETAILS_READ_CACHE_FIELDS: Readonly<{
|
|
69
|
+
ttlMs: number;
|
|
70
|
+
cacheScope: typeof APP_CACHE_SCOPE;
|
|
71
|
+
}>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adopted cache-hint policy for the 2026-07-28 protocol revision (approved
|
|
3
|
+
* values; single source of truth for every configured `ttlMs`/`cacheScope`).
|
|
4
|
+
*
|
|
5
|
+
* Everything is cacheScope "private": every result reflects this server
|
|
6
|
+
* process's local caches, workspace, and feature-flag configuration, so no
|
|
7
|
+
* shared cache may ever serve it across clients.
|
|
8
|
+
*
|
|
9
|
+
* ttlMs by surface, with the rationale each row stands on:
|
|
10
|
+
* - tools/list and server/discover: 0 — the SDK default; deliberately NOT
|
|
11
|
+
* configured. The advertised tool set depends on process feature flags
|
|
12
|
+
* (BATCH_TOOLS_OFF, VERIFY_MIXIN_TARGET_OFF), so caching would risk
|
|
13
|
+
* serving a stale flag-dependent tool set across a reconfiguration.
|
|
14
|
+
* - resources/list and resources/templates/list: 1 hour. The resource
|
|
15
|
+
* registry is process-stable registration metadata (fixed 2+7 set); a
|
|
16
|
+
* cached copy may briefly outlive a changed configuration, which is
|
|
17
|
+
* acceptable for pure discovery metadata.
|
|
18
|
+
* - resources/read of mc://versions/list: 5 minutes — permits up to five
|
|
19
|
+
* minutes of staleness against the upstream Mojang version manifest.
|
|
20
|
+
* - resources/read of mc://metrics: 0 — live runtime counters, never cache
|
|
21
|
+
* (default value, configured explicitly for self-documentation).
|
|
22
|
+
* - resources/read of the seven template resources (class-source,
|
|
23
|
+
* class-source-json, artifact-file, find-mapping, find-member-mapping,
|
|
24
|
+
* class-members, artifact-metadata): 1 minute — short-lived reuse of
|
|
25
|
+
* locally indexed data; local changes (re-index, re-resolve, remap) may
|
|
26
|
+
* be served stale for up to one minute.
|
|
27
|
+
* - ANY successful resource read whose content is a ProblemDetails envelope:
|
|
28
|
+
* 0, with UNCONDITIONAL precedence over the resource's class row —
|
|
29
|
+
* transient failures represented as successful reads must never be
|
|
30
|
+
* cached, or a recovered resource would keep serving its old error. The
|
|
31
|
+
* override is identified STRUCTURALLY on the errorResource(...) build
|
|
32
|
+
* path (never re-inferred from serialized text) and is emitted as
|
|
33
|
+
* handler-returned result fields, which the SDK's encode seam ranks above
|
|
34
|
+
* configured hints. Because the 2025-era codec serializes
|
|
35
|
+
* handler-returned fields verbatim, the override is era-gated in
|
|
36
|
+
* errorResource() — legacy results never carry cache fields.
|
|
37
|
+
*
|
|
38
|
+
* Configured hints ride the SDK's symbol-keyed carrier (never serialized), so
|
|
39
|
+
* none of the values below can ever surface on a 2025-era response.
|
|
40
|
+
*/
|
|
41
|
+
/** cacheScope for every surface this server emits cache hints for. */
|
|
42
|
+
export const APP_CACHE_SCOPE = "private";
|
|
43
|
+
/** resources/list and resources/templates/list: 1 hour. */
|
|
44
|
+
export const RESOURCE_LISTS_TTL_MS = 3_600_000;
|
|
45
|
+
/** resources/read of mc://versions/list: 5 minutes. */
|
|
46
|
+
export const VERSIONS_LIST_READ_TTL_MS = 300_000;
|
|
47
|
+
/** resources/read of mc://metrics: live counters, never cache. */
|
|
48
|
+
export const METRICS_READ_TTL_MS = 0;
|
|
49
|
+
/** resources/read of the seven template resources (class-source, class-source-json, artifact-file, find-mapping, find-member-mapping, class-members, artifact-metadata): 1 minute. */
|
|
50
|
+
export const RESOURCE_READ_TTL_MS = 60_000;
|
|
51
|
+
/** ProblemDetails resource reads: never cache, regardless of the class row. */
|
|
52
|
+
export const PROBLEM_DETAILS_READ_TTL_MS = 0;
|
|
53
|
+
/** Per-operation hints handed to the McpServer constructor (`cacheHints`). */
|
|
54
|
+
export const RESOURCE_LISTS_CACHE_HINT = Object.freeze({
|
|
55
|
+
ttlMs: RESOURCE_LISTS_TTL_MS,
|
|
56
|
+
cacheScope: APP_CACHE_SCOPE
|
|
57
|
+
});
|
|
58
|
+
/** Per-registration hint for the mc://versions/list fixed resource. */
|
|
59
|
+
export const VERSIONS_LIST_READ_CACHE_HINT = Object.freeze({
|
|
60
|
+
ttlMs: VERSIONS_LIST_READ_TTL_MS,
|
|
61
|
+
cacheScope: APP_CACHE_SCOPE
|
|
62
|
+
});
|
|
63
|
+
/** Per-registration hint for the mc://metrics fixed resource (explicit default). */
|
|
64
|
+
export const METRICS_READ_CACHE_HINT = Object.freeze({
|
|
65
|
+
ttlMs: METRICS_READ_TTL_MS,
|
|
66
|
+
cacheScope: APP_CACHE_SCOPE
|
|
67
|
+
});
|
|
68
|
+
/** Per-registration hint for every template resource. */
|
|
69
|
+
export const RESOURCE_READ_CACHE_HINT = Object.freeze({
|
|
70
|
+
ttlMs: RESOURCE_READ_TTL_MS,
|
|
71
|
+
cacheScope: APP_CACHE_SCOPE
|
|
72
|
+
});
|
|
73
|
+
/**
|
|
74
|
+
* Handler-returned result fields for the ProblemDetails-read override
|
|
75
|
+
* (precedence rank 1 at the SDK encode seam — beats the class row's
|
|
76
|
+
* configured hint). Spread onto the errorResource result ONLY on modern-era
|
|
77
|
+
* requests; see errorResource() for the era gate.
|
|
78
|
+
*/
|
|
79
|
+
export const PROBLEM_DETAILS_READ_CACHE_FIELDS = Object.freeze({
|
|
80
|
+
ttlMs: PROBLEM_DETAILS_READ_TTL_MS,
|
|
81
|
+
cacheScope: APP_CACHE_SCOPE
|
|
82
|
+
});
|
|
83
|
+
//# sourceMappingURL=cache-policy.js.map
|
package/dist/cache-registry.js
CHANGED
|
@@ -4,7 +4,7 @@ import { join, resolve } from "node:path";
|
|
|
4
4
|
import { mapWithConcurrencyLimit } from "./concurrency.js";
|
|
5
5
|
import { createError, ERROR_CODES } from "./errors.js";
|
|
6
6
|
import { normalizeOptionalPathForHost } from "./path-converter.js";
|
|
7
|
-
import
|
|
7
|
+
import { openDatabase } from "./storage/db.js";
|
|
8
8
|
import { getProcessWorkspaceContextCache } from "./workspace-context-cache.js";
|
|
9
9
|
export const PUBLIC_CACHE_KINDS = [
|
|
10
10
|
"artifact-index",
|
|
@@ -222,11 +222,11 @@ function prepareSelector(selector, runtimeInfo) {
|
|
|
222
222
|
normalizedProjectPath: normalizePathKey(selector.projectPath, runtimeInfo)
|
|
223
223
|
};
|
|
224
224
|
}
|
|
225
|
-
function openDb(
|
|
226
|
-
if (!existsSync(sqlitePath)) {
|
|
225
|
+
function openDb(config) {
|
|
226
|
+
if (!existsSync(config.sqlitePath)) {
|
|
227
227
|
return undefined;
|
|
228
228
|
}
|
|
229
|
-
return
|
|
229
|
+
return openDatabase(config).db;
|
|
230
230
|
}
|
|
231
231
|
function candidatePathsForEntry(entry) {
|
|
232
232
|
const paths = new Set();
|
|
@@ -388,7 +388,7 @@ function matchesSelector(entry, selector, runtimeInfo) {
|
|
|
388
388
|
return true;
|
|
389
389
|
}
|
|
390
390
|
async function artifactIndexEntries(config) {
|
|
391
|
-
const db = openDb(config
|
|
391
|
+
const db = openDb(config);
|
|
392
392
|
if (!db) {
|
|
393
393
|
return [];
|
|
394
394
|
}
|
|
@@ -650,7 +650,7 @@ export function createCacheRegistry(config) {
|
|
|
650
650
|
const entries = await collectEntries(input.cacheKinds, input.selector);
|
|
651
651
|
const selectedBytes = entries.reduce((total, entry) => total + entry.sizeBytes, 0);
|
|
652
652
|
if (input.executionMode === "apply") {
|
|
653
|
-
const db = openDb(config
|
|
653
|
+
const db = openDb(config);
|
|
654
654
|
try {
|
|
655
655
|
for (const entry of entries) {
|
|
656
656
|
if (entry.cacheKind === "artifact-index") {
|
package/dist/cli.js
CHANGED
|
@@ -6,18 +6,89 @@ import { log } from "./logger.js";
|
|
|
6
6
|
import { STDIO_WORKER_MODE_ENV, StdioSupervisor } from "./stdio-supervisor.js";
|
|
7
7
|
const CHILD_PID_FILE_ENV = "MCP_SUPERVISOR_CHILD_PID_FILE";
|
|
8
8
|
const WORKER_READY_MARKER = "__MCP_STDIO_WORKER_READY__";
|
|
9
|
+
const PARENT_LIVENESS_POLL_MS = 5_000;
|
|
9
10
|
async function main() {
|
|
10
11
|
if (process.env[STDIO_WORKER_MODE_ENV] === "1") {
|
|
11
12
|
const pidFile = process.env[CHILD_PID_FILE_ENV];
|
|
12
13
|
if (pidFile) {
|
|
13
14
|
writeFileSync(pidFile, `${process.pid}\n`, "utf8");
|
|
14
15
|
}
|
|
15
|
-
// Worker mode runs behind the stdio supervisor
|
|
16
|
-
//
|
|
16
|
+
// Worker mode runs behind the stdio supervisor, which holds the SOLE write
|
|
17
|
+
// end of this stdin — so the supervisor's death is delivered here as EOF.
|
|
18
|
+
// This interval keeps the process alive until then; releasing it only from
|
|
19
|
+
// a process "exit" listener (which by definition runs once the process is
|
|
20
|
+
// already leaving) meant the event loop never drained and the worker
|
|
21
|
+
// outlived every supervisor that did not shut down cooperatively.
|
|
17
22
|
const keepAliveTimer = setInterval(() => undefined, 60_000);
|
|
18
|
-
|
|
23
|
+
// Declared ahead of both stand-down routes below, which each read it.
|
|
24
|
+
let stdinEnded = false;
|
|
25
|
+
// Defence in depth for hosts where stdin is not a pipe the parent solely
|
|
26
|
+
// owns, so EOF never arrives. `process.ppid` is a static data property in
|
|
27
|
+
// node — re-reading it can never reveal reparenting — so the parent pid is
|
|
28
|
+
// snapshotted once and probed for LIVENESS instead. The probe errs towards
|
|
29
|
+
// "alive" (EPERM means the pid was reused by another user's process), so it
|
|
30
|
+
// can make a dead parent look alive but never the reverse.
|
|
31
|
+
const parentPid = process.ppid;
|
|
32
|
+
const parentLivenessTimer = setInterval(() => {
|
|
33
|
+
try {
|
|
34
|
+
process.kill(parentPid, 0);
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
if (err.code === "EPERM") {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
clearInterval(parentLivenessTimer);
|
|
41
|
+
clearInterval(keepAliveTimer);
|
|
42
|
+
// Named on stderr because this stand-down has no other visible cause:
|
|
43
|
+
// the supervisor is already gone, nothing arrived on stdin, and the
|
|
44
|
+
// worker simply stops. `stdinEnded` records which of the two
|
|
45
|
+
// stand-down routes actually fired.
|
|
46
|
+
log("warn", "worker.parent_liveness_lost", {
|
|
47
|
+
parentPid,
|
|
48
|
+
stdinEnded: stdinEnded || process.stdin.readableEnded
|
|
49
|
+
});
|
|
50
|
+
// This arm is reached precisely BECAUSE stdin never ended, so the
|
|
51
|
+
// still-flowing stdin handle would keep the event loop referenced on
|
|
52
|
+
// its own. Nothing more can ever arrive on it — the writer is gone —
|
|
53
|
+
// so release it, guarding unref() for a file-backed stdin exactly as
|
|
54
|
+
// the supervisor's own shutdown does.
|
|
55
|
+
process.stdin.pause();
|
|
56
|
+
const unrefStdin = process.stdin.unref;
|
|
57
|
+
if (typeof unrefStdin === "function") {
|
|
58
|
+
unrefStdin.call(process.stdin);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}, PARENT_LIVENESS_POLL_MS);
|
|
62
|
+
parentLivenessTimer.unref();
|
|
63
|
+
// No process.exit() here: src/compat-stdio-transport.ts deliberately keeps
|
|
64
|
+
// writing after a half-close so in-flight responses are still delivered,
|
|
65
|
+
// and an immediate exit would truncate one. Releasing the timers lets the
|
|
66
|
+
// event loop drain once the remaining work is done.
|
|
67
|
+
const releaseKeepAlive = () => {
|
|
68
|
+
clearInterval(keepAliveTimer);
|
|
69
|
+
clearInterval(parentLivenessTimer);
|
|
70
|
+
};
|
|
71
|
+
let workerReady = false;
|
|
72
|
+
const handleStdinEnd = () => {
|
|
73
|
+
stdinEnded = true;
|
|
74
|
+
if (workerReady) {
|
|
75
|
+
releaseKeepAlive();
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
// Registered BEFORE startServer: "end" fires once, and a listener attached
|
|
79
|
+
// afterwards can miss it entirely on a host whose pipe closes immediately.
|
|
80
|
+
// Attaching a listener does not itself start the flow, so this is inert
|
|
81
|
+
// until the transport resumes stdin.
|
|
82
|
+
process.stdin.on("end", handleStdinEnd);
|
|
83
|
+
process.stdin.on("close", handleStdinEnd);
|
|
19
84
|
await startServer();
|
|
20
85
|
process.stderr.write(`${WORKER_READY_MARKER}\n`);
|
|
86
|
+
workerReady = true;
|
|
87
|
+
// An EOF observed during startup must not be lost: act on it now that the
|
|
88
|
+
// worker is up, rather than idling forever on a stream that already ended.
|
|
89
|
+
if (stdinEnded || process.stdin.readableEnded) {
|
|
90
|
+
releaseKeepAlive();
|
|
91
|
+
}
|
|
21
92
|
return;
|
|
22
93
|
}
|
|
23
94
|
const supervisor = new StdioSupervisor({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { JSONRPCMessage } from "@modelcontextprotocol/
|
|
1
|
+
import type { JSONRPCMessage } from "@modelcontextprotocol/server";
|
|
2
2
|
type StdioReadable = NodeJS.ReadStream;
|
|
3
3
|
type StdioWritable = NodeJS.WriteStream;
|
|
4
4
|
export declare class CompatStdioServerTransport {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
|
-
import { JsonRpcFrameReader, encodeJsonRpcMessage } from "./json-rpc-framing.js";
|
|
2
|
+
import { JsonRpcFrameReader, encodeJsonRpcMessage, isJsonRpcFramingFatalError } from "./json-rpc-framing.js";
|
|
3
3
|
function asError(value) {
|
|
4
4
|
return value instanceof Error ? value : new Error(String(value));
|
|
5
5
|
}
|
|
@@ -28,6 +28,10 @@ export class CompatStdioServerTransport {
|
|
|
28
28
|
this.stdin.resume();
|
|
29
29
|
}
|
|
30
30
|
async send(message) {
|
|
31
|
+
// No closed guard on purpose: a peer may half-close stdin (which flips
|
|
32
|
+
// `closed` via the end/close listeners) while stdout is still writable,
|
|
33
|
+
// and in-flight responses must still be delivered — the pre-migration
|
|
34
|
+
// transport wrote unconditionally and that behavior is preserved.
|
|
31
35
|
const frame = encodeJsonRpcMessage(message, this.frameReader.currentMode === "content-length" ? "content-length" : "line");
|
|
32
36
|
await new Promise((resolve) => {
|
|
33
37
|
if (this.stdout.write(frame)) {
|
|
@@ -55,6 +59,14 @@ export class CompatStdioServerTransport {
|
|
|
55
59
|
},
|
|
56
60
|
onError: (error) => {
|
|
57
61
|
this.onerror?.(error);
|
|
62
|
+
if (isJsonRpcFramingFatalError(error)) {
|
|
63
|
+
// The reader has stopped: it can no longer tell where a frame begins
|
|
64
|
+
// and refuses all further input. Leaving the transport attached
|
|
65
|
+
// would make it silently deaf, so the session ends here — the
|
|
66
|
+
// framing invariant's "terminate with a diagnostic" arm (the
|
|
67
|
+
// diagnostic already went out through onerror).
|
|
68
|
+
void this.close();
|
|
69
|
+
}
|
|
58
70
|
}
|
|
59
71
|
});
|
|
60
72
|
};
|
package/dist/config.d.ts
CHANGED
|
@@ -20,8 +20,11 @@ declare const DEFAULTS: {
|
|
|
20
20
|
readonly maxNbtInputBytes: number;
|
|
21
21
|
readonly maxNbtInflatedBytes: number;
|
|
22
22
|
readonly maxNbtResponseBytes: number;
|
|
23
|
+
readonly sqliteCacheKb: 8000;
|
|
24
|
+
readonly sqliteMmapSize: 268435456;
|
|
23
25
|
readonly remapTimeoutMs: 600000;
|
|
24
26
|
readonly remapMaxMemoryMb: 4096;
|
|
27
|
+
readonly decompileMaxMemoryMb: 4096;
|
|
25
28
|
};
|
|
26
29
|
export declare function loadConfig(): Config;
|
|
27
30
|
export declare function stableArtifactId(parts: string[]): string;
|
package/dist/config.js
CHANGED
|
@@ -28,8 +28,11 @@ const DEFAULTS = {
|
|
|
28
28
|
maxNbtInputBytes: 4 * 1024 * 1024,
|
|
29
29
|
maxNbtInflatedBytes: 16 * 1024 * 1024,
|
|
30
30
|
maxNbtResponseBytes: 8 * 1024 * 1024,
|
|
31
|
+
sqliteCacheKb: 8_000,
|
|
32
|
+
sqliteMmapSize: 268_435_456,
|
|
31
33
|
remapTimeoutMs: 600_000,
|
|
32
|
-
remapMaxMemoryMb: 4096
|
|
34
|
+
remapMaxMemoryMb: 4096,
|
|
35
|
+
decompileMaxMemoryMb: 4096
|
|
33
36
|
};
|
|
34
37
|
const MAX_RETRIES_LOWER_BOUND = 0;
|
|
35
38
|
const MAX_RETRIES_UPPER_BOUND = 20;
|
|
@@ -174,9 +177,12 @@ export function loadConfig() {
|
|
|
174
177
|
maxNbtInputBytes: parseNumber(process.env.MCP_MAX_NBT_INPUT_BYTES, DEFAULTS.maxNbtInputBytes, MAX_BYTES_LOWER_BOUND, Number.MAX_SAFE_INTEGER),
|
|
175
178
|
maxNbtInflatedBytes: parseNumber(process.env.MCP_MAX_NBT_INFLATED_BYTES, DEFAULTS.maxNbtInflatedBytes, MAX_BYTES_LOWER_BOUND, Number.MAX_SAFE_INTEGER),
|
|
176
179
|
maxNbtResponseBytes: parseNumber(process.env.MCP_MAX_NBT_RESPONSE_BYTES, DEFAULTS.maxNbtResponseBytes, MAX_BYTES_LOWER_BOUND, Number.MAX_SAFE_INTEGER),
|
|
180
|
+
sqliteCacheKb: parseNumber(process.env.MCP_SQLITE_CACHE_KB, DEFAULTS.sqliteCacheKb, 1, Number.MAX_SAFE_INTEGER),
|
|
181
|
+
sqliteMmapSize: parseNumber(process.env.MCP_SQLITE_MMAP_SIZE, DEFAULTS.sqliteMmapSize, 0, Number.MAX_SAFE_INTEGER),
|
|
177
182
|
tinyRemapperJarPath: parseOptionalJarPath(process.env.MCP_TINY_REMAPPER_JAR_PATH, "MCP_TINY_REMAPPER_JAR_PATH"),
|
|
178
183
|
remapTimeoutMs: parseNumber(process.env.MCP_REMAP_TIMEOUT_MS, DEFAULTS.remapTimeoutMs, TIMEOUT_LOWER_BOUND_MS, Number.MAX_SAFE_INTEGER),
|
|
179
|
-
remapMaxMemoryMb: parseNumber(process.env.MCP_REMAP_MAX_MEMORY_MB, DEFAULTS.remapMaxMemoryMb, 64, Number.MAX_SAFE_INTEGER)
|
|
184
|
+
remapMaxMemoryMb: parseNumber(process.env.MCP_REMAP_MAX_MEMORY_MB, DEFAULTS.remapMaxMemoryMb, 64, Number.MAX_SAFE_INTEGER),
|
|
185
|
+
decompileMaxMemoryMb: parseNumber(process.env.MCP_DECOMPILE_MAX_MEMORY_MB, DEFAULTS.decompileMaxMemoryMb, 64, Number.MAX_SAFE_INTEGER)
|
|
180
186
|
};
|
|
181
187
|
}
|
|
182
188
|
export function stableArtifactId(parts) {
|
|
@@ -4,9 +4,10 @@ import { createHash } from "node:crypto";
|
|
|
4
4
|
import { basename, join, relative, sep } from "node:path";
|
|
5
5
|
import { mapWithConcurrencyLimit } from "../concurrency.js";
|
|
6
6
|
import { createError, ERROR_CODES, isAppError } from "../errors.js";
|
|
7
|
-
import {
|
|
7
|
+
import { javaRunner } from "../java-process.js";
|
|
8
8
|
import { log } from "../logger.js";
|
|
9
9
|
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
10
|
+
const DEFAULT_MAX_MEMORY_MB = 4_096;
|
|
10
11
|
const DECOMPILED_JAVA_READ_CONCURRENCY = 8;
|
|
11
12
|
const DECOMPILE_COMPLETE_MARKER = ".decompile-complete";
|
|
12
13
|
const VINEFLOWER_FLAG_PROFILES = [
|
|
@@ -93,11 +94,12 @@ function clearOutputDir(dir) {
|
|
|
93
94
|
rmSync(dir, { recursive: true, force: true });
|
|
94
95
|
mkdirSync(dir, { recursive: true });
|
|
95
96
|
}
|
|
96
|
-
async function runVineflower(vineflowerJarPath, binaryJarPath, outputDir, timeoutMs, flags = VINEFLOWER_FLAG_PROFILES[0].flags) {
|
|
97
|
-
const result = await
|
|
97
|
+
async function runVineflower(vineflowerJarPath, binaryJarPath, outputDir, timeoutMs, maxMemoryMb, flags = VINEFLOWER_FLAG_PROFILES[0].flags) {
|
|
98
|
+
const result = await javaRunner.run({
|
|
98
99
|
jarPath: vineflowerJarPath,
|
|
99
100
|
args: [...flags, binaryJarPath, outputDir],
|
|
100
101
|
timeoutMs,
|
|
102
|
+
maxMemoryMb,
|
|
101
103
|
normalizePathArgs: true
|
|
102
104
|
});
|
|
103
105
|
if (result.exitCode !== 0) {
|
|
@@ -128,6 +130,7 @@ export async function decompileBinaryJar(binaryJarPath, cacheDir, options) {
|
|
|
128
130
|
artifactIdCandidate: options.artifactIdCandidate
|
|
129
131
|
});
|
|
130
132
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
133
|
+
const maxMemoryMb = options.maxMemoryMb ?? DEFAULT_MAX_MEMORY_MB;
|
|
131
134
|
const signature = options.signature ?? basename(normalizedBinaryJarPath);
|
|
132
135
|
const outputDir = decompileOutputDir(cacheDir, normalizedBinaryJarPath, signature).replace(/[/\\]$/, "");
|
|
133
136
|
try {
|
|
@@ -159,7 +162,7 @@ export async function decompileBinaryJar(binaryJarPath, cacheDir, options) {
|
|
|
159
162
|
}
|
|
160
163
|
clearOutputDir(outputDir);
|
|
161
164
|
await assertVineflowerAvailable(options.vineflowerJarPath);
|
|
162
|
-
await
|
|
165
|
+
await javaRunner.assertAvailable();
|
|
163
166
|
const profilesAttempted = [];
|
|
164
167
|
let lastDecompileError;
|
|
165
168
|
for (const profile of VINEFLOWER_FLAG_PROFILES) {
|
|
@@ -173,7 +176,7 @@ export async function decompileBinaryJar(binaryJarPath, cacheDir, options) {
|
|
|
173
176
|
attempt: profilesAttempted.length
|
|
174
177
|
});
|
|
175
178
|
}
|
|
176
|
-
await runVineflower(options.vineflowerJarPath, normalizedBinaryJarPath, outputDir, timeoutMs, profile.flags);
|
|
179
|
+
await runVineflower(options.vineflowerJarPath, normalizedBinaryJarPath, outputDir, timeoutMs, maxMemoryMb, profile.flags);
|
|
177
180
|
const javaFileNames = await collectJavaFiles(outputDir);
|
|
178
181
|
if (javaFileNames.length === 0) {
|
|
179
182
|
throw createError({
|