@adhisang/minecraft-modding-mcp 3.1.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +37 -18
  3. package/dist/access-transformer-parser.d.ts +17 -0
  4. package/dist/access-transformer-parser.js +97 -0
  5. package/dist/cache-registry.d.ts +1 -1
  6. package/dist/cache-registry.js +10 -2
  7. package/dist/concurrency.d.ts +1 -0
  8. package/dist/concurrency.js +24 -0
  9. package/dist/config.d.ts +10 -1
  10. package/dist/config.js +52 -1
  11. package/dist/decompiler/vineflower.js +22 -21
  12. package/dist/entry-tools/analyze-mod-service.d.ts +4 -4
  13. package/dist/entry-tools/analyze-symbol-service.d.ts +22 -22
  14. package/dist/entry-tools/analyze-symbol-service.js +13 -2
  15. package/dist/entry-tools/inspect-minecraft-service.d.ts +168 -168
  16. package/dist/entry-tools/inspect-minecraft-service.js +8 -2
  17. package/dist/entry-tools/manage-cache-service.d.ts +4 -4
  18. package/dist/entry-tools/validate-project-service.d.ts +153 -16
  19. package/dist/entry-tools/validate-project-service.js +442 -25
  20. package/dist/gradle-paths.d.ts +4 -0
  21. package/dist/gradle-paths.js +57 -0
  22. package/dist/index.js +148 -30
  23. package/dist/lru-list.d.ts +31 -0
  24. package/dist/lru-list.js +102 -0
  25. package/dist/mapping-pipeline-service.d.ts +12 -1
  26. package/dist/mapping-pipeline-service.js +28 -1
  27. package/dist/mapping-service.d.ts +16 -0
  28. package/dist/mapping-service.js +405 -68
  29. package/dist/minecraft-explorer-service.d.ts +13 -0
  30. package/dist/minecraft-explorer-service.js +8 -4
  31. package/dist/mixin-validator.d.ts +33 -2
  32. package/dist/mixin-validator.js +218 -17
  33. package/dist/mod-analyzer.d.ts +1 -0
  34. package/dist/mod-analyzer.js +17 -1
  35. package/dist/mod-decompile-service.js +4 -4
  36. package/dist/mod-remap-service.js +1 -54
  37. package/dist/mod-search-service.d.ts +1 -0
  38. package/dist/mod-search-service.js +84 -51
  39. package/dist/observability.d.ts +18 -1
  40. package/dist/observability.js +44 -1
  41. package/dist/response-utils.d.ts +69 -0
  42. package/dist/response-utils.js +227 -0
  43. package/dist/source-jar-reader.d.ts +16 -0
  44. package/dist/source-jar-reader.js +103 -1
  45. package/dist/source-resolver.d.ts +9 -1
  46. package/dist/source-resolver.js +23 -16
  47. package/dist/source-service.d.ts +119 -3
  48. package/dist/source-service.js +1836 -218
  49. package/dist/storage/artifacts-repo.d.ts +4 -1
  50. package/dist/storage/artifacts-repo.js +33 -5
  51. package/dist/storage/files-repo.d.ts +0 -2
  52. package/dist/storage/files-repo.js +0 -11
  53. package/dist/storage/migrations.d.ts +1 -1
  54. package/dist/storage/migrations.js +10 -2
  55. package/dist/storage/schema.d.ts +2 -0
  56. package/dist/storage/schema.js +25 -0
  57. package/dist/tool-contract-manifest.js +8 -6
  58. package/dist/types.d.ts +20 -0
  59. package/dist/workspace-mapping-service.d.ts +13 -0
  60. package/dist/workspace-mapping-service.js +146 -14
  61. package/package.json +3 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,55 @@ 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.0.0] - 2026-04-18
