@adhisang/minecraft-modding-mcp 4.2.1 → 6.0.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 (144) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +5 -4
  3. package/dist/access-transformer-parser.d.ts +2 -1
  4. package/dist/access-transformer-parser.js +10 -1
  5. package/dist/access-widener-parser.d.ts +1 -0
  6. package/dist/access-widener-parser.js +6 -3
  7. package/dist/build-suggested-call.js +31 -10
  8. package/dist/cache-registry.d.ts +8 -0
  9. package/dist/cache-registry.js +50 -32
  10. package/dist/config.d.ts +1 -0
  11. package/dist/config.js +4 -0
  12. package/dist/decompiler/vineflower.js +8 -2
  13. package/dist/entry-tools/analyze-mod-service.d.ts +24 -24
  14. package/dist/entry-tools/analyze-mod-service.js +2 -1
  15. package/dist/entry-tools/analyze-symbol-service.d.ts +57 -22
  16. package/dist/entry-tools/analyze-symbol-service.js +139 -34
  17. package/dist/entry-tools/batch-class-members-service.d.ts +3 -1
  18. package/dist/entry-tools/batch-class-members-service.js +6 -6
  19. package/dist/entry-tools/batch-class-source-service.d.ts +3 -1
  20. package/dist/entry-tools/batch-class-source-service.js +5 -6
  21. package/dist/entry-tools/batch-mappings-service.d.ts +3 -1
  22. package/dist/entry-tools/batch-mappings-service.js +4 -5
  23. package/dist/entry-tools/batch-symbol-exists-service.d.ts +3 -1
  24. package/dist/entry-tools/batch-symbol-exists-service.js +4 -5
  25. package/dist/entry-tools/compare-minecraft-service.d.ts +10 -10
  26. package/dist/entry-tools/compare-minecraft-service.js +1 -1
  27. package/dist/entry-tools/entry-tool-schema.d.ts +1 -1
  28. package/dist/entry-tools/inspect-minecraft/handlers/artifact.js +1 -3
  29. package/dist/entry-tools/inspect-minecraft/handlers/class-members.js +4 -3
  30. package/dist/entry-tools/inspect-minecraft/handlers/class-overview.js +0 -5
  31. package/dist/entry-tools/inspect-minecraft/handlers/class-source.js +0 -1
  32. package/dist/entry-tools/inspect-minecraft/handlers/file.js +26 -2
  33. package/dist/entry-tools/inspect-minecraft/handlers/list-files.js +0 -1
  34. package/dist/entry-tools/inspect-minecraft/handlers/search.d.ts +8 -0
  35. package/dist/entry-tools/inspect-minecraft/handlers/search.js +13 -2
  36. package/dist/entry-tools/inspect-minecraft/handlers/versions.d.ts +1 -1
  37. package/dist/entry-tools/inspect-minecraft/handlers/versions.js +3 -2
  38. package/dist/entry-tools/inspect-minecraft/internal.d.ts +90 -89
  39. package/dist/entry-tools/inspect-minecraft/internal.js +32 -14
  40. package/dist/entry-tools/inspect-minecraft-service.d.ts +218 -218
  41. package/dist/entry-tools/inspect-minecraft-service.js +1 -1
  42. package/dist/entry-tools/manage-cache-service.d.ts +4 -4
  43. package/dist/entry-tools/response-contract.d.ts +3 -1
  44. package/dist/entry-tools/response-contract.js +3 -2
  45. package/dist/entry-tools/validate-project/cases/mixin.js +2 -1
  46. package/dist/entry-tools/validate-project/cases/project-summary.js +5 -3
  47. package/dist/entry-tools/validate-project-service.d.ts +42 -36
  48. package/dist/entry-tools/validate-project-service.js +3 -1
  49. package/dist/entry-tools/verify-mixin-target-service.d.ts +30 -0
  50. package/dist/entry-tools/verify-mixin-target-service.js +120 -29
  51. package/dist/error-mapping.d.ts +51 -0
  52. package/dist/error-mapping.js +157 -2
  53. package/dist/index.js +147 -76
  54. package/dist/java-process.js +1 -1
  55. package/dist/json-rpc-framing.d.ts +4 -0
  56. package/dist/json-rpc-framing.js +23 -1
  57. package/dist/mapping/internal-types.d.ts +24 -0
  58. package/dist/mapping/loaders/tiny-maven.js +46 -20
  59. package/dist/mapping/lookup.d.ts +13 -2
  60. package/dist/mapping/lookup.js +60 -2
  61. package/dist/mapping/types.d.ts +6 -0
  62. package/dist/mapping-service.d.ts +26 -2
  63. package/dist/mapping-service.js +212 -55
  64. package/dist/mcp-helpers.d.ts +1 -0
  65. package/dist/mcp-helpers.js +25 -3
  66. package/dist/minecraft-explorer-service.js +24 -11
  67. package/dist/mixin/access-validators.js +33 -1
  68. package/dist/mixin/annotation-validators.js +73 -9
  69. package/dist/mixin/parsed-validator.js +3 -2
  70. package/dist/mixin-parser.js +119 -7
  71. package/dist/mod-decompile-service.d.ts +2 -0
  72. package/dist/mod-decompile-service.js +21 -3
  73. package/dist/mod-remap-service.d.ts +20 -1
  74. package/dist/mod-remap-service.js +101 -45
  75. package/dist/mojang-tiny-mapping-service.js +5 -2
  76. package/dist/nbt/java-nbt-codec.js +46 -16
  77. package/dist/nbt/json-patch.js +51 -12
  78. package/dist/nbt/typed-json.d.ts +1 -0
  79. package/dist/nbt/typed-json.js +12 -2
  80. package/dist/nbt/types.d.ts +2 -2
  81. package/dist/observability.d.ts +6 -0
  82. package/dist/observability.js +10 -0
  83. package/dist/page-cursor.d.ts +17 -0
  84. package/dist/page-cursor.js +0 -0
  85. package/dist/path-converter.js +10 -3
  86. package/dist/registry-service.d.ts +2 -0
  87. package/dist/registry-service.js +41 -2
  88. package/dist/repo-downloader.js +4 -3
  89. package/dist/resources.js +45 -9
  90. package/dist/response-utils.d.ts +29 -4
  91. package/dist/response-utils.js +95 -5
  92. package/dist/source/access-validate.js +12 -2
  93. package/dist/source/artifact-resolver.js +2 -2
  94. package/dist/source/class-source/members-builder.d.ts +44 -1
  95. package/dist/source/class-source/members-builder.js +51 -4
  96. package/dist/source/class-source/snippet-builder.d.ts +13 -0
  97. package/dist/source/class-source/snippet-builder.js +54 -4
  98. package/dist/source/class-source-helpers.d.ts +4 -4
  99. package/dist/source/class-source-helpers.js +77 -20
  100. package/dist/source/class-source.d.ts +19 -0
  101. package/dist/source/class-source.js +144 -40
  102. package/dist/source/file-access.js +3 -2
  103. package/dist/source/indexer.js +29 -9
  104. package/dist/source/lifecycle/mapping-helpers.js +28 -3
  105. package/dist/source/lifecycle/runtime-check.js +20 -6
  106. package/dist/source/search.d.ts +1 -1
  107. package/dist/source/search.js +117 -30
  108. package/dist/source/validate-mixin/pipeline/resolve.js +23 -3
  109. package/dist/source/validate-mixin.d.ts +2 -1
  110. package/dist/source/validate-mixin.js +45 -4
  111. package/dist/source/workspace-target.js +17 -10
  112. package/dist/source-jar-reader.d.ts +54 -1
  113. package/dist/source-jar-reader.js +121 -10
  114. package/dist/source-resolver.js +2 -2
  115. package/dist/source-service.d.ts +28 -7
  116. package/dist/source-service.js +9 -1
  117. package/dist/stdio-supervisor.d.ts +2 -2
  118. package/dist/stdio-supervisor.js +29 -9
  119. package/dist/storage/db.js +24 -1
  120. package/dist/storage/files-repo.d.ts +15 -0
  121. package/dist/storage/files-repo.js +67 -37
  122. package/dist/storage/symbols-repo.d.ts +15 -0
  123. package/dist/storage/symbols-repo.js +53 -19
  124. package/dist/symbols/symbol-extractor.js +35 -6
  125. package/dist/text-truncate.d.ts +7 -0
  126. package/dist/text-truncate.js +21 -0
  127. package/dist/tiny-remapper-resolver.js +10 -0
  128. package/dist/tool-contract-manifest.js +2 -2
  129. package/dist/tool-execution-gate.js +12 -1
  130. package/dist/tool-guidance.d.ts +6 -2
  131. package/dist/tool-guidance.js +63 -13
  132. package/dist/tool-schemas.d.ts +561 -496
  133. package/dist/tool-schemas.js +175 -139
  134. package/dist/types.d.ts +1 -0
  135. package/dist/version-diff-service.js +5 -0
  136. package/dist/version-service.d.ts +3 -0
  137. package/dist/version-service.js +18 -2
  138. package/dist/warning-details.d.ts +30 -0
  139. package/dist/warning-details.js +80 -0
  140. package/dist/workspace-mapping-service.js +9 -0
  141. package/docs/README-ja.md +2 -2
  142. package/docs/examples.md +26 -2
  143. package/docs/tool-reference.md +22 -17
  144. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,75 @@ 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
