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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +21 -5
  3. package/dist/cache-policy.d.ts +71 -0
  4. package/dist/cache-policy.js +83 -0
  5. package/dist/cache-registry.js +6 -6
  6. package/dist/cli.js +74 -3
  7. package/dist/compat-stdio-transport.d.ts +1 -1
  8. package/dist/compat-stdio-transport.js +13 -1
  9. package/dist/config.d.ts +3 -0
  10. package/dist/config.js +8 -2
  11. package/dist/decompiler/vineflower.d.ts +1 -0
  12. package/dist/decompiler/vineflower.js +8 -5
  13. package/dist/entry-tools/analyze-mod-service.d.ts +70 -136
  14. package/dist/entry-tools/analyze-symbol-service.d.ts +112 -150
  15. package/dist/entry-tools/compare-minecraft-service.d.ts +59 -145
  16. package/dist/entry-tools/entry-tool-schema.d.ts +38 -4
  17. package/dist/entry-tools/entry-tool-schema.js +4 -1
  18. package/dist/entry-tools/inspect-minecraft/internal.d.ts +235 -799
  19. package/dist/entry-tools/inspect-minecraft/internal.js +65 -28
  20. package/dist/entry-tools/inspect-minecraft-service.d.ts +372 -1736
  21. package/dist/entry-tools/inspect-minecraft-service.js +1 -1
  22. package/dist/entry-tools/manage-cache-service.d.ts +81 -91
  23. package/dist/entry-tools/validate-project/cases/project-summary.d.ts +7 -7
  24. package/dist/entry-tools/validate-project-service.d.ts +164 -592
  25. package/dist/entry-tools/verify-mixin-target-service.d.ts +3 -19
  26. package/dist/era-classifier.d.ts +161 -0
  27. package/dist/era-classifier.js +292 -0
  28. package/dist/error-mapping.js +9 -2
  29. package/dist/index.d.ts +42 -4
  30. package/dist/index.js +637 -475
  31. package/dist/java-process.d.ts +2 -0
  32. package/dist/java-process.js +22 -2
  33. package/dist/json-rpc-framing.d.ts +77 -1
  34. package/dist/json-rpc-framing.js +249 -13
  35. package/dist/mapping/loaders/tiny-loom-selection.d.ts +88 -0
  36. package/dist/mapping/loaders/tiny-loom-selection.js +223 -0
  37. package/dist/mapping/loaders/tiny-loom.js +45 -33
  38. package/dist/mapping/loaders/tiny-maven.js +6 -11
  39. package/dist/mapping/parsers/tiny.d.ts +57 -0
  40. package/dist/mapping/parsers/tiny.js +99 -22
  41. package/dist/mapping-service.d.ts +19 -0
  42. package/dist/mapping-service.js +93 -9
  43. package/dist/mcp-helpers.d.ts +19 -2
  44. package/dist/mcp-helpers.js +48 -6
  45. package/dist/minecraft-explorer-service.d.ts +1 -1
  46. package/dist/mixin/types.d.ts +8 -0
  47. package/dist/mod-analyzer.js +7 -7
  48. package/dist/mod-decompile-service.js +1 -0
  49. package/dist/nbt/java-nbt-codec.js +12 -2
  50. package/dist/nbt/json-patch.js +14 -3
  51. package/dist/nbt/pipeline.js +40 -3
  52. package/dist/nbt/typed-json.js +26 -1
  53. package/dist/registration-adapter.d.ts +32 -0
  54. package/dist/registration-adapter.js +52 -0
  55. package/dist/request-context.d.ts +7 -0
  56. package/dist/request-context.js +9 -0
  57. package/dist/resources.d.ts +1 -1
  58. package/dist/resources.js +25 -19
  59. package/dist/server-identity.d.ts +27 -0
  60. package/dist/server-identity.js +26 -0
  61. package/dist/source/access-validate.js +53 -0
  62. package/dist/source/artifact-resolver.d.ts +69 -1
  63. package/dist/source/artifact-resolver.js +215 -14
  64. package/dist/source/class-source.d.ts +22 -0
  65. package/dist/source/class-source.js +162 -29
  66. package/dist/source/did-you-mean.d.ts +12 -1
  67. package/dist/source/did-you-mean.js +6 -2
  68. package/dist/source/file-access.js +150 -46
  69. package/dist/source/indexer.js +1 -0
  70. package/dist/source/nested-jars.d.ts +19 -0
  71. package/dist/source/nested-jars.js +90 -21
  72. package/dist/source/shared-utils.d.ts +21 -0
  73. package/dist/source/shared-utils.js +23 -0
  74. package/dist/source-service.d.ts +12 -0
  75. package/dist/source-service.js +3 -0
  76. package/dist/stdio-supervisor.d.ts +357 -2
  77. package/dist/stdio-supervisor.js +1031 -80
  78. package/dist/storage/db.d.ts +2 -1
  79. package/dist/storage/db.js +15 -8
  80. package/dist/synthetic-decorator.d.ts +24 -0
  81. package/dist/synthetic-decorator.js +48 -0
  82. package/dist/tool-contract-manifest.js +1 -1
  83. package/dist/tool-guidance.d.ts +17 -1
  84. package/dist/tool-guidance.js +417 -13
  85. package/dist/tool-schema-registry.d.ts +2 -0
  86. package/dist/tool-schema-registry.js +4 -0
  87. package/dist/tool-schemas.d.ts +2214 -3915
  88. package/dist/tool-schemas.js +34 -7
  89. package/dist/types.d.ts +35 -0
  90. package/dist/v1-parity-schemas.d.ts +7 -0
  91. package/dist/v1-parity-schemas.js +5584 -0
  92. package/dist/version-diff-service.d.ts +33 -0
  93. package/dist/version-diff-service.js +148 -3
  94. package/dist/version-service.js +36 -14
  95. package/dist/warning-details.js +18 -1
  96. package/docs/README-ja.md +9 -3
  97. package/docs/tool-reference.md +267 -22
  98. package/package.json +12 -9