9
+
10
+ ### Changed
11
+ - 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.
12
+ - 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).
13
+ - 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.
14
+ - 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.
15
+ - `find-mapping` and `check-symbol-exists` accept an empty `descriptor` as equivalent to omitting it.
16
+ - 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).
17
+
18
+ ### Added
19
+ - `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.
20
+ - `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`).
21
+ - `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.
22
+ - `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`.
23
+ - `validate-mixin` `quickSummary` appends notes when `provenance.scopeFallback` fires or `toolHealth.overallHealthy` is false.
24
+ - 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`).
25
+ - `docs/tool-reference.md` includes a "Which Tool for Which Question" decision table; `README.md` links to it from the Documentation section.
26
+
27
+ ### Fixed
28
+ - `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.
29
+ - `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.
30
+ - 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.
31
+ - `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`.
32
+
33
+ ## [3.2.0] - 2026-04-12
34
+
35
+ ### Added
36
+ - `resolve-artifact`, `find-mapping`, `resolve-method-mapping-exact`, `resolve-workspace-symbol`, and `check-symbol-exists` now accept an optional `compact` parameter (default `false`). When `true`, empty arrays, null values, and empty objects are stripped from the top-level response. For `resolve-artifact`, compact mode additionally omits `provenance`, `artifactContents`, `sampleEntries`, `adjacentSourceCandidates`, `binaryJarPath`, `coordinate`, `repoUrl`, and `resolvedSourceJarPath`. For mapping tools, compact mode omits the redundant `candidates` array when the result is a single full-confidence exact-match resolution.
37
+ - `validate-access-widener` now accepts `projectPath`, `scope`, and `preferProjectVersion` for runtime-aware validation against Loom runtime jars, and reports additive `provenance`, `resolvedInRuntime`, and `resolvedRuntimeAccess` evidence for matched class, method, and field entries.
38
+ - Added `validate-access-transformer` for Forge / NeoForge Access Transformer validation, including `atNamespace` support, workspace-driven namespace inference, runtime artifact provenance, and per-entry runtime access evidence.
39
+ - `analyze-mod-jar` now surfaces packaged Access Transformer paths alongside mod metadata.
40
+
41
+ ### Fixed
42
+ - `validate-access-widener` and `validate-access-transformer` runtime-aware method validation now preserves remapped JVM descriptors through exact and fallback member remaps, eliminating false-negative `method not found` results for entries whose descriptors reference remapped Minecraft classes such as `Level#setBlock(BlockPos, BlockState, int)`.
43
+ - `validate-access-widener` runtime-aware `scope: "merged"` now prefers explicit `*merged-intermediary*` / `*merged-mojang*` runtime jars over ambiguous `minecraft-merged.jar` candidates, and Loom tiny lookup now scans workspace and Gradle user-home caches so runtime validation can resolve descriptor remaps in normal Loom setups.
44
+ - `validate-project` now forwards workspace runtime context to discovered Access Widener validation so project-summary runs can use the same runtime-aware behavior as direct `validate-access-widener` calls.
45
+ - `validate-project` now discovers Access Transformer files, supports `task="access-transformer"`, and can include those validations in workspace summaries when `discover` requests them.
46
+ - `resolve-artifact`, `get-class-source`, `get-class-members`, and `inspect-minecraft` source/class-member flows now treat unobfuscated releases such as `26.1+` as native `mojang` runtime namespaces for both version and versioned-coordinate targets, avoiding false `ERR_MAPPING_NOT_APPLIED` failures and wrong-version Loom source-jar approximations when no exact source jar is present.
47
+ - `validate-mixin` tool-health diagnostics now treat unobfuscated `mojang` runtime names as available, while still flagging `intermediary`/`yarn` as unavailable on `26.1+`.
48
+ - `validate-project task="project-summary"` now pre-resolves `preferProjectVersion=true` consistently across discovered Access Widener and workspace Mixin checks, and returns a blocked summary with version-agnostic recovery guidance when discovered validators need a version but neither the request nor `gradle.properties` can supply one.
49
+ - `check-symbol-exists` and `analyze-symbol task="exists"` now fall back to unobfuscated runtime bytecode for `mojang`/runtime-name existence checks on `26.1+`, preserve the original `mapping_unavailable` result when the runtime JAR cannot be resolved, and return a targeted warning when callers provide only a short class name.
50
+ - Loom tiny mapping discovery no longer performs unbounded directory traversal when the version-specific cache directory is absent, preventing excessive memory consumption on systems with large Gradle caches.
51
+ - Mapping graph cache eviction after lifecycle scans now correctly releases all cached entries for a version instead of silently skipping them.
52
+
53
+ ### Performance
54
+ - `resolve-artifact` source-jar detection and mapping tiny-jar loading now reuse a single ZIP walk per jar probe instead of reopening and fully enumerating matching archives on each helper call.
55
+ - `search-mod-source`, `decompile-mod-jar`, `get-mod-class-source`, `validate-project` workspace discovery, and workspace mapping detection now avoid synchronous hot-path file/glob reads and use bounded concurrent text reads where safe, reducing event-loop stalls on larger decompiled outputs and multi-module workspaces.
56
+
8
57
  ## [3.1.1] - 2026-03-21
9
58
 
10
59
  ### Fixed
