@adhisang/minecraft-modding-mcp 3.2.0 → 4.1.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 (194) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +52 -32
  3. package/dist/build-suggested-call.d.ts +29 -0
  4. package/dist/build-suggested-call.js +58 -0
  5. package/dist/cache-registry.d.ts +3 -1
  6. package/dist/cache-registry.js +59 -7
  7. package/dist/config.d.ts +10 -1
  8. package/dist/config.js +52 -1
  9. package/dist/entry-tools/analyze-symbol-service.d.ts +18 -18
  10. package/dist/entry-tools/analyze-symbol-service.js +13 -2
  11. package/dist/entry-tools/batch-class-members-service.d.ts +34 -0
  12. package/dist/entry-tools/batch-class-members-service.js +97 -0
  13. package/dist/entry-tools/batch-class-source-service.d.ts +37 -0
  14. package/dist/entry-tools/batch-class-source-service.js +100 -0
  15. package/dist/entry-tools/batch-mappings-service.d.ts +36 -0
  16. package/dist/entry-tools/batch-mappings-service.js +66 -0
  17. package/dist/entry-tools/batch-runner.d.ts +72 -0
  18. package/dist/entry-tools/batch-runner.js +90 -0
  19. package/dist/entry-tools/batch-symbol-exists-service.d.ts +46 -0
  20. package/dist/entry-tools/batch-symbol-exists-service.js +113 -0
  21. package/dist/entry-tools/compare-minecraft-service.d.ts +6 -6
  22. package/dist/entry-tools/inspect-minecraft/handlers/artifact.d.ts +5 -0
  23. package/dist/entry-tools/inspect-minecraft/handlers/artifact.js +83 -0
  24. package/dist/entry-tools/inspect-minecraft/handlers/class-members.d.ts +6 -0
  25. package/dist/entry-tools/inspect-minecraft/handlers/class-members.js +80 -0
  26. package/dist/entry-tools/inspect-minecraft/handlers/class-overview.d.ts +5 -0
  27. package/dist/entry-tools/inspect-minecraft/handlers/class-overview.js +248 -0
  28. package/dist/entry-tools/inspect-minecraft/handlers/class-source.d.ts +5 -0
  29. package/dist/entry-tools/inspect-minecraft/handlers/class-source.js +60 -0
  30. package/dist/entry-tools/inspect-minecraft/handlers/file.d.ts +5 -0
  31. package/dist/entry-tools/inspect-minecraft/handlers/file.js +54 -0
  32. package/dist/entry-tools/inspect-minecraft/handlers/list-files.d.ts +5 -0
  33. package/dist/entry-tools/inspect-minecraft/handlers/list-files.js +100 -0
  34. package/dist/entry-tools/inspect-minecraft/handlers/search.d.ts +5 -0
  35. package/dist/entry-tools/inspect-minecraft/handlers/search.js +155 -0
  36. package/dist/entry-tools/inspect-minecraft/handlers/versions.d.ts +6 -0
  37. package/dist/entry-tools/inspect-minecraft/handlers/versions.js +49 -0
  38. package/dist/entry-tools/inspect-minecraft/internal.d.ts +1042 -0
  39. package/dist/entry-tools/inspect-minecraft/internal.js +448 -0
  40. package/dist/entry-tools/inspect-minecraft-service.d.ts +213 -328
  41. package/dist/entry-tools/inspect-minecraft-service.js +20 -1238
  42. package/dist/entry-tools/manage-cache-service.d.ts +16 -16
  43. package/dist/entry-tools/validate-project/cases/access-transformer.d.ts +6 -0
  44. package/dist/entry-tools/validate-project/cases/access-transformer.js +106 -0
  45. package/dist/entry-tools/validate-project/cases/access-widener.d.ts +6 -0
  46. package/dist/entry-tools/validate-project/cases/access-widener.js +86 -0
  47. package/dist/entry-tools/validate-project/cases/mixin.d.ts +6 -0
  48. package/dist/entry-tools/validate-project/cases/mixin.js +90 -0
  49. package/dist/entry-tools/validate-project/cases/project-summary.d.ts +97 -0
  50. package/dist/entry-tools/validate-project/cases/project-summary.js +346 -0
  51. package/dist/entry-tools/validate-project/internal.d.ts +135 -0
  52. package/dist/entry-tools/validate-project/internal.js +287 -0
  53. package/dist/entry-tools/validate-project-service.d.ts +63 -47
  54. package/dist/entry-tools/validate-project-service.js +12 -482
  55. package/dist/entry-tools/verify-mixin-target-service.d.ts +133 -0
  56. package/dist/entry-tools/verify-mixin-target-service.js +323 -0
  57. package/dist/error-mapping.d.ts +40 -0
  58. package/dist/error-mapping.js +139 -0
  59. package/dist/errors.d.ts +6 -0
  60. package/dist/errors.js +6 -0
  61. package/dist/index.d.ts +2 -0
  62. package/dist/index.js +170 -1314
  63. package/dist/lru-list.d.ts +31 -0
  64. package/dist/lru-list.js +102 -0
  65. package/dist/mapping/internal-types.d.ts +54 -0
  66. package/dist/mapping/internal-types.js +14 -0
  67. package/dist/mapping/loaders/mojang.d.ts +2 -0
  68. package/dist/mapping/loaders/mojang.js +64 -0
  69. package/dist/mapping/loaders/tiny-loom.d.ts +2 -0
  70. package/dist/mapping/loaders/tiny-loom.js +73 -0
  71. package/dist/mapping/loaders/tiny-maven.d.ts +2 -0
  72. package/dist/mapping/loaders/tiny-maven.js +104 -0
  73. package/dist/mapping/loaders/types.d.ts +14 -0
  74. package/dist/mapping/loaders/types.js +2 -0
  75. package/dist/mapping/lookup.d.ts +52 -0
  76. package/dist/mapping/lookup.js +496 -0
  77. package/dist/mapping/parsers/normalize.d.ts +10 -0
  78. package/dist/mapping/parsers/normalize.js +52 -0
  79. package/dist/mapping/parsers/proguard.d.ts +20 -0
  80. package/dist/mapping/parsers/proguard.js +138 -0
  81. package/dist/mapping/parsers/symbol-records.d.ts +27 -0
  82. package/dist/mapping/parsers/symbol-records.js +216 -0
  83. package/dist/mapping/parsers/tiny.d.ts +9 -0
  84. package/dist/mapping/parsers/tiny.js +96 -0
  85. package/dist/mapping/types.d.ts +147 -0
  86. package/dist/mapping/types.js +2 -0
  87. package/dist/mapping-pipeline-service.d.ts +10 -1
  88. package/dist/mapping-pipeline-service.js +16 -3
  89. package/dist/mapping-service.d.ts +15 -144
  90. package/dist/mapping-service.js +179 -1119
  91. package/dist/mixin/access-validators.d.ts +9 -0
  92. package/dist/mixin/access-validators.js +257 -0
  93. package/dist/mixin/annotation-validators.d.ts +5 -0
  94. package/dist/mixin/annotation-validators.js +162 -0
  95. package/dist/mixin/helpers.d.ts +28 -0
  96. package/dist/mixin/helpers.js +315 -0
  97. package/dist/mixin/parsed-validator.d.ts +8 -0
  98. package/dist/mixin/parsed-validator.js +337 -0
  99. package/dist/mixin/types.d.ts +208 -0
  100. package/dist/mixin/types.js +28 -0
  101. package/dist/mixin-validator.d.ts +9 -201
  102. package/dist/mixin-validator.js +8 -1005
  103. package/dist/observability.d.ts +18 -1
  104. package/dist/observability.js +44 -1
  105. package/dist/response-utils.d.ts +44 -10
  106. package/dist/response-utils.js +131 -17
  107. package/dist/source/access-validate.d.ts +4 -0
  108. package/dist/source/access-validate.js +254 -0
  109. package/dist/source/artifact-resolver.d.ts +110 -0
  110. package/dist/source/artifact-resolver.js +1174 -0
  111. package/dist/source/cache-metrics.d.ts +26 -0
  112. package/dist/source/cache-metrics.js +172 -0
  113. package/dist/source/class-source/members-builder.d.ts +34 -0
  114. package/dist/source/class-source/members-builder.js +46 -0
  115. package/dist/source/class-source/snippet-builder.d.ts +19 -0
  116. package/dist/source/class-source/snippet-builder.js +46 -0
  117. package/dist/source/class-source-helpers.d.ts +34 -0
  118. package/dist/source/class-source-helpers.js +140 -0
  119. package/dist/source/class-source.d.ts +42 -0
  120. package/dist/source/class-source.js +883 -0
  121. package/dist/source/descriptor-utils.d.ts +6 -0
  122. package/dist/source/descriptor-utils.js +37 -0
  123. package/dist/source/file-access.d.ts +4 -0
  124. package/dist/source/file-access.js +102 -0
  125. package/dist/source/indexer.d.ts +82 -0
  126. package/dist/source/indexer.js +505 -0
  127. package/dist/source/lifecycle/diff-utils.d.ts +9 -0
  128. package/dist/source/lifecycle/diff-utils.js +107 -0
  129. package/dist/source/lifecycle/diff.d.ts +2 -0
  130. package/dist/source/lifecycle/diff.js +265 -0
  131. package/dist/source/lifecycle/mapping-helpers.d.ts +22 -0
  132. package/dist/source/lifecycle/mapping-helpers.js +327 -0
  133. package/dist/source/lifecycle/runtime-check.d.ts +2 -0
  134. package/dist/source/lifecycle/runtime-check.js +142 -0
  135. package/dist/source/lifecycle/trace.d.ts +2 -0
  136. package/dist/source/lifecycle/trace.js +231 -0
  137. package/dist/source/lifecycle.d.ts +4 -0
  138. package/dist/source/lifecycle.js +5 -0
  139. package/dist/source/search.d.ts +51 -0
  140. package/dist/source/search.js +676 -0
  141. package/dist/source/shared-utils.d.ts +6 -0
  142. package/dist/source/shared-utils.js +55 -0
  143. package/dist/source/state.d.ts +21 -0
  144. package/dist/source/state.js +19 -0
  145. package/dist/source/symbol-resolver.d.ts +3 -0
  146. package/dist/source/symbol-resolver.js +212 -0
  147. package/dist/source/validate-mixin/pipeline/mapping-health.d.ts +3 -0
  148. package/dist/source/validate-mixin/pipeline/mapping-health.js +41 -0
  149. package/dist/source/validate-mixin/pipeline/parse.d.ts +2 -0
  150. package/dist/source/validate-mixin/pipeline/parse.js +10 -0
  151. package/dist/source/validate-mixin/pipeline/resolve.d.ts +3 -0
  152. package/dist/source/validate-mixin/pipeline/resolve.js +78 -0
  153. package/dist/source/validate-mixin/pipeline/target-lookup.d.ts +6 -0
  154. package/dist/source/validate-mixin/pipeline/target-lookup.js +260 -0
  155. package/dist/source/validate-mixin/pipeline-context.d.ts +72 -0
  156. package/dist/source/validate-mixin/pipeline-context.js +93 -0
  157. package/dist/source/validate-mixin.d.ts +22 -0
  158. package/dist/source/validate-mixin.js +799 -0
  159. package/dist/source/workspace-target.d.ts +18 -0
  160. package/dist/source/workspace-target.js +305 -0
  161. package/dist/source-resolver.d.ts +9 -1
  162. package/dist/source-resolver.js +14 -6
  163. package/dist/source-service.d.ts +178 -105
  164. package/dist/source-service.js +72 -5312
  165. package/dist/stage-emitter.d.ts +13 -0
  166. package/dist/stage-emitter.js +30 -0
  167. package/dist/stdio-supervisor.d.ts +61 -0
  168. package/dist/stdio-supervisor.js +326 -9
  169. package/dist/storage/artifacts-repo.d.ts +4 -1
  170. package/dist/storage/artifacts-repo.js +33 -5
  171. package/dist/storage/files-repo.d.ts +0 -2
  172. package/dist/storage/files-repo.js +0 -11
  173. package/dist/storage/migrations.d.ts +1 -1
  174. package/dist/storage/migrations.js +10 -2
  175. package/dist/storage/schema.d.ts +2 -0
  176. package/dist/storage/schema.js +25 -0
  177. package/dist/tool-contract-manifest.d.ts +1 -1
  178. package/dist/tool-contract-manifest.js +23 -6
  179. package/dist/tool-guidance.d.ts +82 -0
  180. package/dist/tool-guidance.js +734 -0
  181. package/dist/tool-schema-registry.d.ts +16 -0
  182. package/dist/tool-schema-registry.js +37 -0
  183. package/dist/tool-schemas.d.ts +3518 -0
  184. package/dist/tool-schemas.js +813 -0
  185. package/dist/types.d.ts +39 -0
  186. package/dist/version-service.js +7 -6
  187. package/dist/workspace-context-cache.d.ts +32 -0
  188. package/dist/workspace-context-cache.js +66 -0
  189. package/dist/workspace-mapping-service.d.ts +16 -0
  190. package/dist/workspace-mapping-service.js +173 -1
  191. package/docs/README-ja.md +414 -0
  192. package/docs/examples.md +483 -0
  193. package/docs/tool-reference.md +459 -0
  194. package/package.json +5 -2