package/CHANGELOG.md CHANGED
@@ -7,6 +7,77 @@ 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.0] - 2026-08-22
11
+
12
+ ### Added
13
+
14
+ - MCP protocol revision `2026-07-28` (modern stateless era), served alongside the legacy initialize protocol in one process: `server/discover` returns `supportedVersions: ["2026-07-28"]`, capabilities, and the server identity; modern requests carry per-request `io.modelcontextprotocol/*` `_meta` (protocol version, client capabilities, optional client info) that survives the supervisor/worker boundary, queueing, and worker restarts; every modern result — including supervisor-synthesized overflow/restart/timeout replies — carries `resultType: "complete"` and the server-identity `_meta` echo; the cacheable methods (`tools/list`, `resources/list`, `resources/read`, `resources/templates/list`, `server/discover`) return `ttlMs`/`cacheScope` per the adopted private-cache policy; unsupported modern protocol versions answer `-32022` with `data.supported`/`data.requested`. Era selection is a supervisor-owned one-way lock with machine-readable rejections (`data.kind: "era_conflict"` / `"missing_meta"`); modern worker restarts never replay `initialize`. Contract details: `docs/tool-reference.md` → MCP Protocol Support. Verification: dual-era wire suites (`tests/stdio/stdio-supervisor-era-state.test.ts`, `stdio-supervisor-era-wire.test.ts`, `stdio-supervisor-era-lifecycle.test.ts`, `stdio-supervisor-era-context.test.ts`) plus modern-surface suites (`stdio-modern-*.test.ts`) in the full green suite.
15
+ - SQLite and decompiler tuning knobs: `MCP_SQLITE_CACHE_KB` (page cache in KiB, default `8000`) and `MCP_SQLITE_MMAP_SIZE` (bytes, default `268435456`, `0` disables) tune the artifact-index database, which now also uses `temp_store=MEMORY`; `MCP_DECOMPILE_MAX_MEMORY_MB` (default `4096`) caps the Vineflower JVM heap the same way `MCP_REMAP_MAX_MEMORY_MB` caps remapping. Defaults preserve existing behavior on typical installations.
16
+ - `MCP_MAX_FRAME_BYTES` bounds the JSON-RPC frame size accepted by the stdio supervisor and worker transport (default 64 MiB, clamped to at least 1 MiB). An oversized or `Content-Length`-abusive frame is rejected with a diagnostic naming the observed size and the configured limit, header sections are capped at 8 KiB, and line-delimited frames obey the same limit. A framing violation the reader cannot provably resynchronize past is FATAL: the session is terminated with a `supervisor.client_framing_fatal` diagnostic and exit code 1 rather than silently consuming later frames.
17
+
18
+ ### Changed
19
+
20
+ - **Behavior change.** `get-class-members` (and `batch-class-members`) now inherit the resolved artifact's mapping when the call supplies no explicit `mapping`, matching what `get-class-source` already did. Previously an artifact resolved as mojang answered member reads in the OBFUSCATED namespace — `ownerFqn: "dlp"` with fields `e, f, g` — which silently broke the documented `find-class` → `get-class-source` → `get-class-members` flow; the two tools returned different namespaces for the same artifact and the same absent argument. `context.mappingNamespace` and `minecraftVersion` no longer contradict `returnedNamespace`. An explicit `mapping` is still never overridden. Note the `get-class-members.mapping` schema description still reads "default obfuscated"; the advertised `inputSchema` bytes are pinned for legacy wire parity, so the artifact-target qualifier is documented in `docs/tool-reference.md` instead.
21
+ - **Behavior change — result counts differ.** `compare-versions` now lifts both jars' class lists into the mojang namespace before diffing, instead of diffing raw obfuscated jar entries. Obfuscated names are not stable across Minecraft versions, so the previous diff compared unrelated symbols: on the measured 1.21.10→1.21.11 pair the unfiltered result moved from `added 236 / removed 0 / unchanged 6386` to `added 616 / removed 380 / unchanged 6006`. Results now carry `classes.namespace` and `classes.packageFilter { matchedFrom, matchedTo }`; a filter that matches nothing emits an explicit warning instead of returning a zeroed result that reads as "no changes"; and an unavailable mapping degrades to the obfuscated namespace with a loud warning rather than silently.
22
+ - **Behavior change — a previously successful call now fails closed.** In `validate-access-widener` and `validate-access-transformer`, a `scope: "merged"` runtime resolution that would serve a jar from a DIFFERENT loader than the workspace is now refused with `ERR_CONTEXT_UNRESOLVED` instead of being answered. The refusal sits on those two tools' runtime-evidence guard — the only path that carries the loader mismatch — so `resolve-artifact` and `get-class-source` resolve exactly as before. Previously the fallback could serve a NeoForge 1.21.10 jar for a Fabric 1.21.11 request, and `validate-access-widener` then reported `valid: true` for a Fabric access widener validated against that NeoForge jar — a false PASS. A same-loader version drift is still answered, now marked `approximate: true`. Fallback policy is otherwise unchanged.
23
+ - **Behavior change — a different artifact is resolved.** `inspect-minecraft` now hands a workspace subject to artifact resolution as `target.kind="workspace"`, exactly as `resolve-artifact` handles the same directory, instead of reading `minecraft_version` itself and resolving `target.kind="version"` with no mapping. The old path left `mapping` undefined, which normalizes to `obfuscated`; that flips the remap gate, changes `mappingVariant`, and therefore changes the hash the artifact id is built from, and it also skips Loom source-jar discovery — so `inspect-minecraft` and `resolve-artifact` returned DIFFERENT artifacts for the same project directory. The visible symptom: `task="class-members"` looked a mojang FQCN up verbatim in an obfuscated jar and failed, while `batch-class-members` — which forwards the workspace target untouched — succeeded on the same class. Workspace resolution now picks up the project's detected compile mapping and loader scope, so a call that previously resolved an obfuscated vanilla artifact can now resolve the merged, mojang-mapped one and report a different `artifactId`. An explicit `subject.mapping` / `subject.scope` is still never overridden, and `WORKSPACE_TARGET_OFF=1` keeps the previous routing.
24
+ - **Behavior change — a response field reports a different set.** `resolve-method-mapping-exact` now returns the strict candidates the verdict was actually computed from on `status: "resolved"` and `status: "ambiguous"` — the two verdicts the strict subset decides — instead of the wider name-matched list. `status: "not_found"` and `status: "mapping_unavailable"` deliberately keep reporting that wider name-matched list: no strict subset produced either verdict, and on a miss the near misses are the useful content. The verdict has always been decided by the strict subset, but the response was built from the raw lookup, so a caller could be shown a `confidence: 1, matchKind: "exact"` candidate — on another owner, under another descriptor — sitting beside `status: "ambiguous"` with nothing to mark it as one the tool had already rejected. `candidates` and `candidateCount` therefore shrink on any query where the simple-name index contributed extra matches. On `status: "resolved"` the reported set is the single match, which makes `candidates` redundant with `resolvedSymbol`, so the default response projection omits it and returns `resolvedSymbol` with `candidateCount: 1`. Rejected candidates are accounted for by a warning on the ambiguous verdict that reports the counts BY REASON — how many were rejected for their owner and how many for their descriptor — because a single cause would name the wrong one; a `resolved` answer whose candidate count shrank carries no such accounting, and `find-mapping` still returns the unfiltered list. The ambiguous result also populates `ambiguityReasons[]`, which the type declared but this path never set; because `resolve-workspace-symbol` spreads this result for `kind: "method"`, that field now appears on its method-ambiguous responses too, and it is present only on `status: "ambiguous"` where it always holds at least one reason. No short-circuit was added: the tool still refuses to pick a winner, which is the whole point of the strict variant.
25
+ - **Behavior change.** `resolve-method-mapping-exact` is now OWNER-strict: its strict filter requires the query's full advertised triple `owner + name + descriptor`, where it previously checked the descriptor alone. The tool takes a REQUIRED `owner` and sells itself as the strict variant of `find-mapping`, but the member index also keys every method under an OWNERLESS `<name><descriptor>` key, so a same-signature method on an unrelated class was pulled in at `matchKind: "simple-name"`, survived the descriptor filter, and forced `status: "ambiguous"`. On a two-owner fixture where the queried class declares exactly one matching method, the result moves from `ambiguous` with 2 candidates — one of them `matchKind: "exact", confidence: 1` — to `resolved` naming that one method. The owner is compared after being projected along the same mapping path the candidates travelled, since the query owner arrives in the source namespace while candidate owners are declaring classes in the target namespace. The accepted cost: a method the owner INHERITS rather than declares moves from `ambiguous` to `not_found`, because the mapping formats record declarations and carry no class hierarchy. That case is not silent — the result carries a warning naming the queried owner, the classes that do declare the member, and `find-mapping` as the owner-agnostic lookup. Two methods on the SAME owner remain `ambiguous`.
26
+ - **Behavior change — a different error code.** When a workspace subject's Minecraft version cannot be detected, `inspect-minecraft` now fails with `ERR_WORKSPACE_VERSION_UNRESOLVED`, which names the `projectPath` and carries `nextAction` plus a `suggestedCall`. Previously the tool degraded to an empty `artifactId` and the failure surfaced further down as `ERR_INVALID_INPUT` "Either artifactId or target must be provided." — an error that described neither the cause nor the fix. `task="class-overview"` and `task="file"`, which answered this case with a `blocked` summary, now surface the typed error too.
27
+ - **Wire contract change — both eras.** Advertised capabilities now carry `resources: { listChanged: false }` and `tools: { listChanged: false }`, where both were previously `true`. This changes the observable bytes of the legacy `initialize` result as well as the modern `server/discover` result; the key order (`resources` before `tools`) and the rest of the payload are unchanged, and no prompts, subscriptions, or logging capability is advertised. The `true` was the SDK's default for any registered tool or resource surface and was never true of this server: the tool and resource surface is fixed at process start by environment flags, `notifications/tools/list_changed` and `notifications/resources/list_changed` are never emitted, and `subscriptions/listen` answers `-32601` in both eras — so the flag advertised a stream no client could subscribe to and that would never have carried anything. A client that keyed list invalidation to the advertised flag should treat both lists as static for the lifetime of the process and re-read them only across a restart. `buildServer()` now passes an explicit `capabilities` option to override the SDK default; the suppression is deliberate and is NOT era-gated, so the two eras cannot disagree. Contract details: `docs/tool-reference.md` → MCP Protocol Support. Pinned by `tests/stdio/stdio-modern-discover-contents.test.ts` (modern `server/discover`) and `tests/stdio/stdio-dependency-method-inventory.test.ts` (legacy `initialize`, over the real wire).
28
+ - Runtime migrated from `@modelcontextprotocol/sdk` 1.27.1 (SDK v1) with Zod 3 to `@modelcontextprotocol/server` 2.0.0 / `@modelcontextprotocol/client` 2.0.0 (SDK v2) with Zod 4.4.3, both exact-pinned. Legacy clients keep byte-compatible wire behavior: advertised `inputSchema` bytes are pinned to the pre-migration snapshots (`src/v1-parity-schemas.ts`, `tests/fixtures/premigration/tool-contracts/`), `runTool()` remains the sole tool-argument validator through an identity registration adapter (`src/registration-adapter.ts`, no SDK-private patching — guarded by `tests/contracts/no-sdk-private-request-handler-access.test.ts`), Zod error bytes keep zod3 parity (`tests/utils/zod3-parity.test.ts`), and initialization capture/replay across worker restarts is unchanged. Five recorded legacy exceptions: (1) `tools/call` with omitted `arguments` reaches the application validator as `{}` — input-free tools succeed, required-field schemas answer per-field `ERR_INVALID_INPUT`, and a `{}`-accepting schema with an input-requiring handler answers that tool's handler-level ProblemDetails (`json-to-nbt` → `ERR_NBT_INVALID_TYPED_JSON`); (2) legacy `tools/list` entries omit the v1-only `execution: {"taskSupport":"forbidden"}` field (SDK v2 does not emit it; all other advertised fields identical); (3) unknown/disabled-tool replies are synthesized at the supervisor pre-queue with identical bytes but no queue-slot consumption, so reply ordering and overflow outcomes can differ from v1 under concurrent load; (4) the unmatched-resource-URI error keeps code `-32602` but its message changed from `MCP error -32602: Resource <uri> not found` to `Resource not found: <uri>` with a `data.uri` field (SDK v2 wording); (5) the `initialize` result advertises `resources: { listChanged: false }` and `tools: { listChanged: false }`, where the pre-migration server advertised `true` for both — the suppression is deliberate, and it is not era-gated because both eras read one `getCapabilities()` and a per-era split would leave one process advertising two contracts.
29
+ - Modern-era `tools/list` returns raw tool-name-ascending order; legacy processes keep the frozen pre-migration registration order (goldens: `tests/fixtures/premigration/tools-list-order.*.json`).
30
+ - **Breaking (Node package surface).** The exported `server` is now an SDK v2 `McpServer` with a different API surface than the v1 instance it replaces, `buildServer` takes an optional `McpRequestContext` and branches on `ctx?.era`, and the emitted `dist/index.d.ts` resolves Zod 4 types. Consumers importing this package as a library — rather than speaking to it over stdio — must migrate; the byte-compatibility guarantee above covers the WIRE surface only.
31
+ - **Breaking (minimum runtime).** `engines.node` is raised from `>=22` to `>=22.13.0`. `src/storage/sqlite.ts` imports `node:sqlite` unguarded at module load, on the static import path the `dist/cli.js` bin reaches before it serves anything, and `src/storage/symbols-repo.ts` calls `StatementSync.iterate`. **22.13.0** is the first release where either is usable unflagged: `node:sqlite` landed in 22.5.0 but stayed behind `--experimental-sqlite` until 22.13.0 unflagged it, and 22.13.0 is also where `StatementSync.prototype.iterate` was added. Nothing in this package passes that flag — the bin is a plain `#!/usr/bin/env node` — so on 22.0–22.12 the unguarded import throws at process start and the server never reaches a request, rather than degrading; on 22.13.0 and later both the module and the iterator are present. The previous `>=22` floor understated the real requirement.
32
+ - `provenance.mappingArtifact` for Loom-sourced mappings now names the file actually merged rather than the alphabetically first discovered file (for the measured 1.21.10 cache, `.../mappings-mojang.tiny` instead of `intermediary-v2.tiny`).
33
+ - Modern-era `resources/read` for a URI that matches a registered template but has no backing artifact now answers JSON-RPC `-32602` (the revision requires an error for a resource that does not exist) instead of a successful `resultType: "complete"` carrying a ProblemDetails document. Applies to the five 404-class AppError codes. Legacy replies keep the ProblemDetails envelope and its cache-field absence.
34
+
35
+ ### Fixed
36
+
37
+ - The published tarball no longer risks shipping internal design documents. `files` listed `docs/**/*.md`, and because an npm `files` allowlist overrides `.gitignore`, a publish run from a working tree that contained `docs/plans/`, `docs/specs/`, or `docs/reports/` would have included them — 19 files and about 840 kB on the tree this was measured from, among them the implementation plan for this very release. The CI publish path was never exposed (it publishes from a clean `actions/checkout`), but a local `npm publish` was. `files` now names the three intended documents explicitly, so a newly added internal document cannot leak by default.
38
+ - The npm release path can no longer publish a prerelease under the `latest` dist-tag. The publish workflow derives the dist-tag from the package version — a SemVer prerelease publishes under `rc`, anything else under `latest` — fails before publishing when the pushed git tag and the package version disagree, and refuses to run when the frozen named-test set gate's escape hatch is present in the environment.
39
+ - Runtime provenance no longer claims a version or loader that was not served. `provenance` now describes the jar actually used — `version`, `requestedVersion`, `versionApproximated`, `servedLoader`, `expectedLoader`, `loaderMismatch` — where it previously echoed the REQUESTED version back to the caller, so a substituted artifact was indistinguishable from an exact match. `resolve-artifact` gains `provenance.versionApproximation` and corrects `resolvedFrom.version`.
40
+ - `get-artifact-file` serves root-level and `META-INF/**` jar entries. Delivery was gated on an `assets/`/`data/` prefix with no safety purpose, so `fabric.mod.json`, `META-INF/MANIFEST.MF`, `<mod>.mixins.json`, and license files all answered `ERR_FILE_NOT_FOUND` despite being present in the jar — precisely the files an agent inspecting a mod reads first. Extension-less entries are now classified by sniffing for valid UTF-8 rather than assumed binary. The 512 KiB cap, the `truncated` flag, the `contentOmittedReason` for binary entries, and `ERR_INVALID_INPUT` for traversal-shaped paths are unchanged.
41
+ - `validate-access-transformer` resolves its context from a NeoForge/ModDevGradle workspace. Candidate discovery required an exact Minecraft-version token in the artifact path, but ModDevGradle names artifacts after the LOADER version (`neoforge-21.11.38-beta-*.jar` for Minecraft 1.21.11), so every candidate was dropped and the canonical NeoForge example project answered `ERR_CONTEXT_UNRESOLVED` even with `projectPath` supplied. Loader-to-Minecraft version equivalence is now recognized and recorded as a provenance note, and resources-only `client-extra` jars are deprioritized.
42
+ - An error hint can no longer ask the caller to supply a parameter they already supplied. The `validate-access-transformer` hint was the observed instance; the fix is applied generically to execution-error guidance so any tool's hints drop asks the request already satisfied.
43
+ - The typed-NBT rejections say what is wrong and what to do about it. `ERR_NBT_INVALID_TYPED_JSON` published `code`, `detail`, `status`, `retryClass`, and `issueOrigin` and nothing else: its `jsonPointer` / `expectedType` / `actualType` were stripped by the context allowlist, and there were no `fieldErrors`, no `hints`, and no `suggestedCall` — so a caller was told "Invalid typed NBT JSON document." with no way to learn WHICH node, and the `typedJson` argument advertises an empty JSON Schema (`{}`, frozen for legacy parity) that cannot tell them either. The error now carries `fieldErrors[0].path` (the RFC6901 pointer into the document, or `typedJson` for a root-level failure) plus a `nextAction` naming the expected and received types and the document shape, and `json-to-nbt` / `nbt-apply-json-patch` attach an `exampleCalls` entry pointing at `nbt-to-json` as the reliable way to obtain a valid document. `ERR_NBT_PARSE_FAILED`, `ERR_NBT_ENCODE_FAILED`, `ERR_JSON_PATCH_INVALID`, `ERR_NBT_UNSUPPORTED_FEATURE`, and `ERR_JSON_PATCH_CONFLICT` gain default `nextAction` guidance the same way. No message string and no advertised schema changed; the required document shape is documented in `docs/tool-reference.md` instead.
44
+ - `docs/tool-reference.md` no longer states a condition on the suggested-call fallback hint that the code does not implement. It documented `"suggested call payload failed schema validation; using fallback examples"` as firing only when a primary suggestion is dropped AND no `exampleCalls[]` fallback exists; neither emitting branch checks that, and all six recorded tool-contract envelopes that carry the sentence also carry `exampleCalls`. The sentence also fires for a deliberate placeholder-template drop, where nothing was ever schema-validated. The wording itself is unchanged and now carries a note saying why: it is frozen byte-for-byte in ten pre-migration fixtures that cannot be re-recorded.
45
+ - `get-class-source`'s `ERR_CLASS_NOT_FOUND` names the artifact the caller asked about. When the class was missing, an internal binary fallback re-resolved a DIFFERENT artifact and the error was then written from it: `details.artifactId`, `error.context.artifactId` and `details.suggestedCall.params.artifactId` all named an artifact the caller never requested, and `details.didYouMean` was collected from that artifact's symbol index — so the near-miss candidates came from the wrong jar and a `find-class` retry built from the suggestion searched the wrong place. The requested id is reported again; the fallback id survives as an additive `details.fallbackArtifactId`, present only when the two differ. `details.didYouMean` is collected from the requested artifact's index FIRST and then unioned with the fallback artifact's (see the near-miss entry under Fixed). A SUCCESSFUL fallback still reports the fallback artifact, which is where the returned source actually came from. Separately, the fallback now honors `allowDecompile` instead of hardcoding `true`, so a caller who explicitly passed `allowDecompile: false` no longer pays a full Vineflower pass (measured on a first miss: 22.5 s decompile plus 29.3 s indexing). That cost is UNCHANGED for callers who did not decline decompilation — the fallback is not otherwise narrowed.
46
+ - `ERR_CLASS_NOT_FOUND` offers near-miss candidates in the one scenario they were built for. Collecting `details.didYouMean` from the requested artifact alone returned `[]` exactly when the partial-source binary fallback had fired, because the requested artifact is by construction the one WITHOUT `net.minecraft` symbols — which is why the fallback fired and indexed the other jar. A typo'd vanilla class name therefore lost its only recovery signal. Candidates are now taken from the requested artifact first and unioned with the artifact the lookup ended on, deduplicated by fully-qualified name; a candidate found outside the requested artifact carries an additional `artifactId` naming where it was found, so an entry without that field always means the artifact the caller asked about. The identity fields are unchanged: `details.artifactId`, `error.context.artifactId` and `suggestedCall.params.artifactId` still name the REQUESTED artifact.
47
+ - A `get-class-source` failure that followed a nested-jar redirect no longer describes two artifacts at once. The redirect into a shell jar's bundled inner jar replaced the active artifact id, mapping and quality flags together, so the resulting `ERR_CLASS_NOT_FOUND` reported the outer shell as `details.artifactId` while `details.mapping` and `details.qualityFlags` described the inner jar — and `mapping` is published through the context allowlist, so a client acting on it queried the wrong namespace, with a `suggestedCall` pointing `find-class` at an index holding neither the class nor its siblings. All three fields now describe the artifact the caller named; `details.fallbackArtifactId` continues to name where the lookup ended, for the nested-jar redirect as well as the binary fallback.
48
+ - `docs/tool-reference.md` no longer promises that `error.exampleCalls[]` entries are "always-valid" and "safe to re-call as-is". Examples are validated for schema SHAPE only and are explicitly permitted to be templates, and frozen envelopes already publish `<...>` placeholders that would be rejected if replayed verbatim. The guarantee is corrected to what the code provides — right tool, right argument names and types, placeholders possible — with no change to the emitted examples or to any fixture. The same section's `didYouMean` and `resolve-method-mapping-exact` descriptions are updated for the behavior changes above.
49
+ - `find-class` reports a nested type under its own name. A nested class or enum was reported under its OUTER fully-qualified name because the symbol extractor stores one `qualifiedName` per file, so searching `Block` returned `net.minecraft.world.level.ClipContext` (the outer class of `ClipContext.Block`) ranked above the real `net.minecraft.world.level.block.Block`. Nested matches now render as `<Outer>.<Nested>` with `nested` and `enclosingClass` fields, and top-level matches sort first. A zero-result response classifies its warning as `partial_coverage` and carries a `suggestedCall` to `get-class-source` in one specific case — when the resolved artifact carries the `partial-source-no-net-minecraft` quality flag AND the query looks like a deobfuscated class name, the combination where partial coverage is what makes the index unable to answer while the binary fallback still can. Every other empty result is unchanged.
50
+ - `find-mapping` and the other mapping lookups no longer exhaust the worker heap on a populated Fabric Loom cache. Every `.tiny` file discovered for a version under `<gradleUserHome>/caches/fabric-loom/<version>` was read whole and merged into one unbounded index; on a real 1.21.10 cache that is 13 files totalling 87,744,824 bytes, which drove the worker into `Ineffective mark-compacts near heap limit`, `SIGABRT`, and an `ERR_WORKER_RESTART` for the caller. The loader now selects files from a 64 KiB header probe, deduplicates identical contents (those 13 files are only 6 distinct contents), merges into a shared accumulator instead of building a per-file index first, and bounds the accumulation with a heap-derived budget overridable by `MCP_LOOM_TINY_MAX_INDEX_ENTRIES`. Measured on that cache: 4,256.7 MB peak and a crash at 86.5 s became 2,705.3 MB and a resolved answer at 26.1 s, reading 31.0 MB of the 83.7 MB.
51
+ - Loom mapping lookups no longer report phantom candidates from a descriptor-namespace mismatch. Tiny v2 stores each descriptor once in the file's FIRST namespace, and the parser stamped that column onto every namespace untranslated; because `mappings-base.tiny` is headed `intermediary named official` while the sibling renderings are official-first, merging it re-registered every method under a second, foreign-coordinate descriptor — 216,696 rows on the measured cache, all methods. A method query could therefore return two candidates for one real member, and the phantom could outrank the real record in candidate ordering. Selection now drops a rendering only when it uses a different descriptor namespace, sits in the same directory as an already-selected rendering, and adds no uncovered namespace pair. One measured `resolve-method-mapping-exact` query changed from `resolved: false, status: "ambiguous"` with 2 candidates to `resolved: true` with 1. That is the same shape as the owner-strict change but a different cause and a different second candidate: owner-strictness drops a real method declared by an unrelated class that the member index admitted under its OWNERLESS key, while this drops a phantom of the queried method itself, re-registered under a foreign-coordinate descriptor.
52
+ - Modern-era `subscriptions/listen` is rejected at supervisor admission with `-32601` `Method not found`, as the contract always documented, and `maxSubscriptions: 0` is passed to `serveStdio` as defense in depth. Previously a conformant listen carrying a valid `SubscriptionFilter` was SERVED: the SDK answered with an id-less `notifications/subscriptions/acknowledged` notification and never settled the request id, so the supervisor's pending entry survived forever — permanently blocking the `validate-project` barrier and head-of-line-blocking every queued request. Because `notifications/cancelled` released the SDK subscription but not the supervisor entry, repeated listen/cancel pairs also grew `pendingRequests` without bound while evading the SDK's own 1024-subscription cap.
53
+ - A cancelled request now releases its supervisor slot for every method, not only `validate-project`: `notifications/cancelled` settles the pending entry, clears its deadline timer, releases the `validate-project` barrier if it held one, and records an ordinary response-finality tombstone so a late worker answer is still discarded. An `initialize` in flight is excluded, since its handshake lifecycle owns that entry. Finality tombstones are capped with insertion-order eviction so the bookkeeping cannot itself grow without bound.
54
+ - An `initialize` request that is not a valid MCP initialize request no longer locks the process to the legacy era. Previously any generically valid JSON-RPC frame named `initialize` committed the one-way era lock BEFORE method-schema validation, so a malformed handshake permanently burned the era choice: the client received a `-32603` restart error implying a transient failure, and every later modern request answered `-32600` `era_conflict` until the process was respawned. The request is now validated against the SDK's own initialize schema first and rejected with `-32602` `data.kind: "invalid_initialize"` (carrying `data.required[]` and `data.eraSelected: false`), leaving the era unselected and the client free to retry either era.
55
+ - The modern `protocolVersion` VALUE is validated on every modern request instead of only on the one that pins the connection. Previously, once a connection was pinned by a valid request, later requests carrying an unsupported version were served normally — a `tools/call` with `"1999-12-31"` executed its handler — and whether `-32022` fired at all depended on which method the client happened to send first.
56
+ - A framing violation that the reader cannot provably resynchronize past now terminates the session with a `supervisor.client_framing_fatal` diagnostic and exit code 1, instead of silently consuming every later frame. Previously a 29-byte `Content-Length: 999999999\r\n\r\n` header with no body left the reader waiting on the attacker-declared count for the process lifetime, so all subsequent valid frames were swallowed with no further diagnostic; an under-declared `Content-Length` likewise corrupted the next valid request by prefixing it with the unread remainder. Duplicate `Content-Length` headers are rejected rather than resolved last-wins.
57
+ - Line framing recovers after a line-delimited JSON array frame. The Content-Length reader recognized only `{` as an opener that cannot begin a header block, so an array arriving after a Content-Length frame was neither consumed as a line nor parsed as a header, and every subsequent frame stayed buffered indefinitely with no parse error reported. Arrays are re-dispatched only to surface their JSON-RPC schema error; this does not add batch-message support.
58
+ - A `server/discover` admitted while the worker was starting is no longer drained into a `-32601` by a later `initialize`. The queued arrival-order prefix preceding an `initialize` is now forwarded ahead of it; the suffix stays gated until the initialize response.
59
+ - Legacy `tools/call` with a non-object `arguments` reproduces the v1 error bytes again. Method-shape validation now runs before the unknown-tool intercept on legacy connections, so a malformed call naming a missing tool no longer returns a successful `isError` "not found" envelope, and a registered tool answers the v1 raw `-32603` rather than the SDK v2 `-32602`. Modern calls keep the SDK's `-32602`.
60
+ - A supervisor synthetic terminal reply (queue overflow, cap-blocked restart, unknown-tool intercept, or queued-request terminalization) for a request that reuses a live JSON-RPC id no longer settles or tombstones the different live request sharing that id. Previously the live request's worker answer was tombstone-discarded and a running `validate-project` barrier could strand queued requests for the worker's lifetime. The same identity rule also drops an orphaned preserved initialize entry when a cap-blocked re-initialize discards the cached handshake, instead of leaking it into `pendingRequests`. Pinned by `tests/stdio/stdio-supervisor-finality-id-reuse.test.ts`.
61
+ - A legacy `initialize` carrying a valid modern `io.modelcontextprotocol/*` era claim in `params._meta` completes the legacy handshake: the supervisor strips the era-claim keys before the frame reaches the worker, whose opening classifier would otherwise treat a claim-bearing `initialize` as modern and fail the handshake with a worker restart. Non-era `_meta` keys pass through. Pinned by the enveloped-initialize wire test in `tests/stdio/stdio-supervisor-era-wire.test.ts`.
62
+ - Version manifest and version-detail fetches now abort after `MCP_FETCH_TIMEOUT_MS` with a typed `ERR_REPO_FETCH_FAILED` error instead of hanging indefinitely when a repository stops responding.
63
+ - `manage-cache` opens the artifact index through the integrity-checking recovery path: a corrupt SQLite file is backed up and rebuilt instead of crashing cache inspection, and a missing database file is still not created as a side effect of inspection.
64
+ - In supervised stdio mode, a worker that hits a fatal `uncaughtException`/`unhandledRejection` exits after logging so the supervisor restart/replay path replaces it; previously the faulted worker stayed alive with only `exitCode` set, bypassing recovery.
65
+ - The stdio supervisor retries unresolved process-tree cleanup tokens during normal operation with capped exponential backoff, emitting `supervisor.cleanup_token.retry`/`supervisor.cleanup_token.recovered` events and unblocking the 2-slot live cap without waiting for shutdown. Live-cap saturation that blocks a restart is surfaced as `supervisor.live_cap.saturated`.
66
+ - Tool responses keep their typed error envelopes when SQLite is unavailable: input validation still returns `ERR_INVALID_INPUT`, database-independent tools (such as the NBT utilities) still return their successful results, and a metrics-recording failure no longer replaces a completed response.
67
+ - A supervised worker now stands down when its supervisor goes away, instead of surviving as an orphan for the lifetime of the machine. The worker held an unconditional keep-alive `setInterval` whose only `clearInterval` ran from a process `"exit"` listener — which by definition fires once the process is already leaving — so stdin EOF closed the MCP server but never drained the event loop: any launcher that terminated the server without a cooperative shutdown left a resident node process behind — measured at ~125 MB on the reference host — and each restart added another. The worker now releases the keep-alive on stdin EOF, remembers an EOF that arrives while it is still starting (a fast-closing pipe could otherwise deliver `end` before the listener existed), and — as defense in depth for hosts whose stdin is not a pipe the parent solely owns — stands down when the parent pid it snapshotted at startup stops existing. Teardown never calls `process.exit()`, so replies still being written after a half-close are delivered in full. On the supervisor side, a generation that signals readiness and then stands down cleanly before it has been ready for a second no longer counts as a start that succeeded: adoption still clears the restart backoff, and the clean stand-down then raises it back to the level the repeated failure has earned without reserving a restart, so a host that closes every spawned child's stdin immediately backs off exponentially instead of respawning a worker ten times a second. A crash keeps its prompt replacement. Measured on the reference host, a single full `npm test` run each way: 20 resident orphaned workers were left before the fix and 0 after. Test-side teardown moved onto a shared stdin-close → `SIGTERM` → `SIGKILL` ladder (`tests/helpers/stdio-child-lifecycle.ts`), and `tests/contracts/no-direct-sigkill.test.ts` keeps it that way.
68
+ - The stdio supervisor now reaps its worker on `SIGHUP` and on its own fatal errors, instead of leaving the detached worker process group with nobody to collect it. `SIGHUP` — what a terminating launcher or a vanishing session sends — was never registered, so the OS default terminated the supervisor where it stood; an uncaught exception or unhandled rejection in the supervisor process was likewise left to node's default handler, which prints a stack and exits without any route to shutdown. Both now run the supervisor's ordinary shutdown path, which terminates the worker process group; a fatal error is reported as a `supervisor.fatal` event and then ends the process explicitly with a non-zero status, once the worker group has been collected or a bounded watchdog expires. Registering fatal handlers suppresses node's default abort, so that exit can no longer be left to the event loop draining on its own, and the handlers now stay registered until the teardown tail finishes rather than being withdrawn before it.
69
+
70
+ ## [6.3.0] - 2026-07-18
71
+
72
+ ### Added
73
+
74
+ - `find-class` accepts top-level `projectPath` for workspace-relative `target.kind="workspace"` and dependency targets using `versionFromProject`, and searches simple or qualified class names across nested `.class` inventories when the resolved artifact is a Jar-in-Jar shell such as the Fabric API umbrella JAR. Nested matches retain the existing response shape, deduplicate repeated qualified names, and honor `limit`; top-level matches remain directly usable by the source/member tools.
75
+
76
+ ### Fixed
77
+
78
+ - `inspect-minecraft` now describes `task`, structured workspace `subject.focus`, and each class/search/file focus branch in `tools/list`. A string focus remains invalid instead of being guessed or coerced, but its `ERR_INVALID_INPUT` response now includes three schema-validated `exampleCalls` that preserve valid workspace context and show the exact object shapes for retrying.
79
+ - `find-class` no longer interprets a native dependency or Jar-in-Jar shell miss as evidence of obfuscated Minecraft runtime names, so it does not recommend `mapping="mojang"` for those artifacts. Dotted inner-class names discovered in a shell also continue through `get-class-source` instead of missing the binary `$` entry.
80
+
10
81
  ## [6.2.0] - 2026-07-11