package/README.md CHANGED
@@ -5,23 +5,23 @@
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
9
 
10
- ---
10
+ > **Note**: This project is entirely vibe-coded — built with AI-assisted development without formal specs.
11
11
 
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.
12
+ ---
13
13
 
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.
14
+ `@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. It works with Claude Desktop, Claude Code, VS Code, Codex CLI, Gemini CLI, and other MCP-capable clients.
15
15
 
16
- **35 tools** (6 entry + 29 expert) | **7 resources** | **4 namespace mappings** | **SQLite-backed cache**
16
+ **36 tools** (6 entry + 30 expert) | **7 resources** | **4 namespace mappings** | **SQLite-backed cache**
17
17
 
18
18
  ## Features
19
19
 
20
20
  - **Source Exploration**: browse and search decompiled Minecraft source code with line-level precision and cursor-paginated file listing
21
21
  - **Multi-Mapping Conversion**: translate class, field, and method names between `obfuscated`, `mojang`, `intermediary`, and `yarn`
22
22
  - **Version Comparison**: diff class signatures and registry entries between Minecraft versions
23
- - **Mod JAR Analysis**: extract metadata, dependencies, entrypoints, and Mixin configs from Fabric, Forge, and NeoForge mod JARs
24
- - **Mixin and Access Widener Validation**: validate source and `.accesswidener` files against a target Minecraft version
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
25
  - **NBT Round-Trip**: decode NBT binary to typed JSON, apply RFC 6902 patches, and encode it back to NBT
26
26
  - **Registry Data and Runtime Metrics**: query generated registry snapshots and inspect cache and latency counters
27
27
  - **MCP Resources**: expose versions, class source, artifact metadata, and mappings through URI-based resources
@@ -47,8 +47,17 @@ If automatic JAR downloads are blocked in your environment, set `MCP_VINEFLOWER_
47
47
 
48
48
  CLI clients:
49
49
 
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`
50
+ Claude Code:
51
+
52
+ ```bash
53
+ claude mcp add minecraft-modding -- npx -y @adhisang/minecraft-modding-mcp
54
+ ```
55
+
56
+ OpenAI Codex CLI:
57
+
58
+ ```bash
59
+ codex mcp add minecraft-modding -- npx -y @adhisang/minecraft-modding-mcp
60
+ ```
52
61
 
53
62
  Run `claude mcp list` or `codex mcp list` after registration to verify the server is available.
54
63
 
@@ -136,7 +145,7 @@ All six return `result.summary` first, and can include `summary.nextActions` whe
136
145
  | `analyze-symbol` | symbol existence checks, mapping conversion, lifecycle tracing, and workspace symbol resolution |
137
146
  | `compare-minecraft` | version-pair diffs, class diffs, registry diffs, and migration-oriented overviews |
138
147
  | `analyze-mod` | mod metadata, decompile/search flows, class source, and safe remap preview/apply |
139
- | `validate-project` | workspace summaries plus direct Mixin and Access Widener validation |
148
+ | `validate-project` | workspace summaries plus direct Mixin, Access Widener, and Access Transformer validation |
140
149
  | `manage-cache` | cache inventory, verification, and preview/apply cleanup workflows |
141
150
 
142
151
  ### Workflow Notes
@@ -146,8 +155,9 @@ Keep only the high-frequency notes here. For the full pitfall list, exact contra
146
155
  - `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
156
  - 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
157
  - `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
- - `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`.
158
+ - 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.
159
+ - `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`.
160
+ - `validate-project task="project-summary"` propagates `preferProjectVersion=true` across discovered Mixin, Access Widener, and Access Transformer checks. When no version can be resolved from the request or `gradle.properties`, the summary blocks with version-agnostic recovery guidance.
151
161
 
152
162
  ### Inspect Minecraft source from a version
153
163
 
@@ -216,7 +226,7 @@ Keep only the high-frequency notes here. For the full pitfall list, exact contra
216
226
  "subject": {
217
227
  "kind": "workspace",
218
228
  "projectPath": "/workspace/modid",
219
- "discover": ["mixins", "access-wideners"]
229
+ "discover": ["mixins", "access-wideners", "access-transformers"]
220
230
  },
221
231
  "preferProjectVersion": true,
222
232
  "preferProjectMapping": true
@@ -224,10 +234,12 @@ Keep only the high-frequency notes here. For the full pitfall list, exact contra
224
234
  }
225
235
  ```
226
236
 
237
+ Workspace summaries still default to discovering mixins and access wideners. Add `"access-transformers"` to `subject.discover` when you want Access Transformer files included in the summary run.
238
+
227
239
  ## Documentation
