@adhisang/minecraft-modding-mcp 6.3.0 → 7.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +13 -3
  3. package/dist/cache-policy.d.ts +71 -0
  4. package/dist/cache-policy.js +83 -0
  5. package/dist/cache-registry.js +45 -8
  6. package/dist/cli.js +74 -3
  7. package/dist/compat-stdio-transport.d.ts +1 -1
  8. package/dist/compat-stdio-transport.js +13 -1
  9. package/dist/config.d.ts +3 -0
  10. package/dist/config.js +8 -2
  11. package/dist/decompiler/vineflower.d.ts +1 -0
  12. package/dist/decompiler/vineflower.js +8 -5
  13. package/dist/entry-tools/analyze-mod-service.d.ts +70 -136
  14. package/dist/entry-tools/analyze-symbol-service.d.ts +112 -150
  15. package/dist/entry-tools/compare-minecraft-service.d.ts +59 -145
  16. package/dist/entry-tools/entry-tool-schema.d.ts +38 -4
  17. package/dist/entry-tools/entry-tool-schema.js +4 -1
  18. package/dist/entry-tools/inspect-minecraft/internal.d.ts +235 -799
  19. package/dist/entry-tools/inspect-minecraft/internal.js +50 -13
  20. package/dist/entry-tools/inspect-minecraft-service.d.ts +372 -1736
  21. package/dist/entry-tools/manage-cache-service.d.ts +81 -91
  22. package/dist/entry-tools/validate-project/cases/mixin.js +26 -6
  23. package/dist/entry-tools/validate-project/cases/project-summary.d.ts +7 -7
  24. package/dist/entry-tools/validate-project-service.d.ts +164 -592
  25. package/dist/entry-tools/verify-mixin-target-service.d.ts +3 -19
  26. package/dist/entry-tools/verify-mixin-target-service.js +19 -1
  27. package/dist/era-classifier.d.ts +161 -0
  28. package/dist/era-classifier.js +292 -0
  29. package/dist/error-mapping.d.ts +76 -0
  30. package/dist/error-mapping.js +116 -8
  31. package/dist/index.d.ts +42 -4
  32. package/dist/index.js +636 -473
  33. package/dist/java-process.d.ts +2 -0
  34. package/dist/java-process.js +22 -2
  35. package/dist/json-rpc-framing.d.ts +77 -1
  36. package/dist/json-rpc-framing.js +249 -13
  37. package/dist/mapping/loaders/tiny-loom-selection.d.ts +88 -0
  38. package/dist/mapping/loaders/tiny-loom-selection.js +223 -0
  39. package/dist/mapping/loaders/tiny-loom.js +45 -33
  40. package/dist/mapping/loaders/tiny-maven.js +6 -11
  41. package/dist/mapping/parsers/tiny.d.ts +57 -0
  42. package/dist/mapping/parsers/tiny.js +99 -22
  43. package/dist/mapping-service.d.ts +19 -0
  44. package/dist/mapping-service.js +93 -9
  45. package/dist/maven-resolver.d.ts +18 -0
  46. package/dist/maven-resolver.js +20 -0
  47. package/dist/mcp-helpers.d.ts +19 -2
  48. package/dist/mcp-helpers.js +58 -9
  49. package/dist/minecraft-explorer-service.d.ts +1 -1
  50. package/dist/mixin/types.d.ts +8 -0
  51. package/dist/mod-analyzer.js +7 -7
  52. package/dist/mod-decompile-service.js +1 -0
  53. package/dist/nbt/java-nbt-codec.js +12 -2
  54. package/dist/nbt/json-patch.js +14 -3
  55. package/dist/nbt/pipeline.js +40 -3
  56. package/dist/nbt/typed-json.js +26 -1
  57. package/dist/registration-adapter.d.ts +32 -0
  58. package/dist/registration-adapter.js +52 -0
  59. package/dist/repo-downloader.d.ts +165 -0
  60. package/dist/repo-downloader.js +568 -10
  61. package/dist/request-context.d.ts +7 -0
  62. package/dist/request-context.js +9 -0
  63. package/dist/resources.d.ts +1 -1
  64. package/dist/resources.js +25 -19
  65. package/dist/server-identity.d.ts +27 -0
  66. package/dist/server-identity.js +26 -0
  67. package/dist/source/access-validate.js +53 -0
  68. package/dist/source/artifact-resolver.d.ts +81 -2
  69. package/dist/source/artifact-resolver.js +227 -15
  70. package/dist/source/class-source.d.ts +36 -0
  71. package/dist/source/class-source.js +222 -38
  72. package/dist/source/did-you-mean.d.ts +12 -1
  73. package/dist/source/did-you-mean.js +6 -2
  74. package/dist/source/file-access.js +150 -46
  75. package/dist/source/indexer.js +1 -0
  76. package/dist/source/shared-utils.d.ts +21 -0
  77. package/dist/source/shared-utils.js +23 -0
  78. package/dist/source-resolver.js +224 -57
  79. package/dist/source-service.d.ts +12 -1
  80. package/dist/stdio-supervisor.d.ts +357 -2
  81. package/dist/stdio-supervisor.js +1031 -80
  82. package/dist/storage/db.d.ts +2 -1
  83. package/dist/storage/db.js +15 -8
  84. package/dist/synthetic-decorator.d.ts +24 -0
  85. package/dist/synthetic-decorator.js +48 -0
  86. package/dist/tool-guidance.d.ts +17 -1
  87. package/dist/tool-guidance.js +323 -18
  88. package/dist/tool-schema-registry.d.ts +2 -0
  89. package/dist/tool-schema-registry.js +4 -0
  90. package/dist/tool-schemas.d.ts +2212 -3919
  91. package/dist/tool-schemas.js +33 -7
  92. package/dist/types.d.ts +35 -0
  93. package/dist/v1-parity-schemas.d.ts +7 -0
  94. package/dist/v1-parity-schemas.js +5584 -0
  95. package/dist/version-diff-service.d.ts +33 -0
  96. package/dist/version-diff-service.js +148 -3
  97. package/dist/version-service.js +36 -14
  98. package/dist/warning-details.js +18 -1
  99. package/docs/README-ja.md +5 -3
  100. package/docs/tool-reference.md +196 -19
  101. package/package.json +13 -8