11
82
 
12
83
  ### 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:
@@ -156,7 +166,7 @@ All six return `result.summary` first and can include `summary.nextActions` when
156
166
  These notes cover high-frequency decisions during onboarding. For the full pitfall list, exact contracts, migration notes, and environment variables, see [docs/tool-reference.md](docs/tool-reference.md).
157
167
 
158
168
  - `search-class-source` defaults to `queryMode="auto"` and keeps separator queries such as `foo.bar`, `foo_bar`, and `foo$bar` on the indexed path. Use `queryMode="literal"` for an explicit full substring scan.
159
- - If you do not already have an artifact, prefer `subject.kind="workspace"` for `inspect-minecraft` instead of guessing artifact details. When artifact context is the only missing input, a retryable `suggestedCall` preserves the requested task.
169
+ - If you do not already have an artifact, prefer `subject.kind="workspace"` for `inspect-minecraft` instead of guessing artifact details. `subject.focus` is an object, not a string: use `{ "kind": "class", "className": "..." }`, `{ "kind": "search", "query": "..." }`, or `{ "kind": "file", "filePath": "..." }`. `task="auto"` dispatches only from `subject.kind` and `focus.kind`; it is not a natural-language planner. Invalid string focus returns class/search/file `exampleCalls`, while missing artifact context keeps the requested task in a retryable `suggestedCall`.
160
170
  - `trace-symbol-lifecycle` expects `Class.method` in `symbol`. Keep exact overload matching in the separate `descriptor` field.