228
240
 
229
241
  - [Detailed example requests](docs/examples.md) for copyable payloads and common workflows
230
- - [Tool and configuration reference](docs/tool-reference.md) for exact inputs, outputs, resource behavior, environment variables, and migration notes
242
+ - [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
231
243
  - [日本語 README](docs/README-ja.md) for a Japanese onboarding overview
232
244
 
233
245
  ## Tool Surface
@@ -243,7 +255,7 @@ Start with these top-level workflow tools unless you already know the exact spec
243
255
  | `analyze-symbol` | Handle symbol existence checks, namespace mapping, lifecycle tracing, workspace symbol resolution, and API overviews |
244
256
  | `compare-minecraft` | Compare version pairs, class diffs, registry diffs, and migration-oriented summaries |
245
257
  | `analyze-mod` | Summarize mod metadata, decompile and search mod code, inspect class source, and preview or apply remaps |
246
- | `validate-project` | Summarize workspaces and run direct Mixin or Access Widener validation |
258
+ | `validate-project` | Summarize workspaces and run direct Mixin, Access Widener, or Access Transformer validation |
247
259
  | `manage-cache` | List, verify, and preview or apply cache cleanup and rebuild operations |
248
260
  <!-- END GENERATED TOOL TABLE: v3-entry-tools -->
249
261
 
@@ -265,6 +277,8 @@ Tools for browsing Minecraft versions, resolving source artifacts, and reading o
265
277
  | `index-artifact` | Rebuild indexed metadata for an existing artifact |
266
278
  <!-- END GENERATED TOOL TABLE: source-exploration -->
267
279
 
280
+ 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.
281
+
268
282
  ### Version Comparison & Symbol Tracking
269
283
 
270
284
  Tools for comparing class and registry changes across Minecraft versions and tracing symbol existence over time.
@@ -291,6 +305,10 @@ Tools for converting symbol names between namespaces and checking symbol existen
291
305
  | `check-symbol-exists` | Check whether a class, field, or method exists in a namespace |
292
306
  <!-- END GENERATED TOOL TABLE: mapping-symbols -->
293
307
 
308
+ `resolve-artifact`, `find-mapping`, `resolve-method-mapping-exact`, `resolve-workspace-symbol`, and `check-symbol-exists` default `compact` to `true`: empty fields are stripped, `resolve-artifact` omits diagnostic fields (`provenance`, `artifactContents`, etc.), and mapping tools omit the redundant `candidates` array on full-confidence exact matches and slim the tail to `{kind, symbol, owner, name, descriptor, confidence, matchKind}` (signalled by `candidateDetailsTruncated`) for unresolved results with more than three candidates. Pass `compact: false` for the full diagnostic shape.
309
+
310
+ `get-class-source`, `get-class-members`, `search-class-source`, and `list-artifact-files` accept the same `compact` parameter as opt-in (default `false`). When enabled it strips `provenance`, `artifactContents`, `qualityFlags`, and (for `get-class-members`) `context`, while preserving primary payloads (`sourceText`, `members`/`counts`, `hits`, `items`). See [docs/tool-reference.md](docs/tool-reference.md) for the full per-tool field list.
311
+
294
312
  ### NBT Utilities
295
313
 
296
314
  Tools for decoding, patching, and encoding Java Edition NBT binary data using a typed JSON representation.
@@ -310,7 +328,7 @@ Tools for extracting metadata from mod JARs, decompiling mod source, searching m
310
328
  <!-- BEGIN GENERATED TOOL TABLE: mod-analysis -->
311
329
  | Tool | Purpose |
312
330
  | --- | --- |
313
- | `analyze-mod-jar` | Extract mod metadata, dependencies, entrypoints, and mixin config info from a JAR |
331
+ | `analyze-mod-jar` | Extract mod metadata, dependencies, entrypoints, mixin config info, and packaged access transformer paths from a JAR |
314
332
  | `decompile-mod-jar` | Decompile a mod JAR and optionally return one class source |
315
333
  | `get-mod-class-source` | Read one class source from the decompiled mod cache |
316
334
  | `search-mod-source` | Search decompiled mod source by class, method, field, or content |
@@ -319,13 +337,14 @@ Tools for extracting metadata from mod JARs, decompiling mod source, searching m
319
337
 
320
338
  ### Validation
321
339
 
322
- Tools for validating Mixin source and Access Widener files against a target Minecraft version.
340
+ Tools for validating Mixin source, Access Widener files, and Forge/NeoForge Access Transformer files against a target Minecraft version. `validate-access-widener` defaults to vanilla bytecode validation; pass `projectPath`, `scope`, and `preferProjectVersion` for runtime-aware mode, which returns runtime `provenance` plus per-entry `resolvedRuntimeAccess` evidence. `validate-access-transformer` infers `atNamespace` from Forge/NeoForge workspace context when `projectPath` is provided and uses loader runtime artifacts for `scope="loader"`.
323
341
 
324
342
  <!-- BEGIN GENERATED TOOL TABLE: validation -->
325
343
  | Tool | Purpose |
326
344
  | --- | --- |
327
345
  | `validate-mixin` | Validate Mixin source against a target Minecraft version |
328
- | `validate-access-widener` | Validate Access Widener content against a target Minecraft version |
346
+ | `validate-access-widener` | Validate Access Widener content against a target Minecraft version, optionally using runtime-aware Loom artifacts |
347
+ | `validate-access-transformer` | Validate Access Transformer content against a target Minecraft version, optionally using Forge/NeoForge runtime artifacts |
329
348
  <!-- END GENERATED TOOL TABLE: validation -->
330
349
 
331
350
  ### Registry & Diagnostics
@@ -0,0 +1,17 @@
1
+ export type AccessTransformerAccessAction = "public" | "protected" | "package-private" | "private";
2
+ export type AccessTransformerFinalAction = "add" | "remove";
3
+ export type AccessTransformerEntry = {
4
+ line: number;
5
+ targetKind: "class" | "field" | "method";
6
+ owner: string;
7
+ target: string;
8
+ name?: string;
9
+ descriptor?: string;
10
+ accessAction: AccessTransformerAccessAction;
11
+ finalAction?: AccessTransformerFinalAction;
12
+ };
13
+ export type ParsedAccessTransformer = {
14
+ entries: AccessTransformerEntry[];
15
+ parseWarnings: string[];
16
+ };
17
+ export declare function parseAccessTransformer(content: string): ParsedAccessTransformer;
@@ -0,0 +1,97 @@
1
+ function parseAccessDeclaration(raw) {
2
+ const match = raw.match(/^(public|protected|default|private)([+-]f)?$/i);
3
+ if (!match) {
4
+ return undefined;
5
+ }
6
+ const finalModifier = match[2]?.toLowerCase();
7
+ return {
8
+ accessAction: match[1]?.toLowerCase() === "default"
9
+ ? "package-private"
10
+ : match[1]?.toLowerCase(),
11
+ ...(finalModifier === "+f" ? { finalAction: "add" } : {}),
12
+ ...(finalModifier === "-f" ? { finalAction: "remove" } : {})
13
+ };
14
+ }
15
+ function splitMemberToken(tokens) {
16
+ if (tokens.length === 0) {
17
+ return { targetKind: "class" };
18
+ }
19
+ if (tokens.length === 1) {
20
+ const token = tokens[0] ?? "";
21
+ const descriptorStart = token.indexOf("(");
22
+ if (descriptorStart >= 0) {
23
+ return {
24
+ targetKind: "method",
25
+ name: token.slice(0, descriptorStart),
26
+ descriptor: token.slice(descriptorStart)
27
+ };
28
+ }
29
+ return {
30
+ targetKind: "field",
31
+ name: token
32
+ };
33
+ }
34
+ return {
35
+ targetKind: "method",
36
+ name: tokens[0],
37
+ descriptor: tokens.slice(1).join("")
38
+ };
39
+ }
40
+ export function parseAccessTransformer(content) {
41
+ const entries = [];
42
+ const parseWarnings = [];
43
+ const lines = content.split(/\r?\n/);
44
+ for (let index = 0; index < lines.length; index++) {
45
+ const lineNumber = index + 1;
46
+ const rawLine = (lines[index] ?? "").trim();
47
+ if (!rawLine || rawLine.startsWith("#")) {
48
+ continue;
49
+ }
50
+ const withoutComment = rawLine.replace(/\s+#.*$/, "").trim();
51
+ if (!withoutComment) {
52
+ continue;
53
+ }
54
+ const parts = withoutComment.split(/\s+/);
55
+ if (parts.length < 2) {
56
+ parseWarnings.push(`Line ${lineNumber}: Incomplete access transformer entry "${withoutComment}".`);
57
+ continue;
58
+ }
59
+ const declaration = parseAccessDeclaration(parts[0] ?? "");
60
+ if (!declaration) {
61
+ parseWarnings.push(`Line ${lineNumber}: Unsupported access declaration "${parts[0]}".`);
62
+ continue;
63
+ }
64
+ const owner = parts[1] ?? "";
65
+ if (!owner) {
66
+ parseWarnings.push(`Line ${lineNumber}: Incomplete access transformer entry "${withoutComment}".`);
67
+ continue;
68
+ }
69
+ const member = splitMemberToken(parts.slice(2));
70
+ if (member.targetKind === "method" && (!member.name || !member.descriptor)) {
71
+ parseWarnings.push(`Line ${lineNumber}: Method entry requires a method name and JVM descriptor.`);
72
+ continue;
73
+ }
74
+ if ((member.targetKind === "field" || member.targetKind === "method") && !member.name) {
75
+ parseWarnings.push(`Line ${lineNumber}: Member entry requires a target name.`);
76
+ continue;
77
+ }
78
+ const target = member.targetKind === "class"
79
+ ? owner
80
+ : `${owner}#${member.name}${member.descriptor ?? ""}`;
81
+ entries.push({
82
+ line: lineNumber,
83
+ owner,
84
+ target,
85
+ targetKind: member.targetKind,
86
+ ...(member.name ? { name: member.name } : {}),
87
+ ...(member.descriptor ? { descriptor: member.descriptor } : {}),
88
+ accessAction: declaration.accessAction,
89
+ ...(declaration.finalAction ? { finalAction: declaration.finalAction } : {})
90
+ });
91
+ }
92
+ return {
93
+ entries,
94
+ parseWarnings
95
+ };
96
+ }
97
+ //# sourceMappingURL=access-transformer-parser.js.map
@@ -1,5 +1,5 @@
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
+ export declare const PUBLIC_CACHE_KINDS: readonly ["artifact-index", "downloads", "mapping", "registry", "decompiled-source", "mod-remap", "binary-remap"];
3
3
  export type PublicCacheKind = (typeof PUBLIC_CACHE_KINDS)[number];
4
4
  export declare const CACHE_HEALTH_STATES: readonly ["healthy", "partial", "stale", "orphaned", "corrupt", "in_use"];
5
5
  export type CacheHealthState = (typeof CACHE_HEALTH_STATES)[number];
@@ -10,7 +10,8 @@ export const PUBLIC_CACHE_KINDS = [
10
10
  "mapping",
11
11
  "registry",
12
12
  "decompiled-source",
13
- "mod-remap"
13
+ "mod-remap",
14
+ "binary-remap"
14
15
  ];
15
16
  export const CACHE_HEALTH_STATES = [
16
17
  "healthy",
@@ -37,6 +38,8 @@ function kindRoot(config, cacheKind) {
37
38
  return join(config.cacheDir, "decompiled");
38
39
  case "mod-remap":
39
40
  return join(config.cacheDir, "remapped-mods");
41
+ case "binary-remap":
42
+ return join(config.cacheDir, "remapped");
40
43
  }
41
44
  }
42
45
  async function listFilesRecursive(root) {
@@ -439,7 +442,12 @@ async function fileBackedEntries(config, cacheKind) {
439
442
  inUse: filePath.endsWith(".lock") ||
440
443
  filePath.endsWith(".wal") ||
441
444
  filePath.endsWith(".journal"),
442
- ...(cacheKind === "downloads" || cacheKind === "mod-remap" ? { jarPath: filePath } : {})
445
+ ...(cacheKind === "downloads" || cacheKind === "mod-remap" || cacheKind === "binary-remap"
446
+ ? { jarPath: filePath }
447
+ : {}),
448
+ ...(cacheKind === "binary-remap"
449
+ ? { artifactId: normalizedEntryId.replace(/\.jar$/i, "") }
450
+ : {})
443
451
  }
444
452
  });
445
453
  }