package/CHANGELOG.md CHANGED
@@ -5,6 +5,78 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project aims to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [4.1.0] - 2026-05-09
9
+
10
+ ### Documentation
11
+ - README onboarding and the Japanese README now clarify MCP client setup and packaged documentation links, and the npm package includes the Markdown docs those links reference.
12
+
13
+ ### Added
14
+ - `validate-project task="project-summary"` now returns an additive `tasks` per-probe status report alongside the existing `summary` / `project` / `workspace` blocks. The seven probes (`workspace.detected`, `gradle.readable`, `loom.cache.found`, `minecraft.artifact.resolved`, `mixins.validated`, `accessWideners.validated`, `accessTransformers.validated`) each carry `status: "ok" | "skipped" | "missing" | "error"` plus optional sub-fields (e.g. `evidence`, `propertiesPath`, `buildScripts`, `cachePath`, `artifactId`, `mapping`, `counts`). The headline `result.summary.status` is unchanged; the new field surfaces which probes succeeded even when the overall status is `blocked`. With `detail: "summary"` (or when `include` does not contain `"workspace"`), each entry is slimmed to `status` / `error?` / `warnings?`. Set `VALIDATE_PROJECT_TASKS_OFF=1` to omit the field entirely.
15
+ - `get-class-members` now returns an additive `status: "ok" | "members_unavailable" | "partial"` field. `"ok"` covers both populated classes (`counts.total > 0`) and genuinely empty classes whose binary signature parsed without error and produced no decompiled fallback. `"partial"` fires whenever `decompiledFallback` is populated (bytecode returned zero but indexed source supplied member names; `qualityFlags` includes `"members-from-decompiled-source"`). `"members_unavailable"` fires when binary signature extraction throws a non-`ERR_CLASS_NOT_FOUND` error and no decompiled fallback is available; the response then carries `unavailableReason: string` and a `suggestedCall: { tool: "get-class-source", params }` whose params validate against `get-class-source`'s input schema. `ERR_CLASS_NOT_FOUND` still propagates as a thrown error. The shape is purely additive — `members` / `counts` / `decompiledFallback` / `decompiledMemberCounts` / `qualityFlags` are unchanged for callers that ignore `status`. Set `MEMBERS_STATUS_LEGACY=1` to omit the new fields entirely (legacy shape).
16
+ - `verify-mixin-target` (new tool): single-call probe that answers "does this owner / member exist, and which `@Shadow` / `@Accessor` / `@Invoker` should the mixin use?" Inputs accept `target.kind="workspace" | "version" | "coordinate" | "dependency" | "jar"`, an optional `mixinMemberName` (caller-authored mixin field/method name), and a discriminated `member: { kind: "method" | "field", name, descriptor? }`. Output carries `exists`, `matches[]`, `candidates[]` (descriptor-mismatch and Levenshtein-near misses via the same `suggestSimilar` helper used by `validate-mixin`), and an `accessorAdvice` block with `suggestedAnnotation` (`"@Shadow"` / `"@Accessor"` / `"@Invoker"` / `"@Inject-only"` / `null`), a deterministic `exampleSnippet`, and `candidates` when the rule table cannot decide between `@Shadow` and `@Invoker`. `accessorAdvice` is emitted only when `matches.length === 1` (a single unique match): descriptor-mismatch / nearest-neighbor / candidate-only responses carry no advice, and ambiguous overload responses (descriptor omitted, multiple matches) also carry no advice — re-call with an explicit `descriptor` to disambiguate. `@Accessor` setter templates (when `mixinMemberName` matches `^set[A-Z]`) emit `void name(<type> value);` instead of the zero-arg getter shape. `"@Inject-only"` is a pseudo-tag — not a real annotation — that signals "target already visible, no `@Shadow` required". Owner-not-found surfaces `ERR_CLASS_NOT_FOUND` with `details.suggestedCall.tool === "find-class"`. Namespace mismatch (explicit `mapping` differs from resolved `mappingApplied`) surfaces `ERR_NAMESPACE_MISMATCH` with `details.requestedMapping` and `details.mappingApplied` rather than silently returning a misleading `exists:false`; auto-translation of owner/member names across namespaces is deferred. Set `VERIFY_MIXIN_TARGET_OFF=1` to remove the tool from `tools/list` entirely (direct calls then return the SDK "Tool not found" tool-result envelope — `{ content: [{ type: "text", text: "MCP error -32602: Tool verify-mixin-target not found" }], isError: true }` — so callers cannot rely on `error.code === "ERR_*"` while the toggle is set).
17
+ - `ProblemDetails.code` may now be `ERR_WORKER_RESTART`, `ERR_MIXIN_PARSE_FAILED`, `ERR_STAGE_BUDGET_PRE_PARSE`, `ERR_WORKSPACE_VERSION_UNRESOLVED`, `ERR_DEPENDENCY_VERSION_UNRESOLVED`, or `ERR_BATCH_ABORTED`. Restart envelopes are produced by the supervisor when a worker exits while handling `tools/call`; `ERR_STAGE_BUDGET_PRE_PARSE` is raised by `validate-mixin` when a pre-parse stage exhausts its soft-deadline; `ERR_WORKSPACE_VERSION_UNRESOLVED` / `ERR_DEPENDENCY_VERSION_UNRESOLVED` carry structured `suggestedCall` payloads for caller-side recovery; `ERR_BATCH_ABORTED` (HTTP 412) fills un-started slots in batch tools when `failFast: true` and an earlier entry already failed.
18
+ - `validate-mixin` now reports `result.targetOutcomes[]` (per-target `status: "ok" | "deferred-budget" | "tool-issue"` plus optional `slowTarget` / `elapsedMs` / `budgetMs`) and `summary.targetsDeferredBudget` / `summary.degradedReason: "stage-budget" | "stage-budget-pre-target"` so callers can distinguish completed work from work skipped by the soft-deadline.
19
+ - `meta.restart` (synthetic worker-restart responses only) carries `tool` / `durationMs` / `lastStage` / `lastStageElapsedMs` / `lastStageMeta` / `exit` / `retryRecommendation` for caller diagnostics. `meta.stageBudgetExhausted` / `meta.budgetMs` / `meta.elapsedMs` accompany `ERR_STAGE_BUDGET_PRE_PARSE` error envelopes so callers can detect budget-driven failures from the public envelope without parsing the message string.
20
+ - `validate-mixin` batch-mode results (`paths` / `config` / `project`) now preserve typed AppError metadata on per-entry errors: each `results[i]` carries optional `errorCode` and `errorDetails` (`failedStage`, `stageBudgetExhausted`, `budgetMs`, `elapsedMs`, …) so callers can distinguish budget-driven failures from generic per-file processing errors.
21
+ - `target.kind="workspace"` is now accepted by `resolve-artifact`, `get-class-source`, and `get-class-members`. The synthesizer reads `gradle.properties` and `build.gradle(.kts)` from `projectPath` to detect Minecraft version, compile mapping, and loader, then rewrites the call to the resolved `target.kind="version"` shape. Detected facts and a `cacheHit` flag surface on `provenance.workspaceResolution`.
22
+ - `target.kind="dependency"` (with optional `versionFromProject`) is now accepted by the same three tools. It probes four de-duplicated `gradle.properties` keys (`name_version`, `camelCaseVersion`, `group_name_version`, `groupNameVersion`) and falls back to `~/.gradle/caches/modules-2/files-2.1/<group>/<name>/`, picking the semver-newest non-snapshot. Dependency JARs disable the Mojang binary-remap path; mapping mismatches between the caller's `mapping` and the artifact namespace surface as warnings instead of being remapped.
23
+ - `provenance.workspaceResolution` and `provenance.dependencyResolution` are emitted whenever the new target kinds are used. The new target kinds and the `manage-cache` workspace integration are scoped to the three tools whose input shape carries a `target` field; `find-class`, `search-class-source`, `list-artifact-files`, and `find-mapping` keep their existing `artifactId` / `version`-based shapes unchanged in this release.
24
+ - `manage-cache` exposes a new `cacheKinds: "workspace"` for the in-memory `WorkspaceContextCache`. Listing returns one entry per cached `projectPath`; `delete` invalidates entries by `selector.projectPath`. `prune` / `rebuild` / `verify` are no-ops for this kind.
25
+ - `ProblemDetails.exampleCalls?: Array<{ tool: string; params: Record<string, unknown>; reason: string }>` — additive always-valid fallback emitted when the primary `suggestedCall` payload fails the schema-validation gate. Each entry already passed `getToolSchema(tool).safeParse(params)` at construction time and is safe for the agent to re-call as-is. Construction sites supply alternatives via the `examples: [{ params, reason }]` argument to `buildSuggestedCall(...)`.
26
+ - `tool-schema-registry` runtime registry (internal module exporting `registerToolSchema` / `getToolSchema` / `validateToolParams` / `listRegisteredTools`) populated at server startup from the same zod schemas already used by the `server.tool(...)` registrations. Powers the new `suggestedCall` validation gate; a registry-completeness test in `tests/tool-schema-registry.test.ts` asserts every tool name from `EXPECTED_TOOLS` has a registered schema.
27
+ - `batch-class-source`, `batch-class-members`, `batch-symbol-exists`, and `batch-mappings` (4 new tools) read source / list members / probe symbol existence / translate mappings for fixed shortlists (1..50 entries) in a single call. Each batch shares one resolved artifact (`summary.sharedArtifactId`) instead of paying N artifact-resolution round-trips, dispatches per entry through the existing bounded worker pool (`concurrency: 1..8`, default 4), and returns a per-entry `{ index, status: "ok" | "error", result?, error?, warnings, durationMs }` envelope plus an aggregate `summary: { total, ok, error }`. `failFast: true` halts dispatch on the first entry error — un-started entries are returned with `error.code === "ERR_BATCH_ABORTED"` while in-flight workers complete (no AbortSignal wiring). `compact: true` (default) applies the matching single-tool's compact projection per entry. Per-entry errors carry a `suggestedCall` proposing the matching single tool (`get-class-source` / `get-class-members` / `check-symbol-exists` / `find-mapping`) with the shared artifact threaded through; the suggestion is validated through the same schema gate as single-tool errors. `batch-symbol-exists` accepts only `target.kind="workspace" | "version"` because library/coordinate/jar artifacts carry library versions, not Minecraft versions, which would corrupt the mapping query (rejected at the schema gate with `ERR_INVALID_INPUT`). `batch-mappings` requires top-level `version` (single-version batch); per-entry `version` is rejected as an unrecognized key. `batch-class-source` rejects duplicate `entries[].outputFile` values at the schema gate with `ERR_INVALID_INPUT` (`fieldErrors[0].path === "entries.<i>.outputFile"`), since concurrent worker dispatch would otherwise race on the same file. The shared envelope, `failFast` semantics, per-entry retry mapping, and disabled-tool envelope shape are documented in the new `Batch lookup contract` section of `docs/tool-reference.md`. Set `BATCH_TOOLS_OFF=1` to remove all four tools from `tools/list` (rollback path); direct calls then return the SDK "Tool not found" tool-result envelope (`{ content: [{ type: "text", text: "MCP error -32602: Tool <name> not found" }], isError: true }`), so callers cannot rely on `error.code === "ERR_*"` while the toggle is set.
28
+
29
+ ### Changed
30
+ - `ERR_MAPPING_NOT_APPLIED` now returns a `suggestedCall` of `{ target: { kind: "workspace" }, projectPath, mapping: <detected> }` whenever `projectPath` is supplied, the workspace's compile mapping is non-obfuscated, the failed target was `target.kind="version"`, AND the workspace's detected `minecraftVersion` matches the failed `value`. Coordinate, jar, synthesized dependency failures, and version-mismatched workspaces all keep the legacy obfuscated retry so the suggested retry never silently redirects to a different artifact. Cache hits short-circuit; cache misses now run bounded detection of both compile mapping and Minecraft version (build.gradle / gradle.properties reads only) before deciding whether to suggest the workspace retry. Set `WORKSPACE_FALLBACK_LEGACY=1` to restore the previous obfuscated retry shape.
31
+ - `target.kind="workspace"` now honors a top-level `scope` field. The precedence is `target.scope` → top-level `scope` → loader-derived default (`merged` when a loader is detected, otherwise `vanilla`); previously the loader-derived default silently shadowed the caller's top-level `scope`.
32
+ - Dependency targets (`target.kind="dependency"`) no longer fail with `ERR_MAPPING_NOT_APPLIED` when the caller asks for `mojang` / `intermediary` / `yarn` against a binary-only artifact. The synthesizer keeps `forceBinaryRemapDisabled=true`, but the resolver now bypasses Mojang remap enforcement on dependency-origin failures, returns the JAR with `mappingApplied: "obfuscated"` and `qualityFlags: ["dependency-mapping-unverified"]`, and surfaces a warning that the requested mapping is not enforced. Callers MUST treat the artifact as namespace-unknown and validate symbol availability themselves; `mappingApplied` is no longer fabricated to the requested mapping value.
33
+ - `target.kind="dependency"` now rejects unsafe Maven version tokens before synthesizing the coordinate. `gradle.properties` values that contain path separators, `..`, NUL, control characters, or any character outside `[A-Za-z0-9._+-]` are filtered out (the next configured property key is tried instead, with the rejection recorded under `attempts[]` as `gradle.properties:<key>:rejected-unsafe-version`). Modules-2 directory entries that fail the same validation are excluded from candidates. Explicit `target.version` strings that fail validation raise `ERR_INVALID_INPUT` with `fieldErrors[0].path === "target.version"`.
34
+ - `target.kind="workspace"` no longer falls back to the latest stable Minecraft version when `gradle.properties` does not declare one. Both `strict: true` and `strict: false` now raise `ERR_WORKSPACE_VERSION_UNRESOLVED` when the workspace version cannot be detected, so callers cannot silently resolve and index the wrong Minecraft API surface. The `details.strict` flag records which strict value was supplied.
35
+ - `target.kind="dependency"` no longer picks the semver-newest entry when `~/.gradle/caches/modules-2/files-2.1/<group>/<name>/` contains multiple cached versions. The synthesizer now raises `ERR_DEPENDENCY_VERSION_UNRESOLVED` with `details.ambiguous=true` and `details.candidatesSeen[]` enumerating the cached versions, so a global cache populated by unrelated projects cannot resolve a version the workspace does not actually use. Single-entry caches and `gradle.properties` matches keep their previous behavior.
36
+ - `get-class-members` now passes the caller's raw `mapping` (preserving `undefined`) into the resolver, mirroring `get-class-source`. A workspace target with an omitted `mapping` now resolves through the workspace's detected compile mapping instead of the obfuscated normalization, eliminating the silent obfuscated-result regression.
37
+ - When the worker process exits while handling `tools/call`, the supervisor returns a structured `{ error, meta }` envelope (synthetic `CallToolResult` with `isError: true`) instead of the previous raw JSON-RPC `-32603` error. Non-`tools/call` requests (`initialize`, `tools/list`, etc.) still receive the legacy envelope.
38
+ - `validate-mixin` runs each stage against an independent soft-deadline. When the `target-lookup` stage exhausts its budget mid-loop, the response is `validationStatus: "partial"` with completed targets retained and remaining ones recorded as `deferred-budget`. Pre-parse stages that exhaust their budget surface as `ERR_STAGE_BUDGET_PRE_PARSE` errors.
39
+ - `validate-mixin` budget-driven partial results no longer trigger the `loom-first → maven-first` retry heuristic. The retry path targets mapping infrastructure failures, not soft-deadline cuts; without this guard a stage-budget partial would re-run the full validation under `maven-first` and defeat the cooperative-deadline contract.
40
+ - `stdio-supervisor` records one worker-exit timestamp per `tool` per actual exit, regardless of how many concurrent `tools/call` requests were in flight. Per-pending-request recording inflated `recentRestarts` to N for a single crash and produced false `retryRecommendation: "report-bug"` under concurrent load.
41
+ - `validate-mixin` `targetOutcomes[]` now emits the `tool-issue` status with a `reason` for completed-but-unreliable targets (mapping / signature / remap failures). Previously the per-target outcome was unconditionally `status: "ok"` even when the underlying target classified as `validation-incomplete`, hiding per-target failures from `includeIssues=false` callers. Whole member-remap exceptions (the entire batch failing) now also flag the target as `tool-issue` with `reason: "member-remap-failed-whole"`; previously only per-member remap failures triggered the classifier.
42
+ - `stdio-supervisor` `lastStageStartedAt` is now refreshed only when the worker emits a different stage name (or on the first emit). Per-target progress notifications during `target-lookup` no longer reset the stage timer, so synthetic worker-restart envelopes report `meta.restart.lastStageElapsedMs` measured from stage entry — not from the most recent per-target update — restoring the documented diagnostic contract.
43
+ - `validate-mixin` now enforces the `inputValidation` stage budget (default 5_000 ms): a slow `sourcePath` read, version normalization, or related input-stage I/O surfaces as `ERR_STAGE_BUDGET_PRE_PARSE` with `failedStage: "input-validation"` instead of hanging the call. Previously the budget was declared in `MixinStageBudgets` but never checked.
44
+ - Synthetic worker-restart envelopes no longer expose `error.suggestedCall` when redaction modified the captured tool arguments (truncated long strings, redacted secret-bearing keys, or trimmed arrays / objects). The redacted form is still emitted on the diagnostic-only `meta.restart.redactedToolArgs` field along with `meta.restart.redactedToolArgsModified` so debug surfaces can inspect the placeholder values without granting them retry semantics.
45
+ - Every `ProblemDetails.suggestedCall` payload an agent receives is now validated against the registered zod schema for the named tool BEFORE leaving the process. Invalid payloads (e.g. the bug shape where `target.value` held a JSON-stringified inner target, producing chained `ERR_INVALID_INPUT` on retry) are dropped from the published envelope; when the gate drops a primary suggestion, `error.hints` gains the literal sentence `"suggested call payload failed schema validation; using fallback examples"` so the caller can observe the elision. Construction sites can pass `examples: [{ params, reason }]` to the new `buildSuggestedCall(...)` helper to surface always-valid fallbacks via `error.exampleCalls[]`. `suggestedCall.params` is published byte-identical to the caller-supplied object — the gate never injects schema defaults into the published payload. Set `SUGGESTED_CALL_VALIDATE_OFF=1` at process start to bypass the gate entirely (raw payloads pass through; the fallback hint is not added) as an emergency rollback path.
46
+ - The `validate-mixin` issue construction (`mixin-validator.ts` × 6 sites) and the supervisor's worker-restart envelope synthesis (`stdio-supervisor.ts` × 1 site) now route their result-level `suggestedCall` payloads through the same `buildSuggestedCall(...)` schema gate as error-side suggestions. Previously these 7 assignment-style construction sites (`obj.suggestedCall = { tool, params }`) bypassed the gate's literal-only invariant test, so a malformed payload would have been published unchanged. The invariant test (`tests/suggested-call-invariant.test.ts`) now also rejects assignment-style construction in `src/`, with `[literal]` / `[assign]` markers in offender reports.
47
+ - `buildSyntheticCallToolResult` now skips the `suggestedCall` synthesis entirely when `ctx.toolName` is missing (the supervisor's `tool = ctx.toolName ?? "unknown"` fallback path). Previously the synthesized `tool: "unknown"` flowed through `buildSuggestedCall`'s fail-open unknown-tool branch and published a `suggestedCall: { tool: "unknown", params: {...} }` payload that was not re-callable by the agent — violating the gate's stated invariant. The diagnostic-only `meta.restart.redactedToolArgs` field still surfaces the args so debug surfaces can inspect them. The `buildSuggestedCall` helper's fail-open behavior for unregistered tools is preserved because many service-level tests exercise code without booting `src/index.ts` and expect the fail-open contract; the fix is targeted at the one production code path (the supervisor) that was actually leaking unre-callable payloads.
48
+ - `batch-class-source`'s schema-level duplicate-`outputFile` guard now normalizes paths via `path.resolve` (mirroring the writer's `getClassSource` semantics) before comparing entries. Previously the guard keyed by `entry.outputFile.trim()` only, so semantically-equivalent aliases (`out.java` vs `./out.java` vs `dir/../out.java`) bypassed the check and concurrent writes still raced on the same physical file. The error message now cites the canonical resolved path so the user sees what collided.
49
+ - `errorToBatchEntryProblem` for non-`AppError` failures now emits a fixed sanitized `detail: "Unexpected server error."` to clients (matching the single-tool `mapErrorToProblem` generic-error contract) and logs the raw message server-side (`log("error", "batch.entry.unhandled", { instance, reason })`) for diagnostics. Previously the public per-entry `error.detail` carried `Error.message` verbatim, which could expose internal filesystem paths, parser internals, or assertion text through the public envelope while the top-level batch call still returned a normal result envelope.
50
+ - `buildSyntheticCallToolResult` now also drops `suggestedCall` when `ctx.toolName` is present but unregistered (typo, disabled tool, or version-skewed name from an older server run). The supervisor consults `getToolSchema` itself before invoking the gate; `buildSuggestedCall` continues to fail open for unregistered names by design (service-level tests that do not boot `src/index.ts` rely on the fail-open contract). The diagnostic-only `meta.restart.redactedToolArgs` field still surfaces the args.
51
+ - Batch lookup `summary` now carries `sharedArtifactWarnings?: string[]` populated from the one-shot shared `resolveArtifact` call. Previously the resolution warnings (version approximation, mapping fallback, source coverage gaps, workspace/dependency resolution caveats) were captured into the batch service's local `SharedArtifact` object and discarded, while per-entry calls dispatched by `artifactId` never re-ran the resolution. The field is omitted when no warnings were produced. Applies to `batch-class-source`, `batch-class-members`, and `batch-symbol-exists`; `batch-mappings` does not run a shared artifact resolution and is unaffected.
52
+ - `statusForErrorCode` now maps `ERR_STAGE_BUDGET_PRE_PARSE` to HTTP-style status 408 (Request Timeout). Previously the code fell through to status 500, misclassifying a controlled `validate-mixin` budget exhaustion (caller-recoverable via `mixinConfigPath` shrink, budget raise, or `MIXIN_STAGE_BUDGETS_OFF=1`) as an internal server failure.
53
+ - `statusForErrorCode` now maps `ERR_WORKSPACE_VERSION_UNRESOLVED` and `ERR_DEPENDENCY_VERSION_UNRESOLVED` to HTTP-style status 422 (Unprocessable Entity). Previously they fell through to status 500, misclassifying a caller-recoverable input gap (caller can retry by passing `target.kind="version"` with an explicit Minecraft version, or by pointing `projectPath` at a workspace whose `gradle.properties` carries `minecraft_version`) as an internal server failure. `batch-symbol-exists` is the most likely surface for this gap because workspace targets without a detectable Minecraft version are the new path's primary failure mode.
54
+
55
+ ## [4.0.0] - 2026-04-18
56
+
57
+ ### Changed
58
+ - BREAKING: `resolve-artifact`, `find-mapping`, `resolve-method-mapping-exact`, `resolve-workspace-symbol`, and `check-symbol-exists` now default `compact` to `true` (was `false`). Pass `compact: false` to restore the full diagnostic shape.
59
+ - BREAKING: `find-mapping`, `resolve-method-mapping-exact`, `resolve-workspace-symbol`, `check-symbol-exists`, and `analyze-symbol` now default `maxCandidates` to `5` (was `200`; `200` is still the upper bound).
60
+ - BREAKING: `find-mapping` now defaults `signatureMode` to `"name-only"` (was effectively `"exact"`). `kind="method"` lookups without a `descriptor` no longer fail; pass `signatureMode: "exact"` explicitly for strict descriptor matching.
61
+ - BREAKING: `resolve-artifact` with `mapping="mojang"` and `target.kind="version"` against a still-obfuscated Minecraft version now succeeds by tiny-remapping the binary jar and decompiling the result, tagged `qualityFlags: ["binary-remapped", "decompiled"]` and `provenance.transformChain: ["binary-remap:obf->mojang", "decompile:vineflower"]`. The remapped jar is cached at `<cacheDir>/remapped/<artifactId>.jar`. Coordinate and jar targets are not eligible. `ERR_MAPPING_NOT_APPLIED` is still raised when tiny-remapper, the Mojang tiny mapping file, or the version's Mojang mappings are missing.
62
+ - `find-mapping` and `check-symbol-exists` accept an empty `descriptor` as equivalent to omitting it.
63
+ - Compact mode on mapping tools keeps the top three unresolved candidates with full metadata and slims the tail to `{kind, symbol, owner, name, descriptor, confidence, matchKind}`. A new `candidateDetailsTruncated` flag signals this slim, distinct from the existing `candidatesTruncated` (upstream truncation).
64
+
65
+ ### Added
66
+ - `find-mapping` exposes `signatureMode` as a first-class input. `"exact"` matches `owner + name + descriptor`; `"name-only"` matches by `owner + name` and ranks overloads by confidence. `resolve-method-mapping-exact` retains strict exact-triple semantics.
67
+ - `get-class-source`, `get-class-members`, `search-class-source`, and `list-artifact-files` accept an opt-in `compact` parameter (default `false`). It strips diagnostic envelopes (`provenance`, `artifactContents`, `qualityFlags`, plus `context` for `get-class-members`) while preserving primary payloads (`sourceText`, `members`/`counts`, `hits`, `items`).
68
+ - `manage-cache` exposes the Mojang remapped jar under a new `binary-remap` cache kind with `selector.artifactId` filtering. LRU eviction unlinks the matching remapped jar so cache accounting stays consistent.
69
+ - `validate-mixin` top-level errors now carry `failedStage` on the `ProblemDetails` envelope (one of `input-validation | resolve | mapping-health | parse | target-lookup`), so callers can branch without parsing `message`.
70
+ - `validate-mixin` `quickSummary` appends notes when `provenance.scopeFallback` fires or `toolHealth.overallHealthy` is false.
71
+ - Ambiguous mapping responses in `find-mapping`, `resolve-method-mapping-exact`, and `check-symbol-exists` include recovery guidance in `warnings` (descriptor hints, `signatureMode="exact"` retries, `disambiguation.ownerHint`, raising `maxCandidates`).
72
+ - `docs/tool-reference.md` includes a "Which Tool for Which Question" decision table; `README.md` links to it from the Documentation section.
73
+
74
+ ### Fixed
75
+ - `check-symbol-exists` projects the caller's JVM descriptor to the obfuscated namespace before filtering method overloads, so `signatureMode="exact"` retries work with descriptors that reference remapped Minecraft classes. Mixed descriptors such as `(Lnet/minecraft/world/item/ItemStack;Ljava/lang/String;)V` resolve via partial projection instead of falling back to the unprojected source descriptor.
76
+ - `find-mapping` with `signatureMode="exact"` and `kind="method"` filters resolved candidates by the (projected) requested descriptor. A caller who supplied `foo(Z)V` is no longer told that `foo(I)V` is the exact mapping.
77
+ - Compact mode slim candidate projection retains `kind` and `symbol` alongside `owner`, `name`, `descriptor`, `confidence`, and `matchKind`, so clients keying off `kind` or `symbol` are no longer silently broken.
78
+ - `normalizeMethodDescriptor` rejects descriptors with more than 255 leading `[` (JVM §4.3.2) and rejects malformed shapes such as `()`, `(I)`, and `(L;)V` with `ERR_INVALID_INPUT`.
79
+
8
80
  ## [3.2.0] - 2026-04-12