161
171
  - For unobfuscated releases such as `26.1+`, `check-symbol-exists` and `analyze-symbol task="exists"` validate `mojang` lookups against runtime bytecode when no mapping graph exists, and return `mapping_unavailable` when the runtime JAR itself is unreachable.
162
172
  - `analyze-mod` and `validate-project` require structured `subject` objects and canonical `include` groups; stale string-subject or domain-include payloads return `ERR_INVALID_INPUT` with a retryable `suggestedCall`.
@@ -278,7 +288,7 @@ Tools for browsing Minecraft versions, resolving source artifacts, and reading o
278
288
  | --- | --- |
279
289
  | `list-versions` | List available Minecraft versions from Mojang metadata and local cache |
280
290
  | `resolve-artifact` | Resolve source artifacts from versions, JAR paths, or Maven coordinates |
281
- | `find-class` | Find simple or fully-qualified class names inside an artifact |
291
+ | `find-class` | Find simple or fully-qualified class names, including classes bundled in nested JARs |
282
292
  | `get-class-source` | Read class source from an artifact or resolve the backing artifact on demand |
283
293
  | `get-class-members` | List constructors, fields, and methods from bytecode |
284
294
  | `search-class-source` | Search indexed class source by symbol, text, or path |
@@ -287,6 +297,8 @@ Tools for browsing Minecraft versions, resolving source artifacts, and reading o
287
297
  | `index-artifact` | Rebuild indexed metadata for an existing artifact |