@@ -0,0 +1 @@
1
+ export declare function mapWithConcurrencyLimit<T, R>(items: readonly T[], limit: number, mapper: (item: T, index: number) => Promise<R>): Promise<R[]>;
@@ -0,0 +1,24 @@
1
+ export async function mapWithConcurrencyLimit(items, limit, mapper) {
2
+ if (!Number.isInteger(limit) || limit < 1) {
3
+ throw new Error("limit must be a positive integer");
4
+ }
5
+ if (items.length === 0) {
6
+ return [];
7
+ }
8
+ const results = new Array(items.length);
9
+ let nextIndex = 0;
10
+ const workerCount = Math.min(limit, items.length);
11
+ const worker = async () => {
12
+ while (true) {
13
+ const currentIndex = nextIndex;
14
+ nextIndex += 1;
15
+ if (currentIndex >= items.length) {
16
+ return;
17
+ }
18
+ results[currentIndex] = await mapper(items[currentIndex], currentIndex);
19
+ }
20
+ };
21
+ await Promise.all(Array.from({ length: workerCount }, async () => worker()));
22
+ return results;
23
+ }
24
+ //# sourceMappingURL=concurrency.js.map
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
@@ -1,11 +1,13 @@
1
- import { access, constants } from "node:fs/promises";
2
- import { mkdirSync, readdirSync, rmSync, statSync } from "node:fs";
1
+ import { access, constants, mkdir, readFile, readdir, stat } from "node:fs/promises";
2
+ import { mkdirSync, rmSync } from "node:fs";
3
3
  import { createHash } from "node:crypto";