package/CHANGELOG.md CHANGED
@@ -5,6 +5,96 @@ 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.1] - 2026-08-28
11
+
12
+ ### Added
13
+
14
+ - A class-not-found error on a Jar-in-Jar shell now includes a `nestedJars` array listing the shell's inner jars by entry name, alongside `didYouMean`, on both single-tool and `batch-*` error entries. A miss against a shell — for example the Fabric API umbrella JAR — no longer needs a separate `resolve-artifact` call to find which inner module actually holds the class. The array appears only when the artifact carries an inventory, is never empty, and is capped at 64 entries.
15
+
16
+ ### Changed
17
+
18
+ - **A dependency's `artifactId` is now derived from a sha256 of the jar's bytes**, replacing a signature built from HTTP freshness headers (sources jars) or local file modification time and size (binary jars) — neither of which reflects the actual content. Each already-cached dependency gets a new `artifactId` and re-indexes once on its first resolve after upgrading (including a fresh decompile for binary-only dependencies); ids you already have keep resolving.
19
+ - **A `-SNAPSHOT` coordinate is now re-checked against its repository on every resolve** instead of being pinned to its first download, since Maven allows republishing a `-SNAPSHOT` under the same name. The check is conditional (no transfer) when the cached copy carries an `ETag` or `Last-Modified`, and an ordinary request otherwise; a republished jar replaces the cached one and gets a new `artifactId`. If the check cannot complete — timeout, server error, rate limit, or similar — the cached bytes are served rather than failing the resolve; a definitive not-found or forbidden response moves resolution to the next configured repository instead. Release versions, and `-SNAPSHOT` jars served from `~/.m2` or the Gradle cache, are unaffected.
20
+ - **A binary-only dependency resolved from local disk now reports different fields.** A coordinate whose module publishes no sources jar anywhere, but whose binary jar is already in `~/.m2` or the Gradle cache, is now answered from that local jar (see Fixed) with `origin: "local-m2"` and a `binaryJarPath` pointing at the local jar, instead of `origin: "decompiled"` with a downloaded copy.
21
+ - **The decompiled-source warning on `get-class-source` now follows whether the source text was actually decompiled**, not the artifact's `origin` label. The only case this changes is the binary-only-from-local-disk dependency above: its text is decompiled even though its origin names where the jar came from, so it now correctly carries the warning.
22
+ - **`manage-cache` now counts each downloaded jar's small on-disk freshness record into that jar's own cache entry** instead of surfacing it as a separate entry, so cache-size and entry-count reporting no longer double-counts it.
23
+ - **Wire contract change.** A `get-class-members` failure on an artifact with no binary jar now reports `issueOrigin: "tool_issue"` when the tool itself picked that artifact (from a version or coordinate), instead of always reporting `"code_issue"` — since nothing in that kind of request lets the caller pick a different jar. A caller who named the artifact directly (an explicit `artifactId` or `target: { kind: "jar", ... }`) still gets `"code_issue"` for a missing binary jar. `verify-mixin-target` gets the same reclassification (see Fixed); `batch-class-members` does not yet. Clients that branch on `issueOrigin` should read it from each response rather than assume a fixed value per error code.
24
+
25
+ ### Fixed
26
+
27
+ - `get-class-members` and `batch-class-members` now answer a dependency target whose module publishes no sources jar, when its binary jar is already in the local Gradle cache — previously that binary jar was discarded and the read failed with `ERR_CONTEXT_UNRESOLVED`, even though `get-class-source` could read the same target. Bytecode-backed reads now work for binary-only modules from both the Gradle cache and `~/.m2`.
28
+ - Resolving the same dependency coordinate twice no longer re-downloads its jar or mints a new `artifactId`; a cached jar is reused and identified by the sha256 of its bytes, so the id is stable from the first resolve onward.
29
+ - A class-not-found hint no longer blames Minecraft obfuscation for artifacts that were never obfuscated. Dependency and Jar-in-Jar-shell misses previously advised remapping with `mapping="mojang"` even when the caller had already passed it, or when the artifact held no classes of its own; the hint is now suppressed for those artifacts, and whenever the request already named a non-obfuscated mapping. Vanilla artifacts genuinely indexed in obfuscated names still get the hint.
30
+ - `validate-project` with `task="mixin"` and no `version` no longer suggests a Minecraft version it never derived. The error previously carried a directly re-executable recovery call hardcoding `1.21.10`, so replaying it validated a project's mixins against the wrong version. It now points to `list-versions` and uses a `<your-mc-version>` placeholder naming where the real value lives (`gradle.properties`, or `task="project-summary"` with `preferProjectVersion: true`).
31
+ - `get-class-members` and `batch-class-members` now answer a coordinate whose module ships no sources jar anywhere, when a readable binary jar already sits in `~/.m2` or the Gradle cache — previously that local jar was discarded and the same bytes were downloaded again, or the call failed outright if no repository had it either. `~/.m2` and the Gradle cache are both tried, so a damaged copy in one does not hide a good copy in the other; a caller passing `allowDecompile: false` is still refused, since serving that artifact means decompiling it.
32
+ - A repository answering with something that is not a jar (an HTML error page, a truncated response) no longer becomes the resolved artifact — such a response is now checked as a readable archive before being accepted, on both a fresh download and a cache hit, so a cache already poisoned by an earlier version heals itself on the next resolve. A response with no body is likewise refused rather than cached.
33
+ - Reading an error through a `mc://` resource URI now reports the same `issueOrigin` as the equivalent tool call, instead of always reporting `"code_issue"` for a failure the tool call itself classifies as `"tool_issue"`.
34
+ - `verify-mixin-target` no longer blames the caller when the tool itself resolved an artifact with no binary jar from a `target: { kind: "version", ... }`; it now reports `issueOrigin: "tool_issue"` with guidance naming the two ways forward — re-target at a jar path directly, or use a version whose artifact ships one. A caller who passed `target: { kind: "jar", ... }` directly still gets `"code_issue"` for a missing binary jar.
35
+
36
+ ## [7.0.0-rc.0] - 2026-08-22
37
+
38
+ ### Added
39
+
40
+ - Support for MCP protocol revision `2026-07-28`, served alongside the legacy `initialize` protocol by the same process, so modern and legacy clients can both connect. A modern client calls `server/discover` for `supportedVersions: ["2026-07-28"]`, the advertised capabilities and the server identity, then carries the protocol version, its client capabilities and optional client info in `io.modelcontextprotocol/*` `_meta` on every request. The cacheable methods (`tools/list`, `resources/list`, `resources/read`, `resources/templates/list`, `server/discover`) return `ttlMs` and `cacheScope` for private caching, and every modern result carries `resultType: "complete"` and the server-identity `_meta` echo, including the replies synthesized for queue overflow, worker restart and timeout. A connection selects one era and keeps it: mixing eras is refused with `data.kind: "era_conflict"` or `"missing_meta"`, and an unsupported modern version answers `-32022` with `data.supported` / `data.requested`. The legacy `initialize` protocol stays supported; see the SDK v2 entry under Changed for the five legacy wire differences this release does carry. See `docs/tool-reference.md` → MCP Protocol Support.
41
+ - Memory tuning environment variables: `MCP_SQLITE_CACHE_KB` (artifact-index page cache in KiB, default `8000`) and `MCP_SQLITE_MMAP_SIZE` (bytes, default `268435456`, `0` disables) for the artifact index, which now also keeps temporary tables in memory (`temp_store=MEMORY`), and `MCP_DECOMPILE_MAX_MEMORY_MB` (default `4096`) to cap the Vineflower JVM heap the way `MCP_REMAP_MAX_MEMORY_MB` caps remapping. Defaults preserve existing behavior on typical installations.
42
+ - `MCP_MAX_FRAME_BYTES` bounds the JSON-RPC frame size the stdio transport accepts (default 64 MiB, minimum 1 MiB). Oversized frames are 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.
43
+
44
+ ### Changed
45
+
46
+ - **Breaking — Node.js 22.13.0 or newer is now required.** `engines.node` is raised from `>=22` to `>=22.13.0`, the first release where `node:sqlite` works unflagged and `StatementSync.iterate` exists. On Node 22.0–22.12 the server fails at startup rather than degrading, so upgrade Node.js before installing. The previous `>=22` floor understated the real requirement.
47
+ - **Breaking — importing this package as a Node library requires migration.** The exported `server` is now an SDK v2 `McpServer` with a different API surface, `buildServer` takes an optional `McpRequestContext`, and the published type declarations resolve Zod 4 types. Clients that talk to the server over stdio are unaffected.
48
+ - The runtime moved from MCP SDK v1 (`@modelcontextprotocol/sdk` 1.27.1, Zod 3) to SDK v2 (`@modelcontextprotocol/server` and `@modelcontextprotocol/client` 2.0.0, Zod 4.4.3). Legacy clients keep byte-compatible wire behavior — advertised `inputSchema` bytes, argument validation and validation-error bytes are unchanged — with five exceptions: (1) `tools/call` with `arguments` omitted now reaches the tool as `{}`, so input-free tools succeed, schemas with required fields answer per-field `ERR_INVALID_INPUT`, and a tool whose schema accepts `{}` but whose handler needs input answers its own error (`json-to-nbt` → `ERR_NBT_INVALID_TYPED_JSON`); (2) `tools/list` entries no longer carry the v1-only `execution: {"taskSupport":"forbidden"}` field; (3) unknown and disabled tool replies no longer consume a queue slot, so reply ordering and overflow outcomes under concurrent load can differ; (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 the URI also in `data.uri`; (5) `initialize` advertises `resources: { listChanged: false }` and `tools: { listChanged: false }`, where it previously advertised `true` for both.
49
+ - **Wire contract change — both eras.** Advertised capabilities now carry `resources: { listChanged: false }` and `tools: { listChanged: false }`; both were previously `true`. The tool and resource surface is fixed at process start, the server never emits list-changed notifications, and `subscriptions/listen` is rejected in both eras, so the old `true` advertised a stream that could never carry anything. Treat both lists as static for the lifetime of the process and re-read them only across a restart.
50
+ - **Behavior change.** `get-class-members` and `batch-class-members` now inherit the resolved artifact's mapping when the call supplies no explicit `mapping`, matching `get-class-source`. Previously a mojang-mapped artifact answered member reads in the obfuscated namespace — `ownerFqn: "dlp"` with fields `e, f, g` — so the documented `find-class` → `get-class-source` → `get-class-members` flow returned two different namespaces for the same artifact. `context.mappingNamespace` and `minecraftVersion` no longer contradict `returnedNamespace`, and an explicit `mapping` is still never overridden. The `get-class-members.mapping` schema description still reads "default obfuscated" because the advertised schema bytes are pinned for legacy compatibility; the actual rule is documented in `docs/tool-reference.md`.
51
+ - **Behavior change — result counts differ.** `compare-versions` now diffs both jars in the mojang namespace instead of comparing raw obfuscated entries, which are not stable across Minecraft versions and made the old diff compare 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 carry `classes.namespace` and `classes.packageFilter { matchedFrom, matchedTo }`; a filter that matches nothing now warns instead of returning a zeroed result that reads as "no changes", and an unavailable mapping degrades to the obfuscated namespace with a loud warning.
52
+ - **Behavior change — a previously successful call now fails.** `validate-access-widener` and `validate-access-transformer` refuse a `scope: "merged"` resolution that would serve a jar from a different loader than the workspace, answering `ERR_CONTEXT_UNRESOLVED`. Previously a Fabric 1.21.11 request could be answered with a NeoForge 1.21.10 jar, and `validate-access-widener` then reported `valid: true` for a Fabric access widener checked against that jar — a false PASS. Same-loader version drift is still answered, now marked `approximate: true`. `resolve-artifact` and `get-class-source` are unaffected.
53
+ - **Behavior change — a different artifact is resolved.** `inspect-minecraft` now resolves a workspace subject exactly as `resolve-artifact` does for the same directory, instead of reading `minecraft_version` itself and resolving without a mapping. The two tools previously returned different artifacts for one project directory: `task="class-members"` failed to find a mojang class name that `batch-class-members` found on the same class. A call that used to resolve an obfuscated vanilla artifact can now resolve the merged, mojang-mapped one and report a different `artifactId`. An explicit `subject.mapping` or `subject.scope` is still never overridden, and `WORKSPACE_TARGET_OFF=1` restores the previous routing.
54
+ - **Behavior change — a response field reports a different set.** `resolve-method-mapping-exact` now reports the strict candidates its verdict was computed from on `status: "resolved"` and `"ambiguous"`, so `candidates` and `candidateCount` shrink where the simple-name index contributed extra matches. A caller can no longer be shown a `confidence: 1, matchKind: "exact"` candidate on another owner or descriptor next to an `ambiguous` verdict. `status: "not_found"` and `"mapping_unavailable"` keep the wider list, where the near misses are the useful content. `resolved` responses omit `candidates` and return `resolvedSymbol` with `candidateCount: 1`; `ambiguous` responses gain a warning counting how many candidates were rejected by owner and how many by descriptor, plus `ambiguityReasons[]`, which now also appears on `resolve-workspace-symbol` method-ambiguous responses. `find-mapping` still returns the unfiltered list.
55
+ - **Behavior change.** `resolve-method-mapping-exact` is now owner-strict: it requires the full `owner + name + descriptor`, where it previously filtered on the descriptor alone and could pull in a same-signature method from an unrelated class, forcing a false `ambiguous`. Queries whose owner declares exactly one matching method now resolve. The tradeoff: a method the owner **inherits** rather than declares moves from `ambiguous` to `not_found`, because mapping files record declarations and carry no class hierarchy. That case returns 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`.
56
+ - **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`, naming the `projectPath` and carrying `nextAction` and a `suggestedCall`. Previously it surfaced as `ERR_INVALID_INPUT` "Either artifactId or target must be provided.", which described neither the cause nor the fix. `task="class-overview"` and `task="file"`, which answered with a `blocked` summary, now return the typed error too.
57
+ - Modern-era `resources/read` for a URI matching a registered template with no backing artifact now answers JSON-RPC `-32602`, as the revision requires, instead of a successful `resultType: "complete"` result carrying a ProblemDetails document. Applies to the five 404-class error codes. Legacy replies keep the ProblemDetails envelope.
58
+ - Modern-era `tools/list` returns tools in name-ascending order; legacy connections keep the previous registration order.
59
+ - `provenance.mappingArtifact` for Loom-sourced mappings names the file actually merged rather than the alphabetically first one found (for the measured 1.21.10 cache, `mappings-mojang.tiny` instead of `intermediary-v2.tiny`).
60
+
61
+ ### Fixed
62
+
63
+ - Runtime provenance no longer claims a version or loader that was not served. `provenance` describes the jar actually used — `version`, `requestedVersion`, `versionApproximated`, `servedLoader`, `expectedLoader`, `loaderMismatch` — where it previously echoed the requested version back, making a substituted artifact indistinguishable from an exact match. `resolve-artifact` gains `provenance.versionApproximation` and corrects `resolvedFrom.version`.
64
+ - `get-artifact-file` serves root-level and `META-INF/**` jar entries. Delivery was gated on an `assets/` or `data/` prefix, so `fabric.mod.json`, `META-INF/MANIFEST.MF`, `<mod>.mixins.json` and license files answered `ERR_FILE_NOT_FOUND` despite being in the jar — precisely the files you read first when inspecting a mod. Extension-less entries are now classified by checking for valid UTF-8 instead of being assumed binary. The 512 KiB cap, `truncated` flag, `contentOmittedReason` and `ERR_INVALID_INPUT` for traversal-shaped paths are unchanged.
65
+ - `validate-access-transformer` resolves its context from a NeoForge/ModDevGradle workspace. 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 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.
66
+ - Error hints no longer ask you to supply a parameter you already supplied. Observed on `validate-access-transformer`; fixed for execution-error guidance generally.
67
+ - Typed-NBT rejections now say what is wrong and what to do about it. `ERR_NBT_INVALID_TYPED_JSON` used to report only "Invalid typed NBT JSON document." with no way to learn which node was at fault. It now carries `fieldErrors[0].path` — the RFC 6901 pointer into the document, or `typedJson` for a root-level failure — plus a `nextAction` naming the expected and received types, 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`.
68
+ - `get-class-source`'s `ERR_CLASS_NOT_FOUND` describes the artifact you asked about. An internal fallback or a nested-jar redirect used to overwrite `details.artifactId`, `error.context.artifactId`, `details.suggestedCall.params.artifactId`, `details.mapping` and `details.qualityFlags` with a different artifact — so a client acting on `mapping` queried the wrong namespace, and a `find-class` retry built from the suggestion searched an index holding neither the class nor its siblings. Those fields now describe the requested artifact; where the lookup ended is reported additively as `details.fallbackArtifactId`, present only when the two differ. A successful fallback still reports the fallback artifact, which is where the source came from.
69
+ - `ERR_CLASS_NOT_FOUND` offers near-miss candidates in the case they were built for. `details.didYouMean` came back empty exactly when the binary fallback had fired, so a typo'd vanilla class name lost its only recovery signal. Candidates are now taken from the requested artifact and unioned with the artifact the lookup ended on, deduplicated by fully-qualified name; a candidate found elsewhere carries an `artifactId` naming where it was found, so an entry without that field always means the artifact you asked about.
70
+ - `get-class-source`'s binary fallback honors `allowDecompile` instead of hardcoding `true`, so passing `allowDecompile: false` no longer costs a full Vineflower pass (measured on a first miss: 22.5 s decompile plus 29.3 s indexing). The cost is unchanged if you did not decline decompilation.
71
+ - `find-class` reports a nested type under its own name. A nested class or enum was reported under its outer fully-qualified name, so searching `Block` returned `net.minecraft.world.level.ClipContext` (the outer class of `ClipContext.Block`) 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 is classified as `partial_coverage` and suggests `get-class-source` when the resolved artifact carries the `partial-source-no-net-minecraft` flag and the query looks like a deobfuscated class name — the one case where the index cannot answer but the binary fallback can.
72
+ - Mapping lookups no longer exhaust the worker heap on a populated Fabric Loom cache. Every `.tiny` file found for a version was read whole into one unbounded index; on a real 1.21.10 cache that meant 13 files totalling 87,744,824 bytes, which crashed the worker and returned `ERR_WORKER_RESTART`. Files are now selected from a header probe, duplicate contents are skipped, and accumulation is bounded by a heap-derived budget overridable with `MCP_LOOM_TINY_MAX_INDEX_ENTRIES`. Measured on that cache: a crash at 86.5 s with 4,256.7 MB peak became a resolved answer at 26.1 s with 2,705.3 MB.
73
+ - Loom mapping lookups no longer report phantom candidates. Merging Tiny v2 files whose namespace order differs re-registered every method a second time under a foreign descriptor, so a method query could return two candidates for one real member, and the phantom could outrank the real record. One measured `resolve-method-mapping-exact` query changed from `ambiguous` with 2 candidates to `resolved` with 1.
74
+ - `subscriptions/listen` is rejected with `-32601` `Method not found` in the modern era, as the contract always documented. A conformant listen request was previously accepted but never answered, which permanently blocked the `validate-project` barrier and every request queued behind it.
75
+ - A cancelled request now releases its slot for every method, not only `validate-project`. `notifications/cancelled` settles the request, clears its deadline, releases the `validate-project` barrier if it held one, and discards a late answer. An `initialize` in flight is excluded.
76
+ - A malformed `initialize` no longer locks the process to the legacy protocol. Any JSON-RPC frame named `initialize` used to commit the one-way era choice before validation, so a bad handshake returned a `-32603` restart error and made every later modern request fail with `era_conflict` until the process was respawned. Such a request is now rejected with `-32602` and `data.kind: "invalid_initialize"` (carrying `data.required[]` and `data.eraSelected: false`), leaving the era unselected and the client free to retry either era.
77
+ - The modern `protocolVersion` value is validated on every request instead of only the one that pins the connection. Previously a later request carrying an unsupported version was served normally — a `tools/call` with `"1999-12-31"` ran its handler — and whether `-32022` fired depended on which method you sent first.
78
+ - A framing violation the reader cannot safely recover from now ends the session with a `supervisor.client_framing_fatal` diagnostic and exit code 1, instead of silently swallowing everything that follows. A `Content-Length` header declaring a body that never arrives previously hung the reader for the process lifetime, and an under-declared length corrupted the next request. Duplicate `Content-Length` headers are rejected rather than resolved last-wins.
79
+ - Line framing recovers after a line-delimited JSON array frame. An array arriving after a `Content-Length` frame was neither consumed as a line nor parsed as a header, so every later frame stayed buffered indefinitely with no error reported. Arrays are now re-dispatched to surface their JSON-RPC schema error; this does not add batch-message support.
80
+ - A `server/discover` sent while the worker is still starting is no longer failed with `-32601` by a later `initialize`.
81
+ - Legacy `tools/call` with a non-object `arguments` returns the v1 error bytes again: a malformed call naming a missing tool no longer returns a successful "not found" envelope, and a registered tool answers `-32603` rather than `-32602`. Modern calls keep `-32602`.
82
+ - Reusing a JSON-RPC id while another request still holds it no longer costs that request its answer. A queue overflow, blocked restart or unknown-tool reply for the reused id used to settle the live request instead, discarding its result and stranding everything queued behind a running `validate-project`.
83
+ - A legacy `initialize` carrying a modern `io.modelcontextprotocol/*` era claim in `params._meta` now completes the legacy handshake instead of failing with a worker restart. Other `_meta` keys pass through unchanged.
84
+ - Version manifest and version-detail fetches abort after `MCP_FETCH_TIMEOUT_MS` with a typed `ERR_REPO_FETCH_FAILED` instead of hanging when a repository stops responding.
85
+ - `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 is still not created as a side effect of inspection.
86
+ - 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 results, and a metrics-recording failure no longer replaces a completed response.
87
+ - The server recovers from a crashed worker instead of going unresponsive. A worker that hit a fatal `uncaughtException` or `unhandledRejection` used to stay alive with recovery bypassed; it now exits so the supervisor can replace it. A restart blocked by stale process-tree cleanup is retried with capped backoff instead of waiting for shutdown.
88
+ - The server no longer leaves orphaned processes behind. A keep-alive timer kept the worker's event loop running after stdin closed, so any launcher that terminated the server without a cooperative shutdown left a resident node process — about 125 MB on the reference host — and each restart added another. The worker now stands down on stdin EOF, and also when the parent process it recorded at startup disappears. Replies still being written after a half-close are delivered in full. A host that immediately closes every child's stdin now backs off exponentially instead of respawning a worker ten times a second, while a genuine crash still gets a prompt replacement. Measured on the reference host over one full test run each way: 20 orphaned workers before the fix, 0 after.
89
+ - Ending the session no longer leaves a worker process behind. `SIGHUP` — what a terminating launcher or a vanishing session sends — was never handled, and an uncaught error in the supervisor went to node's default handler, so either one left the detached worker process group with nobody to collect it. Both now run the ordinary shutdown path, report a `supervisor.fatal` event where applicable, and exit non-zero once the worker group is collected or a bounded watchdog expires.
90
+ - The published package no longer risks shipping internal design documents. `files` listed `docs/**/*.md`, and because an npm `files` allowlist overrides `.gitignore`, a publish from a working tree containing local design notes would have included them. `files` now names the three intended documents explicitly.
91
+ - The automated npm release workflow can no longer publish a prerelease under the `latest` dist-tag: it derives the dist-tag from the package version, so a SemVer prerelease publishes under `rc` and an install without an explicit tag never picks up a release candidate.
92
+
93
+ ### Documentation
94
+
95
+ - `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 branch checks that, and the sentence also fires for a deliberate placeholder-template drop, where nothing was schema-validated. The wording itself is unchanged, because it is part of the frozen legacy wire surface, and now carries a note saying why.
96
+ - `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 may be templates; 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. The `didYouMean` and `resolve-method-mapping-exact` descriptions in the same section are updated for the behavior changes in this release.
97
+
8
98
  ## [6.3.0] - 2026-07-18
9
99
 
10
100
  ### Added
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/@adhisang/minecraft-modding-mcp)](https://www.npmjs.com/package/@adhisang/minecraft-modding-mcp)
4
4
  [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
- [![Node.js >=22](https://img.shields.io/badge/node-%3E%3D22-brightgreen.svg)](https://nodejs.org/)
5
+ [![Node.js >=22.13.0](https://img.shields.io/badge/node-%3E%3D22.13.0-brightgreen.svg)](https://nodejs.org/)
6
6
  [![CI](https://github.com/adhi-jp/minecraft-modding-mcp/actions/workflows/ci.yml/badge.svg)](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
@@ -4,7 +4,8 @@ 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 Database from "./storage/sqlite.js";
7
+ import { downloadSidecarPath, isDownloadSidecarPath } from "./repo-downloader.js";
8
+ import { openDatabase } from "./storage/db.js";
8
9
  import { getProcessWorkspaceContextCache } from "./workspace-context-cache.js";
9
10
  export const PUBLIC_CACHE_KINDS = [
10
11
  "artifact-index",
@@ -222,11 +223,11 @@ function prepareSelector(selector, runtimeInfo) {
222
223
  normalizedProjectPath: normalizePathKey(selector.projectPath, runtimeInfo)
223
224
  };
224
225
  }
225
- function openDb(sqlitePath) {
226
- if (!existsSync(sqlitePath)) {
226
+ function openDb(config) {
227
+ if (!existsSync(config.sqlitePath)) {
227
228
  return undefined;
228
229
  }
229
- return new Database(sqlitePath);
230
+ return openDatabase(config).db;
230
231
  }
231
232
  function candidatePathsForEntry(entry) {
232
233
  const paths = new Set();
@@ -388,7 +389,7 @@ function matchesSelector(entry, selector, runtimeInfo) {
388
389
  return true;
389
390
  }
390
391
  async function artifactIndexEntries(config) {
391
- const db = openDb(config.sqlitePath);
392
+ const db = openDb(config);
392
393
  if (!db) {
393
394
  return [];
394
395
  }
@@ -467,15 +468,28 @@ async function fileBackedEntries(config, cacheKind, detectCorruption) {
467
468
  }
468
469
  const root = kindRoot(config, cacheKind);
469
470
  const files = await listFilesRecursive(root);
470
- return mapWithConcurrencyLimit(files, CACHE_STAT_CONCURRENCY, async (filePath) => {
471
+ // A download sidecar (`<jar>.cache.json`, or the `.<hex>.tmp` leftover of an
472
+ // interrupted write) is the identity record of the jar beside it, not a cached
473
+ // artifact in its own right. Listing one would report a `downloads` entry whose
474
+ // jarPath is a JSON file and would let a jarPath selector delete a description
475
+ // instead of the thing described. The finished record's bytes are folded into
476
+ // the jar's entry below, so a listed entry weighs everything that belongs to
477
+ // it; bytes that describe nothing - an orphan record, a half-written one - are
478
+ // deliberately unaccounted, because there is no entry for them to belong to.
479
+ // Only this kind names files that way; every other kind keeps every file.
480
+ const entryFiles = cacheKind === "downloads"
481
+ ? files.filter((filePath) => !isDownloadSidecarPath(filePath))
482
+ : files;
483
+ return mapWithConcurrencyLimit(entryFiles, CACHE_STAT_CONCURRENCY, async (filePath) => {
471
484
  const fileStat = await stat(filePath);
485
+ const sidecarBytes = cacheKind === "downloads" ? await downloadSidecarSizeBytes(filePath) : 0;
472
486
  const normalizedEntryId = filePath.slice(root.length + 1);
473
487
  const inferredScope = inferScope(filePath, normalizedEntryId) ?? (cacheKind === "decompiled-source" ? "vanilla" : undefined);
474
488
  return {
475
489
  cacheKind,
476
490
  entryId: normalizedEntryId,
477
491
  path: filePath,
478
- sizeBytes: fileStat.size,
492
+ sizeBytes: fileStat.size + sidecarBytes,
479
493
  status: "healthy",
480
494
  meta: {
481
495
  updatedAt: fileStat.mtime.toISOString(),
@@ -483,6 +497,8 @@ async function fileBackedEntries(config, cacheKind, detectCorruption) {
483
497
  mapping: inferMapping(filePath, normalizedEntryId),
484
498
  scope: inferredScope,
485
499
  projectPath: inferProjectPath(filePath, config.pathRuntimeInfo),
500
+ // Health follows the cached artifact's own bytes: a zero-byte jar stays
501
+ // partial no matter how much its sidecar weighs.
486
502
  partial: fileStat.size === 0,
487
503
  corrupt: cacheKind === "registry" && detectCorruption ? await isCorruptRegistryJson(filePath) : false,
488
504
  inUse: filePath.endsWith(".lock") ||
@@ -495,6 +511,19 @@ async function fileBackedEntries(config, cacheKind, detectCorruption) {
495
511
  };
496
512
  });
497
513
  }
514
+ /**
515
+ * Bytes of the sidecar describing `downloadPath`, or 0 when there is none.
516
+ * A download cached before sidecars existed, or one whose sidecar write failed,
517
+ * is a normal state and must not fail the inventory.
518
+ */
519
+ async function downloadSidecarSizeBytes(downloadPath) {
520
+ try {
521
+ return (await stat(downloadSidecarPath(downloadPath))).size;
522
+ }
523
+ catch {
524
+ return 0;
525
+ }
526
+ }
498
527
  /**
499
528
  * Binary-remap cache entries are keyed by the final artifact id even when the
500
529
  * on-disk entry is a corrupt final directory or a leftover temp path.
@@ -650,7 +679,7 @@ export function createCacheRegistry(config) {
650
679
  const entries = await collectEntries(input.cacheKinds, input.selector);
651
680
  const selectedBytes = entries.reduce((total, entry) => total + entry.sizeBytes, 0);
652
681
  if (input.executionMode === "apply") {
653
- const db = openDb(config.sqlitePath);
682
+ const db = openDb(config);
654
683
  try {
655
684
  for (const entry of entries) {
656
685
  if (entry.cacheKind === "artifact-index") {
@@ -661,6 +690,14 @@ export function createCacheRegistry(config) {
661
690
  workspaceCache.invalidate(entry.entryId);
662
691
  continue;
663
692
  }
693
+ if (entry.cacheKind === "downloads") {
694
+ // The sidecar is part of this entry, so it goes with the jar —
695
+ // outside the existsSync guard below, so a jar that vanished
696
+ // out-of-band since the listing still takes its sidecar with it
697
+ // instead of leaving an orphan behind. `force` makes a missing
698
+ // sidecar a no-op.
699
+ await rm(downloadSidecarPath(entry.path), { force: true });
700
+ }
664
701
  if (existsSync(entry.path)) {
665
702
  // Only binary-remap inventory can return directories as entries;
666
703
  // other file-backed kinds keep their existing file-only contract.
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; keep the process alive
16
- // until the parent explicitly closes stdin or sends a signal.
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
- process.once("exit", () => clearInterval(keepAliveTimer));
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/sdk/types.js";
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) {