@adhisang/minecraft-modding-mcp 7.0.0-rc.0 → 7.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +77 -49
- package/dist/cache-registry.js +39 -2
- package/dist/entry-tools/manage-cache-service.d.ts +2 -2
- package/dist/entry-tools/validate-project/cases/mixin.js +26 -6
- package/dist/entry-tools/validate-project/cases/project-summary.d.ts +7 -7
- package/dist/entry-tools/validate-project-service.d.ts +2 -2
- package/dist/entry-tools/verify-mixin-target-service.js +19 -1
- package/dist/error-mapping.d.ts +76 -0
- package/dist/error-mapping.js +107 -6
- package/dist/maven-resolver.d.ts +18 -0
- package/dist/maven-resolver.js +20 -0
- package/dist/mcp-helpers.js +10 -3
- package/dist/repo-downloader.d.ts +165 -0
- package/dist/repo-downloader.js +568 -10
- package/dist/source/artifact-resolver.d.ts +12 -1
- package/dist/source/artifact-resolver.js +12 -1
- package/dist/source/class-source.d.ts +15 -0
- package/dist/source/class-source.js +97 -10
- package/dist/source-resolver.js +224 -57
- package/dist/source-service.d.ts +1 -1
- package/dist/tool-guidance.js +14 -7
- package/dist/tool-schemas.d.ts +2 -2
- package/docs/tool-reference.md +2 -0
- package/package.json +6 -4
package/CHANGELOG.md
CHANGED
|
@@ -7,65 +7,93 @@ and this project aims to follow [Semantic Versioning](https://semver.org/spec/v2
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
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
|
+
|
|
10
36
|
## [7.0.0-rc.0] - 2026-08-22
|
|
11
37
|
|
|
12
38
|
### Added
|
|
13
39
|
|
|
14
|
-
- MCP protocol revision `2026-07-28
|
|
15
|
-
-
|
|
16
|
-
- `MCP_MAX_FRAME_BYTES` bounds the JSON-RPC frame size
|
|
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.
|
|
17
43
|
|
|
18
44
|
### Changed
|
|
19
45
|
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
22
|
-
-
|
|
23
|
-
- **
|
|
24
|
-
- **Behavior change
|
|
25
|
-
- **Behavior change.** `
|
|
26
|
-
- **Behavior change — a
|
|
27
|
-
- **
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
- **
|
|
31
|
-
-
|
|
32
|
-
- `
|
|
33
|
-
-
|
|
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`).
|
|
34
60
|
|
|
35
61
|
### Fixed
|
|
36
62
|
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
-
|
|
41
|
-
-
|
|
42
|
-
- An error
|
|
43
|
-
-
|
|
44
|
-
- `
|
|
45
|
-
- `
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
- `
|
|
49
|
-
-
|
|
50
|
-
- `
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
-
|
|
54
|
-
-
|
|
55
|
-
-
|
|
56
|
-
-
|
|
57
|
-
-
|
|
58
|
-
-
|
|
59
|
-
-
|
|
60
|
-
-
|
|
61
|
-
-
|
|
62
|
-
-
|
|
63
|
-
- `
|
|
64
|
-
-
|
|
65
|
-
- The
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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.
|
|
69
97
|
|
|
70
98
|
## [6.3.0] - 2026-07-18
|
|
71
99
|
|
package/dist/cache-registry.js
CHANGED
|
@@ -4,6 +4,7 @@ import { join, resolve } from "node:path";
|
|
|
4
4
|
import { mapWithConcurrencyLimit } from "./concurrency.js";
|
|
5
5
|
import { createError, ERROR_CODES } from "./errors.js";
|
|
6
6
|
import { normalizeOptionalPathForHost } from "./path-converter.js";
|
|
7
|
+
import { downloadSidecarPath, isDownloadSidecarPath } from "./repo-downloader.js";
|
|
7
8
|
import { openDatabase } from "./storage/db.js";
|
|
8
9
|
import { getProcessWorkspaceContextCache } from "./workspace-context-cache.js";
|
|
9
10
|
export const PUBLIC_CACHE_KINDS = [
|
|
@@ -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
|
-
|
|
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.
|
|
@@ -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.
|
|
@@ -27,9 +27,9 @@ export declare const manageCacheShape: {
|
|
|
27
27
|
jarPath: z.ZodOptional<z.ZodString>;
|
|
28
28
|
entryId: z.ZodOptional<z.ZodString>;
|
|
29
29
|
status: z.ZodOptional<z.ZodEnum<{
|
|
30
|
+
stale: "stale";
|
|
30
31
|
healthy: "healthy";
|
|
31
32
|
partial: "partial";
|
|
32
|
-
stale: "stale";
|
|
33
33
|
orphaned: "orphaned";
|
|
34
34
|
corrupt: "corrupt";
|
|
35
35
|
in_use: "in_use";
|
|
@@ -81,9 +81,9 @@ export declare const manageCacheSchema: z.ZodObject<{
|
|
|
81
81
|
jarPath: z.ZodOptional<z.ZodString>;
|
|
82
82
|
entryId: z.ZodOptional<z.ZodString>;
|
|
83
83
|
status: z.ZodOptional<z.ZodEnum<{
|
|
84
|
+
stale: "stale";
|
|
84
85
|
healthy: "healthy";
|
|
85
86
|
partial: "partial";
|
|
86
|
-
stale: "stale";
|
|
87
87
|
orphaned: "orphaned";
|
|
88
88
|
corrupt: "corrupt";
|
|
89
89
|
in_use: "in_use";
|
|
@@ -21,14 +21,34 @@ export async function handleMixin(deps, input, detail, include) {
|
|
|
21
21
|
details: {
|
|
22
22
|
task: "mixin",
|
|
23
23
|
failedStage: "input-validation",
|
|
24
|
-
nextAction: "Pass version explicitly
|
|
24
|
+
nextAction: "Pass version explicitly: the Minecraft version this project targets. task=\"project-summary\" supports preferProjectVersion for auto-detection from gradle.properties, but direct task=\"mixin\" requires an explicit version. Call the suggested list-versions to see what is available, then replay the exampleCalls template with that version substituted.",
|
|
25
|
+
// No concrete version can be derived here -- the caller omitted it and this
|
|
26
|
+
// task does not auto-detect one. A `suggestedCall` is a payload the caller
|
|
27
|
+
// may replay verbatim, so filling the hole with a made-up version produced
|
|
28
|
+
// a call that RUNS and validates the mixin against the wrong Minecraft
|
|
29
|
+
// version, which is worse than no suggestion.
|
|
30
|
+
//
|
|
31
|
+
// So the two roles are split, following what the sibling "a version is
|
|
32
|
+
// required but none was resolved" site already does in
|
|
33
|
+
// src/source/class-source.ts: `suggestedCall` is a REAL next step that
|
|
34
|
+
// needs nothing the caller does not have (list-versions takes no
|
|
35
|
+
// arguments and answers exactly the question blocking them), while the
|
|
36
|
+
// task="mixin" retry shape travels as an `exampleCalls` template whose
|
|
37
|
+
// placeholder makes the substitution the caller must perform obvious.
|
|
38
|
+
...buildSuggestedCall({ tool: "list-versions", params: {} }),
|
|
25
39
|
...buildSuggestedCall({
|
|
26
40
|
tool: "validate-project",
|
|
27
|
-
params:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
41
|
+
params: undefined,
|
|
42
|
+
examples: [
|
|
43
|
+
{
|
|
44
|
+
params: {
|
|
45
|
+
task: "mixin",
|
|
46
|
+
subject: input.subject,
|
|
47
|
+
version: "<your-mc-version>"
|
|
48
|
+
},
|
|
49
|
+
reason: "Replace <your-mc-version> with the Minecraft version this project targets (gradle.properties, or task=\"project-summary\" with preferProjectVersion=true reports it)."
|
|
50
|
+
}
|
|
51
|
+
]
|
|
32
52
|
})
|
|
33
53
|
}
|
|
34
54
|
});
|
|
@@ -9,7 +9,7 @@ export declare function handleProjectSummary(deps: ValidateProjectDeps, input: V
|
|
|
9
9
|
warnings: string[];
|
|
10
10
|
tasks?: {
|
|
11
11
|
"workspace.detected": {
|
|
12
|
-
status: "
|
|
12
|
+
status: "ok" | "error" | "missing" | "skipped";
|
|
13
13
|
durationMs?: number;
|
|
14
14
|
error?: {
|
|
15
15
|
code: string;
|
|
@@ -20,7 +20,7 @@ export declare function handleProjectSummary(deps: ValidateProjectDeps, input: V
|
|
|
20
20
|
evidence?: string[];
|
|
21
21
|
};
|
|
22
22
|
"gradle.readable": {
|
|
23
|
-
status: "
|
|
23
|
+
status: "ok" | "error" | "missing" | "skipped";
|
|
24
24
|
durationMs?: number;
|
|
25
25
|
error?: {
|
|
26
26
|
code: string;
|
|
@@ -32,7 +32,7 @@ export declare function handleProjectSummary(deps: ValidateProjectDeps, input: V
|
|
|
32
32
|
buildScripts?: string[];
|
|
33
33
|
};
|
|
34
34
|
"loom.cache.found": {
|
|
35
|
-
status: "
|
|
35
|
+
status: "ok" | "error" | "missing" | "skipped";
|
|
36
36
|
durationMs?: number;
|
|
37
37
|
error?: {
|
|
38
38
|
code: string;
|
|
@@ -43,7 +43,7 @@ export declare function handleProjectSummary(deps: ValidateProjectDeps, input: V
|
|
|
43
43
|
cachePath?: string;
|
|
44
44
|
};
|
|
45
45
|
"minecraft.artifact.resolved": {
|
|
46
|
-
status: "
|
|
46
|
+
status: "ok" | "error" | "missing" | "skipped";
|
|
47
47
|
durationMs?: number;
|
|
48
48
|
error?: {
|
|
49
49
|
code: string;
|
|
@@ -55,7 +55,7 @@ export declare function handleProjectSummary(deps: ValidateProjectDeps, input: V
|
|
|
55
55
|
mapping?: import("../../../types.js").SourceMapping;
|
|
56
56
|
};
|
|
57
57
|
"mixins.validated": {
|
|
58
|
-
status: "
|
|
58
|
+
status: "ok" | "error" | "missing" | "skipped";
|
|
59
59
|
durationMs?: number;
|
|
60
60
|
error?: {
|
|
61
61
|
code: string;
|
|
@@ -70,7 +70,7 @@ export declare function handleProjectSummary(deps: ValidateProjectDeps, input: V
|
|
|
70
70
|
};
|
|
71
71
|
};
|
|
72
72
|
"accessWideners.validated": {
|
|
73
|
-
status: "
|
|
73
|
+
status: "ok" | "error" | "missing" | "skipped";
|
|
74
74
|
durationMs?: number;
|
|
75
75
|
error?: {
|
|
76
76
|
code: string;
|
|
@@ -84,7 +84,7 @@ export declare function handleProjectSummary(deps: ValidateProjectDeps, input: V
|
|
|
84
84
|
};
|
|
85
85
|
};
|
|
86
86
|
"accessTransformers.validated": {
|
|
87
|
-
status: "
|
|
87
|
+
status: "ok" | "error" | "missing" | "skipped";
|
|
88
88
|
durationMs?: number;
|
|
89
89
|
error?: {
|
|
90
90
|
code: string;
|