9
81
 
10
82
  ### Added
package/README.md CHANGED
@@ -5,26 +5,27 @@
5
5
  [![Node.js >=22](https://img.shields.io/badge/node-%3E%3D22-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
- **[日本語](docs/README-ja.md)** | English
8
+ **English** | [日本語](docs/README-ja.md)
9
+
10
+ > **Note**: This project is entirely vibe-coded — built with AI-assisted development without formal specs.
9
11
 
10
12
  ---
11
13
 
12
- `@adhisang/minecraft-modding-mcp` is an [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that gives AI assistants structured access to Minecraft source code, mappings, mod JARs, registry data, and validation workflows.
14
+ `@adhisang/minecraft-modding-mcp` is an MCP server for AI-assisted Minecraft modding workflows, built on the [Model Context Protocol](https://modelcontextprotocol.io/). Use it when an agent needs to inspect Minecraft source, resolve mappings, compare versions, analyze mod JARs, validate Mixin, Access Widener, or Access Transformer files, or work with NBT and registry data from an MCP client.
13
15
 
14
- [MCP](https://modelcontextprotocol.io/) is an open protocol that lets AI assistants call external tools through a structured interface. This server works with Claude Desktop, Claude Code, VS Code, Codex CLI, Gemini CLI, and other MCP-capable clients.
16
+ It runs over stdio and works with Claude Desktop, Claude Code, VS Code, Codex CLI, Gemini CLI, and other MCP-capable clients.
15
17
 
16
- **35 tools** (6 entry + 29 expert) | **7 resources** | **4 namespace mappings** | **SQLite-backed cache**
18
+ **37 tools** (6 entry + 31 expert) | **7 resources** | **4 namespace mappings** | **SQLite-backed cache**
17
19
 
18
20
  ## Features
19
21
 
20
- - **Source Exploration**: browse and search decompiled Minecraft source code with line-level precision and cursor-paginated file listing
21
- - **Multi-Mapping Conversion**: translate class, field, and method names between `obfuscated`, `mojang`, `intermediary`, and `yarn`
22
- - **Version Comparison**: diff class signatures and registry entries between Minecraft versions
23
- - **Mod JAR Analysis**: extract metadata, dependencies, entrypoints, Mixin configs, and packaged Access Transformer paths from Fabric, Forge, and NeoForge mod JARs
24
- - **Mixin, Access Widener, and Access Transformer Validation**: validate source, `.accesswidener`, and Forge/NeoForge access transformer files against a target Minecraft version
25
- - **NBT Round-Trip**: decode NBT binary to typed JSON, apply RFC 6902 patches, and encode it back to NBT
26
- - **Registry Data and Runtime Metrics**: query generated registry snapshots and inspect cache and latency counters
27
- - **MCP Resources**: expose versions, class source, artifact metadata, and mappings through URI-based resources
22
+ - **Source exploration**: browse, list, and search decompiled Minecraft source with line-level context
23
+ - **Mapping-aware symbol work**: convert class, field, and method names between `obfuscated`, `mojang`, `intermediary`, and `yarn`
24
+ - **Version comparison**: compare class signatures, registry entries, and migration-oriented summaries across Minecraft versions
25
+ - **Mod JAR analysis**: read Fabric, Forge, and NeoForge metadata, entrypoints, Mixin configs, dependencies, source, and remap previews
26
+ - **Project validation**: validate Mixin source, `.accesswidener` files, and Forge/NeoForge Access Transformer files against the target version
27
+ - **NBT, registry, cache, and diagnostics**: patch NBT payloads, inspect generated registry data, and manage cache/runtime state
28
+ - **MCP resources**: expose versions, class source, artifact metadata, and mappings through URI-based resources
28
29
 
29
30
  ## Quick Start
30
31
 
@@ -41,18 +42,27 @@ Start the server locally:
41
42
  npx -y @adhisang/minecraft-modding-mcp
42
43
  ```
43
44
 
44
- If automatic JAR downloads are blocked in your environment, set `MCP_VINEFLOWER_JAR_PATH` and `MCP_TINY_REMAPPER_JAR_PATH` in the client configuration.
45
+ Use this same command in MCP client configs. If automatic JAR downloads are blocked in your environment, set `MCP_VINEFLOWER_JAR_PATH` and `MCP_TINY_REMAPPER_JAR_PATH` there.
45
46
 
46
47
  ### Client Setup
47
48
 
48
- CLI clients:
49
+ CLI clients can register the package command directly.
50
+
51
+ Claude Code:
49
52
 
50
- - `Claude Code`: `claude mcp add minecraft-modding -- npx -y @adhisang/minecraft-modding-mcp`
51
- - `OpenAI Codex CLI`: `codex mcp add minecraft-modding -- npx -y @adhisang/minecraft-modding-mcp`
53
+ ```bash
54
+ claude mcp add minecraft-modding -- npx -y @adhisang/minecraft-modding-mcp
55
+ ```
56
+
57
+ OpenAI Codex CLI:
58
+
59
+ ```bash
60
+ codex mcp add minecraft-modding -- npx -y @adhisang/minecraft-modding-mcp
61
+ ```
52
62
 
53
63
  Run `claude mcp list` or `codex mcp list` after registration to verify the server is available.
54
64
 
55
- The stdio transport auto-detects both newline-delimited and `Content-Length` framing, so the same server command works across Codex and standard MCP clients.
65
+ The stdio transport auto-detects newline-delimited and `Content-Length` framing, so the same server command works across Codex and standard MCP clients.
56
66
 
57
67
  #### Claude Desktop
58
68
 
@@ -126,9 +136,9 @@ Pass environment variables to override defaults:
126
136
 
127
137
  ## Start Here
128
138
 
129
- These six top-level workflow tools cover the common workflows and return summary-first results, so they are the best default starting points for agents and MCP clients.
139
+ These six top-level workflow tools cover the common paths and return summary-first results. They are the best default starting points for agents and MCP clients.
130
140
 
131
- All six return `result.summary` first, and can include `summary.nextActions` when there is a clear follow-up step. Use the table for tool selection, then use the examples and reference docs for exact payloads.
141
+ All six return `result.summary` first and can include `summary.nextActions` when there is a clear follow-up step. Pick the tool from the table, then use the examples and reference docs for exact payloads.
132
142
 
133
143
  | Tool | Start here for |
134
144
  | --- | --- |
@@ -141,16 +151,14 @@ All six return `result.summary` first, and can include `summary.nextActions` whe
141
151
 
142
152
  ### Workflow Notes
143
153
 
144
- Keep only the high-frequency notes here. For the full pitfall list, exact contract details, migration notes, and environment variables, see [docs/tool-reference.md](docs/tool-reference.md).
154
+ 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).
145
155
 
146
156
  - `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.
147
157
  - 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.
148
158
  - `trace-symbol-lifecycle` expects `Class.method` in `symbol`. Keep exact overload matching in the separate `descriptor` field.
149
- - Workspace inspection can still confirm vanilla classes when source coverage is partial, and `inspect-minecraft task="list-files"` reports a partial result with follow-up guidance when that happens.
150
- - `check-symbol-exists` and `analyze-symbol task="exists"` now validate `mojang` lookups on unobfuscated releases such as `26.1+` against runtime bytecode when no mapping graph exists, preserve the original `mapping_unavailable` result if the runtime JAR itself cannot be resolved, and return a targeted warning when callers provide only a short class name.
151
- - `analyze-mod` and `validate-project` still require structured `subject` objects and canonical `include` groups, but stale string-subject or domain-include payloads now return `ERR_INVALID_INPUT` with a retryable `suggestedCall`.
152
- - `validate-project task="project-summary"` now pre-resolves `preferProjectVersion=true` consistently across discovered Access Widener, Access Transformer, and Mixin checks, and blocks with version-agnostic recovery guidance when discovered validators need a version but neither the request nor `gradle.properties` can supply one.
153
- - Local source-jar probing, decompiled mod source reads, and workspace discovery now avoid synchronous hot-path ZIP/file scans and use bounded concurrent reads where safe, so cold `resolve-artifact`, `analyze-mod`, and `validate-project` workflows stay more responsive on larger jars and multi-module workspaces.
159
+ - 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.
160
+ - `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`.
161
+ - `validate-project task="project-summary"` propagates `preferProjectVersion=true` across discovered Mixin, Access Widener, and Access Transformer checks. If no version can be resolved from the request or `gradle.properties`, the summary returns recovery guidance instead of guessing.
154
162
 
155
163
  ### Inspect Minecraft source from a version
156
164
 
@@ -232,7 +240,7 @@ Workspace summaries still default to discovering mixins and access wideners. Add
232
240
  ## Documentation
233
241
 
234
242
  - [Detailed example requests](docs/examples.md) for copyable payloads and common workflows
235
- - [Tool and configuration reference](docs/tool-reference.md) for exact inputs, outputs, resource behavior, environment variables, and migration notes
243
+ - [Tool and configuration reference](docs/tool-reference.md) for exact inputs, outputs, resource behavior, environment variables, and migration notes. Start with the [Which Tool for Which Question](docs/tool-reference.md#which-tool-for-which-question) decision table when you are not sure which tool to call.
236
244
  - [日本語 README](docs/README-ja.md) for a Japanese onboarding overview
237
245
 
238
246
  ## Tool Surface
@@ -270,7 +278,7 @@ Tools for browsing Minecraft versions, resolving source artifacts, and reading o
270
278
  | `index-artifact` | Rebuild indexed metadata for an existing artifact |
271
279
  <!-- END GENERATED TOOL TABLE: source-exploration -->
272
280
 
273
- For unobfuscated releases such as `26.1+`, `mapping="mojang"` now uses the runtime/decompile path directly for version and versioned-coordinate targets and skips Loom source-jar discovery entirely, while `intermediary` and `yarn` still fall back to `obfuscated` with a warning.
281
+ 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.
274
282
 
275
283
  ### Version Comparison & Symbol Tracking
276
284
 
@@ -298,7 +306,7 @@ Tools for converting symbol names between namespaces and checking symbol existen
298
306
  | `check-symbol-exists` | Check whether a class, field, or method exists in a namespace |
299
307
  <!-- END GENERATED TOOL TABLE: mapping-symbols -->
300
308
 
301
- `resolve-artifact`, `find-mapping`, `resolve-method-mapping-exact`, `resolve-workspace-symbol`, and `check-symbol-exists` accept an optional `compact` parameter (default `false`). When `true`, empty arrays, null values, and empty objects are stripped from the top-level response to reduce token overhead. For `resolve-artifact`, compact mode additionally omits diagnostic fields (`provenance`, `artifactContents`, `sampleEntries`, `adjacentSourceCandidates`, `binaryJarPath`, `coordinate`, `repoUrl`, `resolvedSourceJarPath`), returning only the essential fields needed for downstream tool calls. For mapping tools, compact mode omits the redundant `candidates` array when the result is a single full-confidence exact-match resolution (`resolved=true`, `resolvedSymbol` present, `candidates.length=1`, `candidateCount=1`, `!candidatesTruncated`, `matchKind="exact"`, `confidence` missing or `1`).
309
+ Several lookup tools support `compact` result shaping for shorter responses. See [docs/tool-reference.md](docs/tool-reference.md) for defaults and the full per-tool field list.
302
310
 
303
311
  ### NBT Utilities
304
312
 
@@ -328,16 +336,15 @@ Tools for extracting metadata from mod JARs, decompiling mod source, searching m
328
336
 
329
337
  ### Validation
330
338
 
331
- Tools for validating Mixin source, Access Widener files, and Forge/NeoForge Access Transformer files against a target Minecraft version.
332
- `validate-access-widener` keeps vanilla bytecode validation by default, and now also supports runtime-aware validation through `projectPath`, `scope`, and `preferProjectVersion`, returning runtime `provenance` plus per-entry `resolvedRuntimeAccess` evidence when that mode is used. Runtime-aware method validation now preserves remapped JVM descriptors across namespace changes, searches Loom tiny mappings from workspace and Gradle user-home caches, and prefers explicit `*merged-intermediary*` / `*merged-mojang*` jars over ambiguous `minecraft-merged.jar` candidates.
333
- `validate-access-transformer` infers `atNamespace` from Forge or NeoForge workspace context when `projectPath` is provided, validates packaged or inline AT content, and uses loader/runtime artifacts for `scope="loader"` instead of treating loader as a merged-only alias.
339
+ Tools for validating Mixin source, Access Widener files, and Forge/NeoForge Access Transformer files against a target Minecraft version. Workspace options let validation use loader/runtime context when a project path is available.
334
340
 
335
341
  <!-- BEGIN GENERATED TOOL TABLE: validation -->
336
342
  | Tool | Purpose |
337
343
  | --- | --- |
338
- | `validate-mixin` | Validate Mixin source against a target Minecraft version |
344
+ | `validate-mixin` | Validate Mixin source against a target Minecraft version (returns `validationStatus: "partial"` with `targetOutcomes` when a stage budget defers work) |
339
345
  | `validate-access-widener` | Validate Access Widener content against a target Minecraft version, optionally using runtime-aware Loom artifacts |
340
346
  | `validate-access-transformer` | Validate Access Transformer content against a target Minecraft version, optionally using Forge/NeoForge runtime artifacts |
347
+ | `verify-mixin-target` | Single-call probe for owner / member existence with `@Shadow` / `@Accessor` / `@Invoker` advice |
341
348
  <!-- END GENERATED TOOL TABLE: validation -->
342
349
 
343
350
  ### Registry & Diagnostics
@@ -351,6 +358,19 @@ Tools for querying generated registry data and inspecting server runtime state.
351
358
  | `get-runtime-metrics` | Inspect runtime metrics and latency snapshots |
352
359
  <!-- END GENERATED TOOL TABLE: registry-diagnostics -->
353
360
 
361
+ ### Batch Lookup
362
+
363
+ Tools that share one resolved artifact or Minecraft version across a fixed shortlist. Results include one status per item plus an aggregate `summary`. See [Batch lookup contract](docs/tool-reference.md#batch-lookup-contract) for failure handling and retry mapping.
364
+
365
+ <!-- BEGIN GENERATED TOOL TABLE: batch-lookup -->
366
+ | Tool | Purpose |
367
+ | --- | --- |
368
+ | `batch-class-source` | Read source for many classes against one shared resolved artifact (1..50 entries per call) |
369
+ | `batch-class-members` | List members for many classes against one shared resolved artifact (1..50 entries per call) |
370
+ | `batch-symbol-exists` | Probe symbol existence for many entries against one shared Minecraft-version artifact (workspace / version targets only) |
371
+ | `batch-mappings` | Translate many symbols across mapping namespaces with one shared Minecraft version (no shared artifact) |
372
+ <!-- END GENERATED TOOL TABLE: batch-lookup -->
373
+
354
374
  Detailed parameter constraints, migration notes, resource behavior, and the full environment-variable matrix live in [docs/tool-reference.md](docs/tool-reference.md).
355
375
 
356
376
  ## Development
@@ -0,0 +1,29 @@
1
+ export declare const SUGGESTED_CALL_VALIDATE_OFF: boolean;
2
+ export type SuggestedCallExample = {
3
+ params: unknown;
4
+ reason: string;
5
+ };
6
+ export type SuggestedCallSpec = {
7
+ tool: string;
8
+ params: unknown;
9
+ examples?: SuggestedCallExample[];
10
+ };
11
+ export type ValidatedSuggestedCall = {
12
+ tool: string;
13
+ params: Record<string, unknown>;
14
+ };
15
+ export type ValidatedExampleCall = {
16
+ tool: string;
17
+ params: Record<string, unknown>;
18
+ reason: string;
19
+ valid: true;
20
+ };
21
+ export type SuggestedCallOutput = {
22
+ suggestedCall?: ValidatedSuggestedCall;
23
+ exampleCalls?: ValidatedExampleCall[];
24
+ /** Set to `true` when the caller supplied `params` but the gate dropped it.
25
+ * Spread into AppError `details`; `mapErrorToProblem` reads the marker to
26
+ * append the fallback hint to `error.hints` and strips it before emission. */
27
+ _suggestedCallPrimaryDropped?: true;
28
+ };
29
+ export declare function buildSuggestedCall(spec: SuggestedCallSpec): SuggestedCallOutput;
@@ -0,0 +1,58 @@
1
+ import { getToolSchema, validateToolParams } from "./tool-schema-registry.js";
2
+ export const SUGGESTED_CALL_VALIDATE_OFF = process.env.SUGGESTED_CALL_VALIDATE_OFF === "1";
3
+ function asParamsRecord(params) {
4
+ if (typeof params !== "object" || params === null || Array.isArray(params)) {
5
+ return undefined;
6
+ }
7
+ return params;
8
+ }
9
+ export function buildSuggestedCall(spec) {
10
+ if (SUGGESTED_CALL_VALIDATE_OFF) {
11
+ const rawParams = asParamsRecord(spec.params);
12
+ if (rawParams) {
13
+ return { suggestedCall: { tool: spec.tool, params: rawParams } };
14
+ }
15
+ return {};
16
+ }
17
+ const primaryParams = asParamsRecord(spec.params);
18
+ if (primaryParams) {
19
+ // Unknown-tool fail-open: the registry is populated at index.ts startup;
20
+ // service-level tests that do not boot index.ts run with an empty
21
+ // registry and rely on this pass-through. Callers that synthesize a tool
22
+ // name from runtime data (e.g. `?? "unknown"`) MUST skip this helper, or
23
+ // a non-callable payload escapes via this branch.
24
+ if (!getToolSchema(spec.tool)) {
25
+ return { suggestedCall: { tool: spec.tool, params: primaryParams } };
26
+ }
27
+ const primary = validateToolParams(spec.tool, primaryParams);
28
+ if (primary.valid) {
29
+ return { suggestedCall: { tool: spec.tool, params: primaryParams } };
30
+ }
31
+ }
32
+ const droppedMarker = spec.params !== undefined ? { _suggestedCallPrimaryDropped: true } : {};
33
+ if (spec.examples && spec.examples.length > 0) {
34
+ const validated = spec.examples
35
+ .map((example) => {
36
+ const exampleParams = asParamsRecord(example.params);
37
+ if (!exampleParams) {
38
+ return null;
39
+ }
40
+ const result = validateToolParams(spec.tool, exampleParams);
41
+ if (!result.valid) {
42
+ return null;
43
+ }
44
+ return {
45
+ tool: spec.tool,
46
+ params: exampleParams,
47
+ reason: example.reason,
48
+ valid: true
49
+ };
50
+ })
51
+ .filter((entry) => entry !== null);
52
+ if (validated.length > 0) {
53
+ return { exampleCalls: validated, ...droppedMarker };
54
+ }
55
+ }
56
+ return droppedMarker;
57
+ }
58
+ //# sourceMappingURL=build-suggested-call.js.map
@@ -1,5 +1,6 @@
1
1
  import { type PathRuntimeInfo } from "./path-converter.js";
2
- export declare const PUBLIC_CACHE_KINDS: readonly ["artifact-index", "downloads", "mapping", "registry", "decompiled-source", "mod-remap"];
2
+ import { type WorkspaceContextCache } from "./workspace-context-cache.js";
3
+ export declare const PUBLIC_CACHE_KINDS: readonly ["artifact-index", "downloads", "mapping", "registry", "decompiled-source", "mod-remap", "binary-remap", "workspace"];
3
4
  export type PublicCacheKind = (typeof PUBLIC_CACHE_KINDS)[number];
4
5
  export declare const CACHE_HEALTH_STATES: readonly ["healthy", "partial", "stale", "orphaned", "corrupt", "in_use"];
5
6
  export type CacheHealthState = (typeof CACHE_HEALTH_STATES)[number];
@@ -37,6 +38,7 @@ export type CacheRegistryConfig = {
37
38
  cacheDir: string;
38
39
  sqlitePath: string;
39
40
  pathRuntimeInfo?: PathRuntimeInfo;
41
+ workspaceContextCache?: WorkspaceContextCache;
40
42
  };
41
43
  export interface CacheRegistry {
42
44
  summarize(input: {
@@ -4,13 +4,16 @@ import { join, resolve } from "node:path";
4
4
  import { createError, ERROR_CODES } from "./errors.js";
5
5
  import { normalizeOptionalPathForHost } from "./path-converter.js";
6
6
  import Database from "./storage/sqlite.js";
7
+ import { getProcessWorkspaceContextCache } from "./workspace-context-cache.js";
7
8
  export const PUBLIC_CACHE_KINDS = [
8
9
  "artifact-index",
9
10
  "downloads",
10
11
  "mapping",
11
12
  "registry",
12
13
  "decompiled-source",
13
- "mod-remap"
14
+ "mod-remap",
15
+ "binary-remap",
16
+ "workspace"
14
17
  ];
15
18
  export const CACHE_HEALTH_STATES = [
16
19
  "healthy",
@@ -37,6 +40,10 @@ function kindRoot(config, cacheKind) {
37
40
  return join(config.cacheDir, "decompiled");
38
41
  case "mod-remap":
39
42
  return join(config.cacheDir, "remapped-mods");
43
+ case "binary-remap":
44
+ return join(config.cacheDir, "remapped");
45
+ case "workspace":
46
+ return "<in-memory:workspace-context-cache>";
40
47
  }
41
48
  }
42
49
  async function listFilesRecursive(root) {
@@ -414,6 +421,26 @@ async function artifactIndexEntries(config) {
414
421
  db.close();
415
422
  }
416
423
  }
424
+ function workspaceCacheEntries(workspaceCache) {
425
+ const contexts = workspaceCache.list();
426
+ return contexts.map((ctx) => ({
427
+ cacheKind: "workspace",
428
+ entryId: ctx.projectPath,
429
+ path: ctx.projectPath,
430
+ sizeBytes: 0,
431
+ status: "healthy",
432
+ meta: {
433
+ projectPath: ctx.projectPath,
434
+ minecraftVersion: ctx.minecraftVersion,
435
+ compileMapping: ctx.compileMapping,
436
+ loader: ctx.loader,
437
+ detectedAt: new Date(ctx.detectedAt).toISOString(),
438
+ updatedAt: new Date(ctx.detectedAt).toISOString(),
439
+ partial: ctx.partial === true,
440
+ dependencyVersionCount: ctx.dependencyVersions.size
441
+ }
442
+ }));
443
+ }
417
444
  async function fileBackedEntries(config, cacheKind) {
418
445
  const root = kindRoot(config, cacheKind);
419
446
  const files = await listFilesRecursive(root);
@@ -439,25 +466,37 @@ async function fileBackedEntries(config, cacheKind) {
439
466
  inUse: filePath.endsWith(".lock") ||
440
467
  filePath.endsWith(".wal") ||
441
468
  filePath.endsWith(".journal"),
442
- ...(cacheKind === "downloads" || cacheKind === "mod-remap" ? { jarPath: filePath } : {})
469
+ ...(cacheKind === "downloads" || cacheKind === "mod-remap" || cacheKind === "binary-remap"
470
+ ? { jarPath: filePath }
471
+ : {}),
472
+ ...(cacheKind === "binary-remap"
473
+ ? { artifactId: normalizedEntryId.replace(/\.jar$/i, "") }
474
+ : {})
443
475
  }
444
476
  });
445
477
  }
446
478
  return entries;
447
479
  }
448
480
  export function createCacheRegistry(config) {
481
+ const workspaceCache = config.workspaceContextCache ?? getProcessWorkspaceContextCache();
449
482
  async function collectEntries(cacheKinds, selector) {
450
483
  const selectedKinds = cacheKinds?.length ? cacheKinds : [...PUBLIC_CACHE_KINDS];
451
484
  const preparedSelector = prepareSelector(selector, config.pathRuntimeInfo);
452
485
  const now = Date.now();
453
- const entries = await Promise.all(selectedKinds.map((cacheKind) => cacheKind === "artifact-index"
454
- ? artifactIndexEntries(config)
455
- : fileBackedEntries(config, cacheKind)));
486
+ const entries = await Promise.all(selectedKinds.map((cacheKind) => {
487
+ if (cacheKind === "artifact-index") {
488
+ return artifactIndexEntries(config);
489
+ }
490
+ if (cacheKind === "workspace") {
491
+ return Promise.resolve(workspaceCacheEntries(workspaceCache));
492
+ }
493
+ return fileBackedEntries(config, cacheKind);
494
+ }));
456
495
  const enriched = entries
457
496
  .flat()
458
497
  .map((entry) => ({
459
498
  ...entry,
460
- status: deriveEntryStatus(entry, config, now)
499
+ status: entry.cacheKind === "workspace" ? entry.status : deriveEntryStatus(entry, config, now)
461
500
  }));
462
501
  return sortEntries(enriched.filter((entry) => matchesSelector(entry, preparedSelector, config.pathRuntimeInfo)));
463
502
  }
@@ -467,8 +506,17 @@ export function createCacheRegistry(config) {
467
506
  const entries = await collectEntries(selectedKinds, input.selector);
468
507
  const kinds = {};
469
508
  for (const cacheKind of selectedKinds) {
470
- const root = kindRoot(config, cacheKind);
471
509
  const rows = entries.filter((entry) => entry.cacheKind === cacheKind);
510
+ if (cacheKind === "workspace") {
511
+ kinds[cacheKind] = {
512
+ cacheKind,
513
+ entryCount: rows.length,
514
+ totalBytes: 0,
515
+ status: "healthy"
516
+ };
517
+ continue;
518
+ }
519
+ const root = kindRoot(config, cacheKind);
472
520
  kinds[cacheKind] = {
473
521
  cacheKind,
474
522
  entryCount: rows.length,
@@ -511,6 +559,10 @@ export function createCacheRegistry(config) {
511
559
  db?.prepare("DELETE FROM artifacts WHERE artifact_id = ?").run([entry.entryId]);
512
560
  continue;
513
561
  }
562
+ if (entry.cacheKind === "workspace") {
563
+ workspaceCache.invalidate(entry.entryId);
564
+ continue;
565
+ }
514
566
  if (existsSync(entry.path)) {
515
567
  await rm(entry.path, { force: true });
516
568
  }
package/dist/config.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Config } from "./types.js";
1
+ import type { ArtifactTargetKind, Config, MappingVariant } from "./types.js";
2
2
  declare const DEFAULTS: {
3
3
  readonly cacheDir: "~/.cache/minecraft-modding-mcp";
4
4
  readonly sourceRepos: readonly ["https://repo1.maven.org/maven2", "https://maven.fabricmc.net", "https://maven.minecraftforge.net", "https://maven.neoforged.net/releases"];
@@ -24,4 +24,13 @@ declare const DEFAULTS: {
24
24
  };
25
25
  export declare function loadConfig(): Config;
26
26
  export declare function stableArtifactId(parts: string[]): string;
27
+ export interface ArtifactAliasInput {
28
+ artifactId: string;
29
+ kind: ArtifactTargetKind;
30
+ value: string;
31
+ mappingVariant?: MappingVariant;
32
+ resolvedVersion?: string;
33
+ coordinate?: string;
34
+ }
35
+ export declare function buildArtifactAlias(input: ArtifactAliasInput): string;
27
36
  export { DEFAULTS };
package/dist/config.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { homedir } from "node:os";
3
- import { isAbsolute, resolve } from "node:path";
3
+ import { basename, isAbsolute, resolve } from "node:path";
4
4
  import { normalizePathForHost } from "./path-converter.js";
5
5
  const DEFAULTS = {
6
6
  cacheDir: "~/.cache/minecraft-modding-mcp",
@@ -182,5 +182,56 @@ export function stableArtifactId(parts) {
182
182
  .join("|");
183
183
  return createHash("sha256").update(normalizer).digest("hex");
184
184
  }
185
+ // 12 hex chars (48 bits) of artifactId entropy keeps alias collisions astronomically
186
+ // unlikely while staying short enough to copy/paste. A shorter prefix would let two
187
+ // distinct artifacts whose readable tokens (kind, version, mapping, scope, variant)
188
+ // happen to match share an alias and trip the schema-v4 UNIQUE constraint.
189
+ const ARTIFACT_ALIAS_HASH_LEN = 12;
190
+ function slugifyAliasPart(value) {
191
+ return value
192
+ .toLowerCase()
193
+ .replace(/[^a-z0-9]+/g, "-")
194
+ .replace(/^-+|-+$/g, "");
195
+ }
196
+ function aliasJarBase(jarPath) {
197
+ const base = basename(jarPath);
198
+ return base.toLowerCase().endsWith(".jar") ? base.slice(0, -4) : base;
199
+ }
200
+ // Generates a deterministic, human-readable alias canonical to the artifact row.
201
+ // The alias is 1:1 with `artifactId`: only dimensions baked into `artifactId`
202
+ // itself (kind, value/version/coordinate, mappingVariant) appear in the alias,
203
+ // plus a 12-hex-char (48-bit) suffix taken from `artifactId`. Request-level
204
+ // dimensions like `mapping` and `scope` are intentionally excluded — including
205
+ // them would make resolveArtifact rotate the alias for the same row whenever
206
+ // a caller passed a different mapping/scope, breaking aliases already returned
207
+ // to earlier callers. Two distinct artifactIds whose readable tokens collide and
208
+ // whose first 48 hash bits also collide would trip the schema-v4 alias UNIQUE
209
+ // constraint at upsert time — a hard error, not silent drift.
210
+ // Phase 3.1a: display-only. Phase 3.1b stores it for lookup.
211
+ export function buildArtifactAlias(input) {
212
+ const tokens = [];
213
+ if (input.kind === "version") {
214
+ tokens.push("mc", input.resolvedVersion ?? input.value);
215
+ }
216
+ else if (input.kind === "coordinate") {
217
+ tokens.push("coord", input.coordinate ?? input.value);
218
+ }
219
+ else {
220
+ tokens.push("jar", aliasJarBase(input.value));
221
+ }
222
+ // mappingVariant is canonical — `artifactIdForJar` / `artifactIdForCoordinate`
223
+ // bake "mojang-remapped" into the artifactId, so the same artifactId always
224
+ // produces the same alias regardless of how many times resolveArtifact is
225
+ // called. The token here is purely a human-readable hint; the 12-char hash
226
+ // suffix would already separate remapped artifacts from pass-through ones.
227
+ if (input.mappingVariant === "mojang-remapped") {
228
+ tokens.push("remapped");
229
+ }
230
+ tokens.push(input.artifactId.slice(0, ARTIFACT_ALIAS_HASH_LEN));
231
+ return tokens
232
+ .map(slugifyAliasPart)
233
+ .filter(Boolean)
234
+ .join("-");
235
+ }
185
236
  export { DEFAULTS };
186
237
  //# sourceMappingURL=config.js.map