288
298
  <!-- END GENERATED TOOL TABLE: source-exploration -->
289
299
 
300
+ `find-class` accepts either an `artifactId` or the shared object `target` shape. For a workspace-relative dependency, pass `target: { kind: "dependency", group, name, versionFromProject: true }` with top-level `projectPath`. Fabric-style umbrella JARs are searched through their nested `.class` inventories; a top-level class match can then be passed to `get-class-source` or `get-class-members`, which resolve the containing nested JAR. Dotted inner-class matches are also readable through `get-class-source`. An empty result still means that the requested class name is absent from the resolved dependency version.
301
+
290
302
  For unobfuscated releases such as `26.1+`, `mapping="mojang"` uses the runtime/decompile path directly and skips Loom source-jar discovery, while `intermediary` and `yarn` fall back to `obfuscated` with a warning.
291
303
 
292
304
  ### Version Comparison & Symbol Tracking
@@ -388,7 +400,7 @@ Detailed parameter constraints, migration notes, resource behavior, and the full
388
400
 
389
401
  Repository requirements:
390
402
 
391
- - Node.js 22+
403
+ - Node.js 22.13.0+
392
404
  - `pnpm`
393
405
  - Java when running remap or decompile flows locally
394
406
 
@@ -415,12 +427,16 @@ pnpm test
415
427
 