+ ## [6.0.0] - 2026-06-25
11
+
12
+ ### Changed
13
+
14
+ - **Breaking:** tool responses omit `meta.warnings` entirely when there are no warnings (previously always emitted as `[]`), and omit `meta.detailApplied` when it equals the tool's default detail level (entry tools: `summary`; expert/batch tools: their per-tool default). Clients must treat a missing `meta.warnings` as "no warnings" and a missing `meta.detailApplied` as "the default detail was applied". `meta.includeApplied` semantics are unchanged (already omitted when empty).
15
+ - `tools/list` payload slimmed by ~12% (93.2 KB → 82.2 KB, roughly 2,700 fewer tokens held in an agent's context): parameter descriptions that merely echoed enum values (e.g. `"obfuscated | mojang | intermediary | yarn"`) are removed since the JSON Schema `enum` already carries them, the shared `detail`/`include`/`target`/`scope`/`gradleUserHome` descriptions are tightened, the expert-tool steering note is shortened, and the longest tool descriptions (`resolve-method-mapping-exact`, `get-class-source`, `get-class-members`, the `batch-*` family) no longer repeat information already present in their input schemas. Wording-only — no input shapes, defaults, or validation rules changed.
16
+ - Per-member remap-failure warnings are aggregated into a single line per namespace direction (e.g. `Could not remap 12 methods from obfuscated to mojang (foo, bar, baz, +9 more).`) instead of one line per member, and the structured `meta.warningDetails[]` companion is capped to a representative set (5 entries) at `detail: "summary"` (the full text still lives in `meta.warnings[]`, so each kept entry's `index` still dereferences it). On unobfuscated versions a single core class could previously emit hundreds of near-identical remap-warning lines — the dominant token sink for those responses.
17
+
18
+ ### Fixed
19
+
20
+ - `validate-mixin` / mixin parsing: `@Mixin(My$GeneratedClass.class)` class-literal targets containing `$` (a legal Java identifier character) are now captured as the full class name; previously the target regex started the capture mid-name and produced a wrong target.
21
+ - **Unobfuscated Minecraft versions (26.1+):** `get-class-members` / `get-class-source` no longer mislabel the already-deobfuscated runtime namespace as `obfuscated` for a `mapping: "mojang"` request when reusing a previously-resolved artifact. The mislabel made `requestedMapping ("mojang") !== mappingApplied ("obfuscated")`, which triggered a doomed mojang↔obfuscated remap that dropped every member (forcing a spurious `decompiledFallback`), disabled `memberPattern`, and flooded the response with per-member remap warnings. The namespace is now reconciled to `mojang` (identity), so members come back from bytecode with correct names, `memberPattern` applies, and the warning flood disappears.
22
+ - **`batch-symbol-exists` / `check-symbol-exists` on unobfuscated versions:** method existence checks returned `not_found` for methods that genuinely exist, because the empty mapping graph reported `not_found` and skipped the runtime-bytecode fallback. The fallback now runs for `not_found` method queries; name-only lookups resolve when any overload matches (no longer reported as `ambiguous` for an existence check), and inherited methods are included (`level.getGameTime()` resolves on `Level`).
23
+ - `check-symbol-exists`: a class genuinely absent from the runtime jar now yields a clear `… was not found in the Minecraft <version> runtime jar` message instead of a generic load-failure note.
24
+ - `resolve-artifact`: a decompilation failure (`ERR_DECOMPILER_FAILED`) no longer advertises an `artifactId` for an artifact that was never persisted — the leaked id previously caused a follow-up `find-class` / `get-class-*` to fail with "Artifact not found. Resolve context first."
25
+ - Workspace dependency version detection falls back to the umbrella package's version property (e.g. `fabricApiVersion` / `fabric_api_version`) for submodules that don't declare their own (e.g. `net.fabricmc.fabric-api:fabric-screen-handler-api-v1`), instead of failing with `ERR_DEPENDENCY_VERSION_UNRESOLVED`. The submodule's own property still takes precedence when present.
26
+ - `get-class-source` now honors an explicit `mapping` argument instead of forcing the artifact's stored namespace (matching `get-class-members`), so a `className` supplied in a declared namespace no longer fails with `CLASS_NOT_FOUND`. Several recovery `suggestedCall`s are corrected: the snippet continuation no longer drops the default `maxLines` (which produced an unbounded follow-up read), the `decompiledFallback` truncation flag is reported accurately, and a hardcoded `"latest"` version that `resolve-artifact` rejects is replaced with the concrete resolved version. A method query with an omitted `signatureMode` is treated as a non-exact match in the runtime check (descriptorless methods are no longer guaranteed `not_found`), binary jars are remapped from the source jar (not the absent output) after an index loss, the `globToSqlLike` `**` push-down no longer drops direct children, the vanilla-jar fallback no longer retains the named-signature mapping, and the workspace-context write-back is serialized so it stops racing dependency-target synthesis.
27
+ - `resolve-method-mapping-exact` mirrors `find-mapping`'s obfuscated-namespace acceptance set, so a single-hop `intermediary → yarn` lookup whose candidate carries an obfuscated-namespace descriptor no longer returns a false `not_found`. Loading yarn Tiny pairs from Maven no longer unions three builds (or fetches unusable v1 jars), removing the spurious `ambiguous` results that produced. Tiny caches (`extractTinyFromJar` / `resolveMojangTinyFile`) are now written atomically (temp file + `rename`) so a crash mid-write cannot leave a truncated `.tiny` behind a permanent `existsSync` fast path, and `get-class-api-matrix` keys its cursor context on `gradleUserHome` so a cursor minted against one dependency graph cannot replay against another.
28
+ - `validate-mixin` / mixin parsing no longer matches the word `class` inside comments or javadoc (which yielded a wrong `className`) and no longer parses annotations inside block comments (which produced phantom `@Shadow` / `@Accessor` members and false definite errors); both passes now skip comment regions. The injection-point wildcard quantifier is anchored so an exact-match target no longer triggers a false positive.
29
+ - `search-class-source` / file search no longer re-emits `both`-tier hits as duplicate `content`-tier hits on later pages (they are deduped across the cursor), escapes the `LIKE` needle in path search, push-down, and count queries so the declared `ESCAPE` clause actually applies, and stops returning a `nextCursor` on the final partial page (which previously forced an extra empty-page fetch).
30
+ - `inspect-minecraft`: `task: "versions"` no longer drops the entire `versions` block at the default detail level, and `task: "file"` content is reachable now that `file` is a valid `include` group. `task: "api-overview"` honors `maxRows` and flags truncation instead of silently capping the matrix at 25 rows while reporting `rowsTruncated: false`. The `file` / `search` / `list-files` tasks return the friendly "blocked" summary (carrying the version-inference warning) instead of forwarding an empty `artifactId`, `task: "workspace"` no longer passes the literal `"unknown"` as the Minecraft version, the `get-class-members` truncation `nextAction` raises the member limit so the suggested retry no longer reproduces the same cut, and the `remap-mod-jar` preview `nextAction` preserves an explicit `outputJar`.
31
+ - stdio supervisor: a supervisor-replayed `initialize` no longer emits a stray error response when the worker crashes again after the client is already initialized; a worker spawn error that previously stalled the supervisor permanently now recovers; and a stale worker exit no longer detaches and orphans the replacement worker. The Java subprocess runner drains trailing stdout/stderr before resolving on child exit, so the final output is no longer lost.
32
+ - NBT JSON Patch: an `add` / `replace` op with key `__proto__` is assigned as an own enumerable property instead of silently mutating the prototype and dropping the member. Non-finite floats round-trip correctly — decode emits a sentinel string rather than a raw number, and the typed-JSON layer no longer rejects otherwise-valid NBT containing `NaN` / `Infinity`.
33
+ - Data generation no longer deadlocks once the child's stdout pipe buffer fills (the output is now drained), the download timeout now covers the response body stream instead of being cleared after headers (removing an indefinite hang), and concurrent identical `remap-mod-jar` calls no longer race on a non-atomic cache copy.
34
+ - Access Widener parsing accepts valid `transitive-` kinds — the prefix is stripped, the base kind validated, and transitivity recorded on the entry — instead of rejecting them as unknown. Path conversion recognizes the `wsl.localhost` UNC form (not only `wsl$`) and rejects a UNC WSL path that targets a different distro than the server runs in, instead of silently mapping it.
35
+ - `validate-mixin` recovery suggestions no longer read `input.path` before checking the mode (which turned a project-mode call into an executable path-mode `suggestedCall`), and the access-transformer suggestion no longer hardcodes the access-widener subject kind. The `batch-class-source` `outputFile` dedupe guard canonicalizes paths unconditionally so absolute-path aliases (`/a/../b`, `//a`) collide as intended and cannot race `writeFile` on the same inode.
36
+
37
+ ### Performance
38
+
39
+ - SQLite repositories cache prepared statements for dynamically built queries (cursor-paginated symbol/file lookups, scoped symbol search, count queries, `IN (...)`-list lookups) in a small per-repo LRU instead of re-preparing the SQL on every call.
40
+ - Symbol search with regex/glob/package filters now streams rows from SQLite instead of materializing every symbol of the artifact in memory first; only matching rows are retained.
41
+ - Cache-registry operations no longer read and JSON-parse every registry file from disk on each call; the parsed registry state is cached and invalidated on write.
42
+ - `get-class-members` / `get-class-source` no longer re-strip the whole decompiled file once per nested type when extracting members (`extractDecompiledMembers` was quadratic in the nested-type count).
43
+ - NBT patch validation revalidates only the touched subtree per patch op instead of the whole document (previously quadratic), and the array encoders reuse a single buffer cursor instead of allocating a `Buffer` per scalar write.
44
+ - `check-symbol-exists` indexes its lookups instead of scanning the full target namespace on every call.
45
+
46
+ ## [5.0.0] - 2026-06-04
47
+
48
+ ### Changed
49
+
50
+ - **Breaking:** `get-class-members` now returns at most **150** members per page by default (was 500). The existing `nextCursor` continuation already covers paging; callers that need more in one response can pass an explicit `maxMembers` (still capped at 5000). `maxClassResults` and `maxVersions` defaults are unchanged.
51
+ - **Breaking:** `inspect-minecraft` responses no longer echo the requested subject inside `summary.subject`. The raw requested subject still surfaces exactly once, in the always-on top-level `subject` block as `subject.requested` (paired with `subject.resolved`). Clients reading `summary.subject.requested` must switch to `subject.requested` (or correlate via `requestId`).
52
+ - **Breaking:** `meta.warningDetails[]` entries no longer duplicate the warning text. Each entry now carries `index: number` referencing its string in `meta.warnings[]` (1:1, order-preserving) instead of a `message` field. Clients reading `warningDetails[].message` must switch to `meta.warnings[detail.index]`.
53
+ - **Breaking:** `get-class-source` and `get-class-members` now omit the diagnostic fields `provenance`, `qualityFlags`, and `artifactContents` by default. Pass the new `includeProvenance: true` to include them. (`compact` keeps its existing meaning — strip empty fields, and for `get-class-members` also `context`.) Callers that previously read those fields without setting `compact` must now pass `includeProvenance: true`.
54
+ - **Breaking:** `get-class-members` member objects are slimmed on the wire. `accessFlags` is dropped (the modifiers are already encoded in `javaSignature`), `isSynthetic` is emitted only when `true`, and `ownerFqn` is hoisted to a single block-level `members.ownerFqn` when every member shares one owner (the common, non-`includeInherited` case); it stays per-member when `includeInherited` surfaces multiple owners. `name` and `javaSignature` are unchanged. Read the owner from `members.ownerFqn` (falling back to `member.ownerFqn`), and derive access modifiers from `javaSignature` instead of `accessFlags`. `diff-class-signatures` / `verify-mixin-target` member shapes are unchanged.
55
+ - **Breaking:** `get-class-members` omits `jvmDescriptor` on **field** members by default (the type is already in `javaSignature`). Method/constructor `jvmDescriptor` is always present for overload disambiguation. Pass `includeDescriptors: true` (legacy `get-class-members`) or `include: ["descriptors"]` (`inspect-minecraft` task=`class-members`) to also emit field descriptors.
56
+ - **Breaking:** `analyze-symbol` (and the `inspect-minecraft`-adjacent mapping flows `exists`/`map`/`exact-map`/`workspace`) now reuse the expert-tool candidate projection: when a lookup resolves to a single exact candidate that just duplicates `match`, the `candidates` block is omitted; for unresolved results with more than three candidates the tail is slimmed and `candidateDetailsTruncated: true` is surfaced. Read the resolved symbol from `match` rather than `candidates[0]`.
57
+ - **Breaking:** `validate-mixin` (and `validate-project` task=`mixin`/`project-summary`) now default `reportMode` to `summary-first` (was `full`) — for every caller, not just the MCP wire boundary. The default response hoists shared provenance/warnings/incomplete reasons and omits per-result `resolvedMembers`, `toolHealth`, `structuredWarnings`, `aggregatedWarnings`, and `confidenceBreakdown`; `summary`, `issues`, `issueSummary`, and top-level provenance/toolHealth are retained. Pass `reportMode="full"` or `explain=true` to restore per-result `resolvedMembers`/`toolHealth`/`resolutionTrace`. `explain=true` now also preserves `resolutionTrace` under `reportMode="compact"`.
58
+ - `verify-mixin-target`: `member.descriptor` now accepts empty / whitespace-only strings as "omitted" (normalized to `undefined`), matching every other optional descriptor field on the surface. Previously rejected with `ERR_INVALID_INPUT`. Non-breaking — no change for callers passing a real descriptor.
59
+ - **Breaking:** `get-class-source` and `get-class-members` now take the same `kind`-based `target` shape as `resolve-artifact`: `target: { kind: "version"|"jar"|"coordinate"|"workspace"|"dependency", ... }`, plus `target: { kind: "artifact", artifactId }` to reuse an already-resolved artifact. The old `target: { type: "artifact", artifactId }` form is removed — migrate to `{ kind: "artifact", artifactId }`. The redundant `{ type: "resolve", kind, value }` wrapper is still accepted (the `type` key is now ignored), but `{ kind, value }` is the canonical shape. Published `suggestedCall` targets for these tools now use the `kind`-based shape.
60
+ - **Breaking:** `analyze-symbol` `task="lifecycle"` gains explicit range controls `fromVersion`, `toVersion`, `maxVersions`, `includeTimeline`, and `includeSnapshots` (accepted only for `task="lifecycle"`; supplying them on another task is now rejected instead of silently ignored). The existing `version` field is kept as a back-compat alias for `toVersion` (range end; `toVersion` takes precedence). **Behavior change:** the lifecycle scan default grew from the previous hard-coded **5 versions** to the service default of **120** (clamped at 400) — pass `maxVersions: 5` to restore the old window. A lifecycle request still requires an end anchor (`version` or `toVersion`).
61
+ - `analyze-symbol` `subject.kind="symbol"` is now real selector-based auto-detect (owner+descriptor ⇒ method, owner only ⇒ field, otherwise ⇒ class) with the inferred kind surfaced as a warning and documented via the schema `describe()`. Previously `"symbol"` was silently coerced to `class` (exists/map/workspace), rejected (api-overview), or ignored (exact-map/lifecycle). `summary.subject.kind` now reports the resolved concrete kind instead of echoing `"symbol"`. Non-breaking — the enum keeps all four values and explicit kinds are unaffected.
62
+ - **Breaking:** `check-symbol-exists` now defaults `signatureMode` to `name-only` (was `exact`), matching `find-mapping` so the two sibling lookup tools behave identically. A `kind="method"` query with no descriptor now matches by owner+name (resolving uniquely or reporting ambiguous overloads) instead of failing with `ERR_INVALID_INPUT`; pass `signatureMode="exact"` to require a descriptor. `find-mapping` gains a `nameMode` parameter (default `auto`, mirroring `check-symbol-exists`): `auto` accepts a dotless class name for any `sourceMapping` (previously only `obfuscated`), `fqcn` requires a fully-qualified name. `analyze-symbol`'s own `signatureMode` default is intentionally left at `exact`.
63
+ - **Breaking:** the per-tool `compact` boolean is removed from all expert tools (`resolve-artifact`, `find-mapping`, `resolve-method-mapping-exact`, `resolve-workspace-symbol`, `check-symbol-exists`, `get-class-source`, `get-class-members`, `search-class-source`, `list-artifact-files`) and the four `batch-*` tools, replaced by the entry-tool **`detail`** (`summary` | `standard` | `full`) + **`include[]`** response contract — one response-shaping vocabulary across the whole server. Per-tool defaults keep every default response byte-identical: resolution/mapping tools + batch default `detail: "summary"` (= old `compact: true`); `get-class-source`/`get-class-members`/`search-class-source`/`list-artifact-files` default `detail: "standard"` (= old `compact: false`, post-Phase-4 diagnostic strip). **Migration:** `compact: true` → `detail: "summary"`, `compact: false` → `detail: "full"`. `include[]` opts specific field groups back in (`["provenance"]`, `["candidates"]`, `["samples"]`, `["artifact"]`, `["paths"]`, `["descriptors"]`). The Phase-4 `includeProvenance` / `includeDescriptors` flags are retained as aliases for `include: ["provenance"]` / `include: ["descriptors"]`. Expert and batch responses now also report `meta.detailApplied` / `meta.includeApplied`.
64
+ - `resolve-method-mapping-exact` is now documented as a strict shortcut for `find-mapping` (kind=method, signatureMode=exact): its description, schema, and the README/tool-reference entries explain that it additionally requires a **complete** descriptor projection (returns `mapping_unavailable` when class references cannot all be projected, where `find-mapping`'s exact mode resolves leniently). Documentation-only — no wire, behavior, or input-schema change; the tool is retained.
65
+ - The display caps that were previously clamped silently now emit a clamp warning so the documented contract is truthful: `list-versions` `limit` (capped at 200), `get-class-api-matrix` `maxRows` (capped at 5000), and `compare-versions` `maxClassResults` (capped at 5000) push `"<field> was clamped to <cap> from <value>."` when an over-range value is supplied (and `inspect-minecraft task="versions"` surfaces the `list-versions` warning). Non-breaking — out-of-range values still clamp and succeed; the caps are unchanged.
66
+ - Resource read errors (`mc://…` URIs) now carry the same recovery metadata as the equivalent tool call: `retryClass` and `issueOrigin` are always present, and `hints`, `suggestedCall`, `exampleCalls`, `fieldErrors`, and `context` are surfaced whenever the underlying `AppError` supplies them (previously the resource envelope returned only `code` + `detail`). The envelope keeps its existing nested `{ error, meta }` shape and the generic `problems/resource` type/title. Additive and non-breaking for clients reading by key.
67
+ - **Breaking:** error envelopes add a new `retryClass` value `"server"` for non-recoverable internal server faults. `ERR_INTERNAL` (sanitized programming bugs / unexpected throws, including every non-`AppError` thrown by a handler) and `ERR_DB_FAILURE` (SQLite integrity/migration failures) now report `retryClass: "server"` instead of `"transient"`, so agents stop retrying deterministic server bugs. Genuinely-transient codes are unchanged — `ERR_REPO_FETCH_FAILED`, `ERR_ARTIFACT_RESOLUTION_FAILED`, `ERR_STAGE_BUDGET_PRE_PARSE`, `ERR_LIMIT_EXCEEDED`, and `ERR_JAVA_PROCESS_FAILED` stay `"transient"`. Clients that exhaustively `switch` on `retryClass` must add a `"server"` case (treat it like `"permanent"` for retry posture: do not retry the identical call). `issueOrigin` is unchanged (`ERR_INTERNAL` stays `tool_issue`).
68
+
69
+ ### Fixed
70
+
71
+ - `remap-mod-jar` no longer serves a stale remapped JAR after a newer Fabric **yarn** build is published for the same Minecraft version. Yarn Tiny mappings are now cached per build coordinate (`mappings/yarn/${coordinate}.tiny`) and the resolved yarn coordinate is part of the remap cache key, so a new build produces a fresh remap instead of reusing the old jar. The newest build is re-checked at most once per 24h per MC version (a small on-disk metadata marker keeps the check off the cache-hit fast path; on a Fabric Maven outage the last-known-good build is reused rather than failing). Mojang mappings are immutable per release and unaffected. `remap-mod-jar` gains an optional `forceRemap: true` to bypass the cache and re-resolve on demand. The remap pipeline version bumps (`v2`→`v3`), a one-time invalidation of previously cached remapped jars; obsolete `mappings/${version}-yarn.tiny` files can be reclaimed via `manage-cache` on the `mapping` kind.
72
+
73
+ ### Performance
74
+
75
+ - `get-class-api-matrix` now computes per-row cross-namespace mappings only for the requested page (`maxRows`/`cursor` window) instead of the whole class on every request, so paginated/deep-page calls scale with the window rather than the full member set. `rows`, row order, `rowCount`, `nextCursor`, and `cursorIgnored` are byte-identical. **Behavior change:** on paginated calls, `warnings` (per-row ambiguity notes) and `ambiguousRowCount` now reflect only the returned page; an unpaginated call (no `maxRows`) still reports them for the whole class.
76
+
8
77
  ## [4.2.1] - 2026-05-23
9
78
 
10
79
  ### Fixed
package/README.md CHANGED
@@ -15,7 +15,7 @@
15
15
 
16
16
  It runs over stdio and works with Claude Desktop, Claude Code, VS Code, Codex CLI, Gemini CLI, and other MCP-capable clients.
17
17
 
18
- **37 tools** (6 entry + 31 expert) | **7 resources** | **4 namespace mappings** | **SQLite-backed cache**
18
+ **41 tools** (6 entry + 35 expert) | **9 resources** | **4 namespace mappings** | **SQLite-backed cache**
19
19
 
20
20
  ## Features
21
21
 
@@ -203,7 +203,8 @@ These notes cover high-frequency decisions during onboarding. For the full pitfa
203
203
  "version": "1.21.10",
204
204
  "sourceMapping": "mojang",
205
205
  "targetMapping": "intermediary",
206
- "signatureMode": "name-only"
206
+ "signatureMode": "name-only",
207
+ "detail": "standard"
207
208
  }
208
209
  }
209
210
  ```
@@ -306,13 +307,13 @@ Tools for converting symbol names between namespaces and checking symbol existen
306
307
  | Tool | Purpose |
307
308
  | --- | --- |
308
309
  | `find-mapping` | Look up mapping candidates for class, field, or method symbols |
309
- | `resolve-method-mapping-exact` | Resolve one method mapping with strict owner, name, and descriptor matching |
310
+ | `resolve-method-mapping-exact` | Strict shortcut for `find-mapping` (kind=method, signatureMode=exact); requires the full owner+name+descriptor triple |
310
311
  | `get-class-api-matrix` | Show one class API across `obfuscated`, `mojang`, `intermediary`, and `yarn` |
311
312
  | `resolve-workspace-symbol` | Resolve compile-visible symbol names from a Gradle workspace |
312
313
  | `check-symbol-exists` | Check whether a class, field, or method exists in a namespace |
313
314
  <!-- END GENERATED TOOL TABLE: mapping-symbols -->
314
315
 
315
- Several lookup tools support `compact` result shaping for shorter responses. See [docs/tool-reference.md](docs/tool-reference.md) for defaults and the full per-tool field list.
316
+ Expert and batch tools share the `detail` (`summary` | `standard` | `full`) + `include[]` response contract for shorter responses (replacing the old per-tool `compact` flag). See [docs/tool-reference.md](docs/tool-reference.md) for per-tool defaults and the full field list.
316
317
 
317
318
  ### NBT Utilities
318
319
 
@@ -1,8 +1,9 @@
1
1
  export type AccessTransformerAccessAction = "public" | "protected" | "package-private" | "private";
2
2
  export type AccessTransformerFinalAction = "add" | "remove";
3
+ export type AccessTransformerTargetKind = "class" | "field" | "method" | "wildcard-all" | "wildcard-method";
3
4
  export type AccessTransformerEntry = {
4
5
  line: number;
5
- targetKind: "class" | "field" | "method";
6
+ targetKind: AccessTransformerTargetKind;
6
7
  owner: string;
7
8
  target: string;
8
9
  name?: string;
@@ -19,10 +19,19 @@ function splitMemberToken(tokens) {
19
19
  if (tokens.length === 1) {
20
20
  const token = tokens[0] ?? "";
21
21
  const descriptorStart = token.indexOf("(");
22
+ // Wildcards: "*" targets all fields and methods; "*()" targets all methods.
23
+ // (Standard Forge/NeoForge AT syntax — previously parsed as a literal name.)
24
+ if (token === "*") {
25
+ return { targetKind: "wildcard-all", name: "*" };
26
+ }
22
27
  if (descriptorStart >= 0) {
28
+ const namePart = token.slice(0, descriptorStart);
29
+ if (namePart === "*") {
30
+ return { targetKind: "wildcard-method", name: "*", descriptor: token.slice(descriptorStart) };
31
+ }
23
32
  return {
24
33
  targetKind: "method",
25
- name: token.slice(0, descriptorStart),
34
+ name: namePart,
26
35
  descriptor: token.slice(descriptorStart)
27
36
  };
28
37
  }
@@ -9,6 +9,7 @@
9
9
  export type AccessWidenerEntry = {
10
10
  line: number;
11
11
  kind: "accessible" | "extendable" | "mutable";
12
+ transitive?: boolean;
12
13
  targetKind: "class" | "method" | "field";
13
14
  target: string;
14
15
  owner?: string;
@@ -40,7 +40,9 @@ export function parseAccessWidener(content) {
40
40
  }
41
41
  const kind = parts[0];
42
42
  const targetKind = parts[1];
43
- if (!VALID_KINDS.has(kind)) {
43
+ const transitive = kind.startsWith("transitive-");
44
+ const baseKind = transitive ? kind.slice("transitive-".length) : kind;
45
+ if (!VALID_KINDS.has(baseKind)) {
44
46
  parseWarnings.push(`Line ${lineNum}: Unknown access kind "${kind}".`);
45
47
  continue;
46
48
  }
@@ -48,16 +50,17 @@ export function parseAccessWidener(content) {
48
50
  parseWarnings.push(`Line ${lineNum}: Unknown target kind "${targetKind}".`);
49
51
  continue;
50
52
  }
51
- const validKind = kind;
53
+ const validKind = baseKind;
52
54
  const validTargetKind = targetKind;
53
55
  if (validTargetKind === "class") {
54
- entries.push({ line: lineNum, kind: validKind, targetKind: validTargetKind, target: parts[2] });
56
+ entries.push({ line: lineNum, kind: validKind, transitive, targetKind: validTargetKind, target: parts[2] });
55
57
  }
56
58
  else if (parts.length >= 5) {
57
59
  // method/field: <kind> <targetKind> <owner> <name> <descriptor>
58
60
  entries.push({
59
61
  line: lineNum,
60
62
  kind: validKind,
63
+ transitive,
61
64
  targetKind: validTargetKind,
62
65
  target: parts[2],
63
66
  owner: parts[2],
@@ -6,16 +6,33 @@ function asParamsRecord(params) {
6
6
  }
7
7
  return params;
8
8
  }
9
- export function buildSuggestedCall(spec) {
10
- if (SUGGESTED_CALL_VALIDATE_OFF) {
11
- const rawParams = asParamsRecord(spec.params);
12
- if (rawParams) {
13
- return { suggestedCall: { tool: spec.tool, params: rawParams } };
14
- }
15
- return {};
9
+ // Placeholder sentinels are `<...>` tokens that a caller must substitute before
10
+ // the call is executable (e.g. "<version>", "<fully-qualified-class-name>").
11
+ // The two JVM pseudo-method-names `<init>` / `<clinit>` are real, valid values
12
+ // and are explicitly excluded so constructor lookups are not misclassified.
13
+ const PLACEHOLDER_SENTINEL_RE = /<(?!init>)(?!clinit>)[^<>]+>/;
14
+ function containsPlaceholderSentinel(value) {
15
+ if (typeof value === "string") {
16
+ return PLACEHOLDER_SENTINEL_RE.test(value);
17
+ }
18
+ if (Array.isArray(value)) {
19
+ return value.some(containsPlaceholderSentinel);
20
+ }
21
+ if (value && typeof value === "object") {
22
+ return Object.values(value).some(containsPlaceholderSentinel);
16
23
  }
24
+ return false;
25
+ }
26
+ export function buildSuggestedCall(spec) {
17
27
  const primaryParams = asParamsRecord(spec.params);
18
- if (primaryParams) {
28
+ // A `suggestedCall` must be directly executable. Reject any primary payload
29
+ // that still carries a placeholder sentinel (e.g. "<version>") even when it
30
+ // is schema-valid: those belong in `exampleCalls` as templates, not as a
31
+ // call the caller can replay verbatim.
32
+ if (primaryParams && !containsPlaceholderSentinel(primaryParams)) {
33
+ if (SUGGESTED_CALL_VALIDATE_OFF) {
34
+ return { suggestedCall: { tool: spec.tool, params: primaryParams } };
35
+ }
19
36
  // Unknown-tool fail-open: the registry is populated at index.ts startup;
20
37
  // service-level tests that do not boot index.ts run with an empty
21
38
  // registry and rely on this pass-through. Callers that synthesize a tool
@@ -31,14 +48,18 @@ export function buildSuggestedCall(spec) {
31
48
  }
32
49
  const droppedMarker = spec.params !== undefined ? { _suggestedCallPrimaryDropped: true } : {};
33
50
  if (spec.examples && spec.examples.length > 0) {
51
+ // Example calls are templates (placeholders allowed) and only their
52
+ // schema-shape is checked. Mirror the primary path's unknown-tool
53
+ // fail-open so service-level tests with an unpopulated registry still
54
+ // surface templates instead of silently dropping them.
55
+ const exampleSchemaUnavailable = SUGGESTED_CALL_VALIDATE_OFF || !getToolSchema(spec.tool);
34
56
  const validated = spec.examples
35
57
  .map((example) => {
36
58
  const exampleParams = asParamsRecord(example.params);
37
59
  if (!exampleParams) {
38
60
  return null;
39
61
  }
40
- const result = validateToolParams(spec.tool, exampleParams);
41
- if (!result.valid) {
62
+ if (!exampleSchemaUnavailable && !validateToolParams(spec.tool, exampleParams).valid) {
42
63
  return null;
43
64
  }
44
65
  return {
@@ -34,6 +34,14 @@ type CacheEntryPage = {
34
34
  entries: CacheEntry[];
35
35
  nextCursor?: string;
36
36
  };
37
+ /**
38
+ * Whether a cache entry path contains the selector version as a whole version
39
+ * token. Used as a fallback when an entry has no structured meta.version. The
40
+ * match is anchored so that a coarse selector like "1.2" does NOT match a "1.21"
41
+ * path (which would cause destructive prune/delete to hit the wrong caches),
42
+ * while still allowing major.minor sweeps where "1.21" matches "1.21.4".
43
+ */
44
+ export declare function pathContainsVersion(path: string, version: string): boolean;
37
45
  export type CacheRegistryConfig = {
38
46
  cacheDir: string;
39
47
  sqlitePath: string;
@@ -1,6 +1,7 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { readdir, rm, stat } from "node:fs/promises";
1
+ import { existsSync } from "node:fs";
2
+ import { readFile, readdir, rm, stat } from "node:fs/promises";
3
3
  import { join, resolve } from "node:path";
4
+ import { mapWithConcurrencyLimit } from "./concurrency.js";
4
5
  import { createError, ERROR_CODES } from "./errors.js";
5
6
  import { normalizeOptionalPathForHost } from "./path-converter.js";
6
7
  import Database from "./storage/sqlite.js";
@@ -25,6 +26,7 @@ export const CACHE_HEALTH_STATES = [
25
26
  ];
26
27
  const STALE_ENTRY_AGE_MS = 30 * 24 * 60 * 60 * 1000;
27
28
  const CURSOR_VERSION = 1;
29
+ const CACHE_STAT_CONCURRENCY = 8;
28
30
  const STATUS_PRIORITY = ["in_use", "corrupt", "orphaned", "stale", "partial", "healthy"];
29
31
  function kindRoot(config, cacheKind) {
30
32
  switch (cacheKind) {
@@ -99,6 +101,20 @@ function inferVersion(...candidates) {
99
101
  }
100
102
  return undefined;
101
103
  }
104
+ /**
105
+ * Whether a cache entry path contains the selector version as a whole version
106
+ * token. Used as a fallback when an entry has no structured meta.version. The
107
+ * match is anchored so that a coarse selector like "1.2" does NOT match a "1.21"
108
+ * path (which would cause destructive prune/delete to hit the wrong caches),
109
+ * while still allowing major.minor sweeps where "1.21" matches "1.21.4".
110
+ */
111
+ export function pathContainsVersion(path, version) {
112
+ const escaped = version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
113
+ // Preceding char must not be a digit or dot (avoid matching inside a longer
114
+ // version like 1.21 -> 21); following char must not be a digit (block
115
+ // 1.2 -> 1.21) but a dot is allowed so 1.21 sweeps 1.21.4.
116
+ return new RegExp(`(?<![\\d.])${escaped}(?!\\d)`).test(path);
117
+ }
102
118
  function inferMapping(...candidates) {
103
119
  for (const candidate of candidates) {
104
120
  const normalized = candidate?.toLowerCase();
@@ -151,12 +167,12 @@ function inferProjectPath(pathValue, runtimeInfo) {
151
167
  }
152
168
  return undefined;
153
169
  }
154
- function isCorruptRegistryJson(filePath) {
170
+ async function isCorruptRegistryJson(filePath) {
155
171
  if (!filePath.endsWith(".json")) {
156
172
  return false;
157
173
  }
158
174
  try {
159
- JSON.parse(readFileSync(filePath, "utf8"));
175
+ JSON.parse(await readFile(filePath, "utf8"));
160
176
  return false;
161
177
  }
162
178
  catch {
@@ -228,7 +244,7 @@ function candidatePathsForEntry(entry) {
228
244
  function entryUpdatedAt(entry) {
229
245
  return typeof entry.meta?.updatedAt === "string" ? entry.meta.updatedAt : undefined;
230
246
  }
231
- function deriveEntryStatus(entry, config, now) {
247
+ function deriveEntryStatus(entry, config, now, entryPathExists = false) {
232
248
  const maybeMeta = entry.meta ?? {};
233
249
  if (maybeMeta.inUse === true) {
234
250
  return "in_use";
@@ -237,7 +253,7 @@ function deriveEntryStatus(entry, config, now) {
237
253
  return "corrupt";
238
254
  }
239
255
  const candidatePaths = candidatePathsForEntry(entry);
240
- const existingPaths = candidatePaths.filter((candidate) => existsSync(candidate));
256
+ const existingPaths = candidatePaths.filter((candidate) => (entryPathExists && candidate === entry.path) || existsSync(candidate));
241
257
  if (entry.cacheKind === "artifact-index" && !existsSync(config.sqlitePath)) {
242
258
  return "orphaned";
243
259
  }
@@ -261,10 +277,9 @@ function deriveEntryStatus(entry, config, now) {
261
277
  }
262
278
  function sortEntries(entries) {
263
279
  return [...entries].sort((left, right) => {
264
- if (left.cacheKind !== right.cacheKind) {
265
- return left.cacheKind.localeCompare(right.cacheKind);
266
- }
267
- return left.entryId.localeCompare(right.entryId);
280
+ const leftKey = entrySortKey(left);
281
+ const rightKey = entrySortKey(right);
282
+ return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
268
283
  });
269
284
  }
270
285
  function entrySortKey(entry) {
@@ -329,7 +344,7 @@ function matchesSelector(entry, selector, runtimeInfo) {
329
344
  }
330
345
  if (selector.version) {
331
346
  const version = typeof maybeMeta.version === "string" ? maybeMeta.version : undefined;
332
- if (version !== selector.version && !entry.path.includes(selector.version)) {
347
+ if (version !== selector.version && !pathContainsVersion(entry.path, selector.version)) {
333
348
  return false;
334
349
  }
335
350
  }
@@ -353,17 +368,19 @@ function matchesSelector(entry, selector, runtimeInfo) {
353
368
  return false;
354
369
  }
355
370
  }
356
- const normalizedPaths = candidatePathsForEntry(entry)
357
- .map((candidate) => normalizePathKey(candidate, runtimeInfo))
358
- .filter((candidate) => Boolean(candidate));
359
- if (selector.normalizedJarPath && !normalizedPaths.includes(selector.normalizedJarPath)) {
360
- return false;
361
- }
362
- if (selector.normalizedProjectPath) {
363
- const projectMatch = normalizedPaths.some((candidate) => candidate === selector.normalizedProjectPath || candidate.startsWith(`${selector.normalizedProjectPath}/`));
364
- if (!projectMatch) {
371
+ if (selector.normalizedJarPath || selector.normalizedProjectPath) {
372
+ const normalizedPaths = candidatePathsForEntry(entry)
373
+ .map((candidate) => normalizePathKey(candidate, runtimeInfo))
374
+ .filter((candidate) => Boolean(candidate));
375
+ if (selector.normalizedJarPath && !normalizedPaths.includes(selector.normalizedJarPath)) {
365
376
  return false;
366
377
  }
378
+ if (selector.normalizedProjectPath) {
379
+ const projectMatch = normalizedPaths.some((candidate) => candidate === selector.normalizedProjectPath || candidate.startsWith(`${selector.normalizedProjectPath}/`));
380
+ if (!projectMatch) {
381
+ return false;
382
+ }
383
+ }
367
384
  }
368
385
  return true;
369
386
  }
@@ -441,18 +458,17 @@ function workspaceCacheEntries(workspaceCache) {
441
458
  }
442
459
  }));
443
460
  }
444
- async function fileBackedEntries(config, cacheKind) {
461
+ async function fileBackedEntries(config, cacheKind, detectCorruption) {
445
462
  if (cacheKind === "binary-remap") {
446
463
  return binaryRemapEntries(config);
447
464
  }
448
465
  const root = kindRoot(config, cacheKind);
449
466
  const files = await listFilesRecursive(root);
450
- const entries = [];
451
- for (const filePath of files) {
467
+ return mapWithConcurrencyLimit(files, CACHE_STAT_CONCURRENCY, async (filePath) => {
452
468
  const fileStat = await stat(filePath);
453
469
  const normalizedEntryId = filePath.slice(root.length + 1);
454
470
  const inferredScope = inferScope(filePath, normalizedEntryId) ?? (cacheKind === "decompiled-source" ? "vanilla" : undefined);
455
- entries.push({
471
+ return {
456
472
  cacheKind,
457
473
  entryId: normalizedEntryId,
458
474
  path: filePath,
@@ -465,7 +481,7 @@ async function fileBackedEntries(config, cacheKind) {
465
481
  scope: inferredScope,
466
482
  projectPath: inferProjectPath(filePath, config.pathRuntimeInfo),
467
483
  partial: fileStat.size === 0,
468
- corrupt: cacheKind === "registry" ? isCorruptRegistryJson(filePath) : false,
484
+ corrupt: cacheKind === "registry" && detectCorruption ? await isCorruptRegistryJson(filePath) : false,
469
485
  inUse: filePath.endsWith(".lock") ||
470
486
  filePath.endsWith(".wal") ||
471
487
  filePath.endsWith(".journal"),
@@ -473,9 +489,8 @@ async function fileBackedEntries(config, cacheKind) {
473
489
  ? { jarPath: filePath }
474
490
  : {})
475
491
  }
476
- });
477
- }
478
- return entries;
492
+ };
493
+ });
479
494
  }
480
495
  /**
481
496
  * Binary-remap cache entries are keyed by the final artifact id even when the
@@ -556,9 +571,10 @@ async function directoryFileSizeBytes(root) {
556
571
  }
557
572
  export function createCacheRegistry(config) {
558
573
  const workspaceCache = config.workspaceContextCache ?? getProcessWorkspaceContextCache();
559
- async function collectEntries(cacheKinds, selector) {
574
+ async function collectEntries(cacheKinds, selector, detectCorruption = false) {
560
575
  const selectedKinds = cacheKinds?.length ? cacheKinds : [...PUBLIC_CACHE_KINDS];
561
576
  const preparedSelector = prepareSelector(selector, config.pathRuntimeInfo);
577
+ const detectCorruptionForKinds = detectCorruption || selector?.status === "corrupt";
562
578
  const now = Date.now();
563
579
  const entries = await Promise.all(selectedKinds.map((cacheKind) => {
564
580
  if (cacheKind === "artifact-index") {
@@ -567,13 +583,15 @@ export function createCacheRegistry(config) {
567
583
  if (cacheKind === "workspace") {
568
584
  return Promise.resolve(workspaceCacheEntries(workspaceCache));
569
585
  }
570
- return fileBackedEntries(config, cacheKind);
586
+ return fileBackedEntries(config, cacheKind, detectCorruptionForKinds);
571
587
  }));
572
588
  const enriched = entries
573
589
  .flat()
574
590
  .map((entry) => ({
575
591
  ...entry,
576
- status: entry.cacheKind === "workspace" ? entry.status : deriveEntryStatus(entry, config, now)
592
+ status: entry.cacheKind === "workspace"
593
+ ? entry.status
594
+ : deriveEntryStatus(entry, config, now, entry.cacheKind !== "artifact-index")
577
595
  }));
578
596
  return sortEntries(enriched.filter((entry) => matchesSelector(entry, preparedSelector, config.pathRuntimeInfo)));
579
597
  }
@@ -614,7 +632,7 @@ export function createCacheRegistry(config) {
614
632
  return entries.slice(0, limit);
615
633
  },
616
634
  async verifyEntries(input) {
617
- const entries = await collectEntries(input.cacheKinds, input.selector);
635
+ const entries = await collectEntries(input.cacheKinds, input.selector, true);
618
636
  const unhealthy = entries.filter((entry) => entry.status !== "healthy");
619
637
  const warningStatuses = [...new Set(unhealthy.map((entry) => entry.status))];
620
638
  return {
package/dist/config.d.ts CHANGED
@@ -12,6 +12,7 @@ declare const DEFAULTS: {
12
12
  readonly fetchTimeoutMs: 15000;
13
13
  readonly fetchRetries: 2;
14
14
  readonly searchScanPageSize: 250;
15
+ readonly searchScanMaxBytes: 67108864;
15
16
  readonly indexInsertChunkSize: 200;
16
17
  readonly maxMappingGraphCache: 16;
17
18
  readonly maxSignatureCache: 2000;
package/dist/config.js CHANGED
@@ -20,6 +20,7 @@ const DEFAULTS = {
20
20
  fetchTimeoutMs: 15000,
21
21
  fetchRetries: 2,
22
22
  searchScanPageSize: 250,
23
+ searchScanMaxBytes: 67_108_864,
23
24
  indexInsertChunkSize: 200,
24
25
  maxMappingGraphCache: 16,
25
26
  maxSignatureCache: 2_000,
@@ -41,6 +42,8 @@ const MAX_CACHE_BYTES_LOWER_BOUND = 1_024;
41
42
  const TIMEOUT_LOWER_BOUND_MS = 500;
42
43
  const SEARCH_SCAN_PAGE_SIZE_LOWER_BOUND = 1;
43
44
  const SEARCH_SCAN_PAGE_SIZE_UPPER_BOUND = 10_000;
45
+ const SEARCH_SCAN_MAX_BYTES_LOWER_BOUND = 1_024;
46
+ const SEARCH_SCAN_MAX_BYTES_UPPER_BOUND = 4_294_967_296;
44
47
  const INDEX_INSERT_CHUNK_SIZE_LOWER_BOUND = 1;
45
48
  const INDEX_INSERT_CHUNK_SIZE_UPPER_BOUND = 20_000;
46
49
  const CACHE_ENTRIES_LOWER_BOUND = 1;
@@ -163,6 +166,7 @@ export function loadConfig() {
163
166
  fetchTimeoutMs: parseNumber(process.env.MCP_FETCH_TIMEOUT_MS, DEFAULTS.fetchTimeoutMs, TIMEOUT_LOWER_BOUND_MS, Number.MAX_SAFE_INTEGER),
164
167
  fetchRetries: parseNumber(process.env.MCP_FETCH_RETRIES, DEFAULTS.fetchRetries, MAX_RETRIES_LOWER_BOUND, MAX_RETRIES_UPPER_BOUND),
165
168
  searchScanPageSize: parseNumber(process.env.MCP_SEARCH_SCAN_PAGE_SIZE, DEFAULTS.searchScanPageSize, SEARCH_SCAN_PAGE_SIZE_LOWER_BOUND, SEARCH_SCAN_PAGE_SIZE_UPPER_BOUND),
169
+ searchScanMaxBytes: parseNumber(process.env.MCP_SEARCH_SCAN_MAX_BYTES, DEFAULTS.searchScanMaxBytes, SEARCH_SCAN_MAX_BYTES_LOWER_BOUND, SEARCH_SCAN_MAX_BYTES_UPPER_BOUND),
166
170
  indexInsertChunkSize: parseNumber(process.env.MCP_INDEX_INSERT_CHUNK_SIZE, DEFAULTS.indexInsertChunkSize, INDEX_INSERT_CHUNK_SIZE_LOWER_BOUND, INDEX_INSERT_CHUNK_SIZE_UPPER_BOUND),
167
171
  maxMappingGraphCache: parseNumber(process.env.MCP_CACHE_GRAPH_MAX, DEFAULTS.maxMappingGraphCache, CACHE_ENTRIES_LOWER_BOUND, CACHE_ENTRIES_UPPER_BOUND),
168
172
  maxSignatureCache: parseNumber(process.env.MCP_CACHE_SIGNATURE_MAX, DEFAULTS.maxSignatureCache, CACHE_ENTRIES_LOWER_BOUND, CACHE_ENTRIES_UPPER_BOUND),
@@ -1,4 +1,4 @@
1
- import { access, constants, mkdir, readFile, readdir, stat } from "node:fs/promises";
1
+ import { access, constants, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
2
2
  import { mkdirSync, rmSync } from "node:fs";
3
3
  import { createHash } from "node:crypto";
4
4
  import { basename, join, relative, sep } from "node:path";
@@ -8,6 +8,7 @@ import { assertJavaAvailable, runJavaProcess } from "../java-process.js";
8
8
  import { log } from "../logger.js";
9
9
  const DEFAULT_TIMEOUT_MS = 120_000;
10
10
  const DECOMPILED_JAVA_READ_CONCURRENCY = 8;
11
+ const DECOMPILE_COMPLETE_MARKER = ".decompile-complete";
11
12
  const VINEFLOWER_FLAG_PROFILES = [
12
13
  { label: "default", flags: ["-din=1", "-rbr=1", "-dgs=1"] },
13
14
  { label: "relaxed", flags: ["-din=1", "-rbr=0", "-dgs=0"] },
@@ -132,7 +133,10 @@ export async function decompileBinaryJar(binaryJarPath, cacheDir, options) {
132
133
  try {
133
134
  await mkdir(outputDir, { recursive: true });
134
135
  const outputDirStats = await stat(outputDir).catch(() => undefined);
135
- if (outputDirStats) {
136
+ const markerPresent = outputDirStats
137
+ ? await access(join(outputDir, DECOMPILE_COMPLETE_MARKER), constants.F_OK).then(() => true, () => false)
138
+ : false;
139
+ if (outputDirStats && markerPresent) {
136
140
  const existingJavaFiles = await collectJavaFiles(outputDir);
137
141
  if (existingJavaFiles.length > 0) {
138
142
  const results = await mapWithConcurrencyLimit(existingJavaFiles, DECOMPILED_JAVA_READ_CONCURRENCY, async (candidate) => {
@@ -153,6 +157,7 @@ export async function decompileBinaryJar(binaryJarPath, cacheDir, options) {
153
157
  };
154
158
  }
155
159
  }
160
+ clearOutputDir(outputDir);
156
161
  await assertVineflowerAvailable(options.vineflowerJarPath);
157
162
  await assertJavaAvailable();
158
163
  const profilesAttempted = [];
@@ -189,6 +194,7 @@ export async function decompileBinaryJar(binaryJarPath, cacheDir, options) {
189
194
  content: await readFileTreeText(abs)
190
195
  };
191
196
  });
197
+ await writeFile(join(outputDir, DECOMPILE_COMPLETE_MARKER), profile.label, "utf8");
192
198
  emitDecompileLog("decompile.done", {
193
199
  durationMs: Date.now() - startedAt,
194
200
  artifactIdCandidate: options.artifactIdCandidate,