4
4
  import { basename, join, relative, sep } from "node:path";
5
+ import { mapWithConcurrencyLimit } from "../concurrency.js";
5
6
  import { createError, ERROR_CODES, isAppError } from "../errors.js";
6
7
  import { assertJavaAvailable, runJavaProcess } from "../java-process.js";
7
8
  import { log } from "../logger.js";
8
9
  const DEFAULT_TIMEOUT_MS = 120_000;
10
+ const DECOMPILED_JAVA_READ_CONCURRENCY = 8;
9
11
  const VINEFLOWER_FLAG_PROFILES = [
10
12
  { label: "default", flags: ["-din=1", "-rbr=1", "-dgs=1"] },
11
13
  { label: "relaxed", flags: ["-din=1", "-rbr=0", "-dgs=0"] },
@@ -47,14 +49,14 @@ async function assertVineflowerAvailable(vineflowerJarPath) {
47
49
  });
48
50
  }
49
51
  }
50
- function collectJavaFilesSync(baseDir, currentDir = "") {
52
+ async function collectJavaFilesRecursive(baseDir, currentDir = "") {
51
53
  const absoluteBase = currentDir ? join(baseDir, currentDir) : baseDir;
52
- const entries = readdirSync(absoluteBase, { withFileTypes: true });
54
+ const entries = await readdir(absoluteBase, { withFileTypes: true });
53
55
  const result = [];
54
56
  for (const entry of entries) {
55
57
  const next = currentDir ? join(currentDir, entry.name) : entry.name;
56
58
  if (entry.isDirectory()) {
57
- result.push(...collectJavaFilesSync(baseDir, next));
59
+ result.push(...await collectJavaFilesRecursive(baseDir, next));
58
60
  continue;
59
61
  }
60
62
  if (entry.isFile() && entry.name.endsWith(".java")) {
@@ -66,23 +68,21 @@ function collectJavaFilesSync(baseDir, currentDir = "") {
66
68
  async function collectJavaFiles(baseDir) {
67
69
  try {
68
70
  const fastGlobModule = (await import("fast-glob"));
69
- const sync = fastGlobModule.default?.sync;
70
- if (typeof sync === "function") {
71
- return sync("**/*.java", { cwd: baseDir, onlyFiles: true });
71
+ const glob = fastGlobModule.default?.glob;
72
+ if (typeof glob === "function") {
73
+ return (await glob("**/*.java", { cwd: baseDir, onlyFiles: true }))
74
+ .sort((left, right) => left.localeCompare(right));
72
75
  }
73
76
  }
74
77
  catch {
75
78
  // optional dependency: fallback to recursive traversal
76
79
  }
77
- return collectJavaFilesSync(baseDir).map((candidate) => candidate.split(sep).join("/"));
80
+ return (await collectJavaFilesRecursive(baseDir))
81
+ .map((candidate) => candidate.split(sep).join("/"))
82
+ .sort((left, right) => left.localeCompare(right));
78
83
  }
79
84
  function readFileTreeText(filePath) {
80
- return new Promise((resolve, reject) => {
81
- import("node:fs/promises")
82
- .then((fs) => fs.readFile(filePath, "utf8"))
83
- .then(resolve)
84
- .catch(reject);
85
- });
85
+ return readFile(filePath, "utf8");
86
86
  }
87
87
  function decompileOutputDir(cacheDir, binaryJarPath, signature) {
88
88
  const digest = createHash("sha256").update(binaryJarPath).update(signature).digest("hex");
@@ -130,17 +130,18 @@ export async function decompileBinaryJar(binaryJarPath, cacheDir, options) {
130
130
  const signature = options.signature ?? basename(normalizedBinaryJarPath);
131
131
  const outputDir = decompileOutputDir(cacheDir, normalizedBinaryJarPath, signature).replace(/[/\\]$/, "");
132
132
  try {
133
- mkdirSync(outputDir, { recursive: true });
134
- if (statSync(outputDir, { throwIfNoEntry: false })) {
133
+ await mkdir(outputDir, { recursive: true });
134
+ const outputDirStats = await stat(outputDir).catch(() => undefined);
135
+ if (outputDirStats) {
135
136
  const existingJavaFiles = await collectJavaFiles(outputDir);
136
137
  if (existingJavaFiles.length > 0) {
137
- const results = await Promise.all(existingJavaFiles.map(async (candidate) => {
138
+ const results = await mapWithConcurrencyLimit(existingJavaFiles, DECOMPILED_JAVA_READ_CONCURRENCY, async (candidate) => {
138
139
  const abs = join(outputDir, candidate);
139
140
  return {
140
141
  filePath: normalizeOutputPath(outputDir, abs),
141
142
  content: await readFileTreeText(abs)
142
143
  };
143
- }));
144
+ });
144
145
  emitDecompileLog("decompile.done", {
145
146
  durationMs: Date.now() - startedAt,
146
147
  artifactIdCandidate: options.artifactIdCandidate,
@@ -181,13 +182,13 @@ export async function decompileBinaryJar(binaryJarPath, cacheDir, options) {
181
182
  }
182
183
  });
183
184
  }
184
- const javaFiles = await Promise.all(javaFileNames.map(async (candidate) => {
185
+ const javaFiles = await mapWithConcurrencyLimit(javaFileNames, DECOMPILED_JAVA_READ_CONCURRENCY, async (candidate) => {
185
186
  const abs = join(outputDir, candidate);
186
187
  return {
187
188
  filePath: normalizeOutputPath(outputDir, abs),
188
189
  content: await readFileTreeText(abs)
189
190
  };
190
- }));
191
+ });
191
192
  emitDecompileLog("decompile.done", {
192
193
  durationMs: Date.now() - startedAt,
193
194
  artifactIdCandidate: options.artifactIdCandidate,
@@ -75,7 +75,7 @@ export declare const analyzeModSchema: z.ZodEffects<z.ZodObject<{
75
75
  include: z.ZodOptional<z.ZodArray<z.ZodEnum<[string, ...string[]]>, "many">>;
76
76
  }, "strip", z.ZodTypeAny, {
77
77
  limit: number;
78
- searchType: "class" | "method" | "field" | "all" | "content";
78
+ searchType: "class" | "field" | "method" | "all" | "content";
79
79
  task: "search" | "remap" | "summary" | "class-source" | "decompile";
80
80
  subject: {
81
81
  kind: "jar";
@@ -111,7 +111,7 @@ export declare const analyzeModSchema: z.ZodEffects<z.ZodObject<{
111
111
  targetMapping?: "mojang" | "yarn" | undefined;
112
112
  outputJar?: string | undefined;
113
113
  query?: string | undefined;
114
- searchType?: "class" | "method" | "field" | "all" | "content" | undefined;
114
+ searchType?: "class" | "field" | "method" | "all" | "content" | undefined;
115
115
  detail?: "full" | "summary" | "standard" | undefined;
116
116
  include?: string[] | undefined;
117
117
  includeFiles?: boolean | undefined;
@@ -119,7 +119,7 @@ export declare const analyzeModSchema: z.ZodEffects<z.ZodObject<{
119
119
  executionMode?: "preview" | "apply" | undefined;
120
120
  }>, {
121
121
  limit: number;
122
- searchType: "class" | "method" | "field" | "all" | "content";
122
+ searchType: "class" | "field" | "method" | "all" | "content";
123
123
  task: "search" | "remap" | "summary" | "class-source" | "decompile";
124
124
  subject: {
125
125
  kind: "jar";
@@ -155,7 +155,7 @@ export declare const analyzeModSchema: z.ZodEffects<z.ZodObject<{
155
155
  targetMapping?: "mojang" | "yarn" | undefined;
156
156
  outputJar?: string | undefined;
157
157
  query?: string | undefined;
158
- searchType?: "class" | "method" | "field" | "all" | "content" | undefined;
158
+ searchType?: "class" | "field" | "method" | "all" | "content" | undefined;
159
159
  detail?: "full" | "summary" | "standard" | undefined;
160
160
  include?: string[] | undefined;
161
161
  includeFiles?: boolean | undefined;