416
428
  Run these when relevant:
417
429
 
430
+ - `pnpm test:file <path...>` for focused ordinary suites
431
+ - `pnpm test:grep <name-pattern>` for recursive ordinary suites filtered by test name
418
432
  - `pnpm test:manual:stdio-smoke` for MCP transport, registration, or manual workflow changes
419
433
  - `pnpm test:manual:package-smoke` when checking packaged install and distribution behavior
420
434
  - `pnpm test:perf` for search, index, or performance-sensitive changes
421
435
  - `pnpm test:coverage` or `pnpm test:coverage:lcov` for coverage checks (`lines=80`, `branches=70`, `functions=80`)
422
436
  - `pnpm validate` for the full local validation suite
423
437
 
438
+ Ordinary `.test.ts` files live in domain directories under `tests/` (for example `source-service/`, `entry-tools/`, `mapping/`, `mixin/`, `integration/mcp-tools/`, `contracts/`, and `utils/`). Keep helper-only modules in `tests/helpers`; manual smoke, performance, resource, and smoke-only files remain in their existing special directories.
439
+
424
440
  ## License
425
441
 
426
442
  [MIT](LICENSE)
@@ -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,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 Database from "./storage/sqlite.js";
7
+ import { openDatabase } from "./storage/db.js";
8
8
  import { getProcessWorkspaceContextCache } from "./workspace-context-cache.js";
9
9
  export const PUBLIC_CACHE_KINDS = [
10
10
  "artifact-index",
@@ -222,11 +222,11 @@ function prepareSelector(selector, runtimeInfo) {
222
222
  normalizedProjectPath: normalizePathKey(selector.projectPath, runtimeInfo)
223
223
  };
224
224
  }
225
- function openDb(sqlitePath) {
226
- if (!existsSync(sqlitePath)) {
225
+ function openDb(config) {
226
+ if (!existsSync(config.sqlitePath)) {
227
227
  return undefined;
228
228
  }
229
- return new Database(sqlitePath);
229
+ return openDatabase(config).db;
230
230
  }
231
231
  function candidatePathsForEntry(entry) {
232
232
  const paths = new Set();
@@ -388,7 +388,7 @@ function matchesSelector(entry, selector, runtimeInfo) {
388
388
  return true;
389
389
  }
390
390
  async function artifactIndexEntries(config) {
391
- const db = openDb(config.sqlitePath);
391
+ const db = openDb(config);
392
392
  if (!db) {
393
393
  return [];
394
394
  }
@@ -650,7 +650,7 @@ export function createCacheRegistry(config) {
650
650
  const entries = await collectEntries(input.cacheKinds, input.selector);
651
651
  const selectedBytes = entries.reduce((total, entry) => total + entry.sizeBytes, 0);
652
652
  if (input.executionMode === "apply") {
653
- const db = openDb(config.sqlitePath);
653
+ const db = openDb(config);
654
654
  try {
655
655
  for (const entry of entries) {
656
656
  if (entry.cacheKind === "artifact-index") {
package/dist/cli.js CHANGED
@@ -6,18 +6,89 @@ import { log } from "./logger.js";
6
6
  import { STDIO_WORKER_MODE_ENV, StdioSupervisor } from "./stdio-supervisor.js";
7
7
  const CHILD_PID_FILE_ENV = "MCP_SUPERVISOR_CHILD_PID_FILE";
8
8
  const WORKER_READY_MARKER = "__MCP_STDIO_WORKER_READY__";
9
+ const PARENT_LIVENESS_POLL_MS = 5_000;
9
10
  async function main() {
10
11
  if (process.env[STDIO_WORKER_MODE_ENV] === "1") {
11
12
  const pidFile = process.env[CHILD_PID_FILE_ENV];
12
13
  if (pidFile) {
13
14
  writeFileSync(pidFile, `${process.pid}\n`, "utf8");
14
15
  }
15
- // Worker mode runs behind the stdio supervisor; 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) {
@@ -9,6 +9,7 @@ export interface DecompileResult {
9
9
  interface DecompileBinaryOptions {
10
10
  vineflowerJarPath?: string;
11
11
  timeoutMs?: number;
12
+ maxMemoryMb?: number;
12
13
  signature?: string;
13
14
  artifactIdCandidate?: string;
14
15
  }