@adhisang/minecraft-modding-mcp 6.1.1 → 6.2.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 (53) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +4 -2
  3. package/dist/cache-registry.d.ts +1 -1
  4. package/dist/cache-registry.js +3 -0
  5. package/dist/entry-tools/analyze-mod-service.d.ts +12 -6
  6. package/dist/entry-tools/analyze-mod-service.js +37 -3
  7. package/dist/entry-tools/analyze-symbol-service.d.ts +2 -0
  8. package/dist/entry-tools/analyze-symbol-service.js +37 -2
  9. package/dist/entry-tools/inspect-minecraft/internal.d.ts +4 -0
  10. package/dist/entry-tools/inspect-minecraft/internal.js +28 -0
  11. package/dist/entry-tools/manage-cache-service.d.ts +4 -4
  12. package/dist/error-mapping.d.ts +13 -0
  13. package/dist/error-mapping.js +35 -2
  14. package/dist/errors.d.ts +2 -0
  15. package/dist/errors.js +2 -0
  16. package/dist/index.js +37 -17
  17. package/dist/mapping/internal-types.d.ts +7 -0
  18. package/dist/mapping/types.d.ts +18 -0
  19. package/dist/mapping-service.js +16 -3
  20. package/dist/minecraft-explorer-service.d.ts +4 -0
  21. package/dist/minecraft-explorer-service.js +156 -17
  22. package/dist/mod-analyzer.d.ts +7 -0
  23. package/dist/mod-analyzer.js +28 -7
  24. package/dist/source/artifact-resolver.d.ts +2 -0
  25. package/dist/source/artifact-resolver.js +24 -1
  26. package/dist/source/class-source/members-builder.d.ts +4 -0
  27. package/dist/source/class-source/members-builder.js +3 -1
  28. package/dist/source/class-source.d.ts +2 -1
  29. package/dist/source/class-source.js +154 -17
  30. package/dist/source/did-you-mean.d.ts +14 -0
  31. package/dist/source/did-you-mean.js +79 -0
  32. package/dist/source/file-access.js +159 -3
  33. package/dist/source/indexer.js +72 -2
  34. package/dist/source/lifecycle/runtime-check.js +9 -5
  35. package/dist/source/nested-jars.d.ts +59 -0
  36. package/dist/source/nested-jars.js +198 -0
  37. package/dist/source/workspace-target.js +5 -2
  38. package/dist/source-jar-reader.d.ts +16 -0
  39. package/dist/source-jar-reader.js +82 -0
  40. package/dist/source-service.d.ts +36 -0
  41. package/dist/source-service.js +49 -6
  42. package/dist/stage-emitter.js +24 -8
  43. package/dist/stdio-supervisor.d.ts +92 -9
  44. package/dist/stdio-supervisor.js +915 -103
  45. package/dist/tool-contract-manifest.js +1 -1
  46. package/dist/tool-guidance.js +3 -1
  47. package/dist/tool-schemas.d.ts +1337 -149
  48. package/dist/tool-schemas.js +38 -7
  49. package/dist/types.d.ts +23 -0
  50. package/dist/workspace-mapping-service.d.ts +1 -0
  51. package/dist/workspace-mapping-service.js +120 -8
  52. package/docs/tool-reference.md +19 -3
  53. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,36 @@ and this project aims to follow [Semantic Versioning](https://semver.org/spec/v2
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [6.2.0] - 2026-07-11
11
+
12
+ ### Added
13
+
14
+ - Workspace ergonomics for artifact addressing, all additive: (1) `find-class`, `get-artifact-file`, `list-artifact-files`, `search-class-source`, and `index-artifact` accept the shared `target` shape as an alternative to a flat `artifactId` (mutually exclusive — exactly one must be supplied; existing `artifactId` calls are unchanged). (2) `analyze-symbol` infers an omitted `version` from a supplied `projectPath` instead of rejecting at schema time; inferred runs always carry a `versionInference { version, source }` block plus a warning, an explicit `version` is never overridden, and an undetectable version fails with `ERR_WORKSPACE_VERSION_UNRESOLVED`. (3) `inspect-minecraft` direct subjects (`class`/`file`/`search`) without `subject.artifact` auto-resolve through the workspace only when exactly one workspace is known, always with a provenance warning; several known workspaces refuse with `workspaceCandidates`, and an explicit `subject.artifact` is never overridden.
15
+
16
+ - Bytecode member reads now surface annotation metadata: annotation-type classes (`@interface`) expose each member's default value as `annotationDefault` (rendered from the `AnnotationDefault` attribute, including string/enum/class/array/nested-annotation and numeric constants), and `get-class-members` gains an optional `includeAnnotations` flag that adds runtime-visible member annotations (e.g. `@java.lang.Deprecated`) to each member. Defaults are always present when the classfile carries them; member annotations appear only under the opt-in flag. `analyze-mod` `task="members"` includes both without a flag. No new dependencies — the existing classfile reader parses the attribute bodies.
17
+
18
+ - `analyze-mod` gains a `members` task (`subject: { kind: "class", jarPath, className }`) that reads a mod class's constructors, fields, and methods (all access levels, including private and protected) directly from bytecode — no decompiler invocation on this path, unlike `task: "class-source"`. Responses carry `extractionMethod: "bytecode-only"`. Additive: existing tasks and inputs are unchanged, and the tool count stays at 41.
19
+
20
+ - `get-artifact-file` serves `assets/**` and `data/**` text files (including vanilla `data/minecraft/dimension_type/*.json`, and resources in any jar-backed artifact such as mod jars) directly from the backing jar when the source index has no row for them — responses carry `deliveryMode: "jar-read-through"`. Delivery is text-only with a 512 KiB per-file cap and explicit `truncated` flag; binary entries answer with size metadata and a `contentOmittedReason` instead of content; traversal-shaped paths are rejected with `ERR_INVALID_INPUT`; misses return `ERR_FILE_NOT_FOUND` with `nearbyPaths` hints that surface directory relocations such as the `assets/minecraft/models/item/*` → `assets/minecraft/items/*` move. The `list-artifact-files` assets/data warning now points at this read-through path.
21
+
22
+ - Class-not-found errors (`ERR_CLASS_NOT_FOUND`) from `get-class-source`, `get-class-members`, and their batch variants now include a top-level `didYouMean` array with ranked near-miss candidates from the artifact's symbol index — `{ className, matchReason }` with reasons `exact-simple-name` (moved class, ranked first), `case-insensitive`, and `edit-distance:N`. A stale fully-qualified name like a pre-move `ItemTransform` package now names the current location instead of dead-ending. The array is empty when the index has no usable candidates, and candidates are phrased as hints, not assertions.
23
+
24
+ - Jar-in-Jar shell jars (near-zero own classes, content bundled under `META-INF/jars`, like the Fabric API umbrella jar) resolve instead of failing with `ERR_DECOMPILER_FAILED`. The resolved artifact carries `qualityFlags: ["shell-jar"]` and the nested-jar inventory in `provenance.nestedJars` (also surfaced by `analyze-mod-jar` as `nestedJars`); `get-class-source` and `get-class-members` redirect automatically to the single nested jar containing the requested class, marking the response with `provenance.nestedJar`. A class present in several nested jars returns the new `ERR_NESTED_JAR_AMBIGUOUS` error with `nestedJarCandidates` instead of a silent pick, and class-not-found errors on shells list the inventory. Extracted nested jars live in a content-addressed `nested-jars` cache directory, manageable via `manage-cache` `cacheKinds: ["nested-jar"]`.
25
+
26
+ - `validate-project` calls now have a supervisor-owned 120-second end-to-end deadline, configurable with `MCP_VALIDATE_PROJECT_TIMEOUT_MS` from 10,000 through 600,000 ms. Expiry returns the standard synthetic `{ error, meta }` tool-result envelope with `ERR_TOOL_TIMEOUT`, queue/running phase diagnostics, redacted progress context, and restart-initiation state. Queue expiry leaves the active worker untouched; running expiry replaces the isolated worker and preserves admitted later requests when initialization replay succeeds. Replacement startup/replay failure terminalizes queued work with existing restart errors. The supervisor FIFO holds at most two worker-bound requests and returns `ERR_LIMIT_EXCEEDED` (tool calls) or JSON-RPC `-32000` (non-tool requests) on overflow; cap-blocked requests fail immediately and undeliverable notifications are warning-dropped. POSIX process-group cleanup treats an already-gone process group as clean so stale cleanup tokens do not block later validation.
27
+
28
+ ### Changed
29
+
30
+ - The two constant per-response warning sentences on unobfuscated versions are replaced by structured flags on `mappingContext` — agents that pattern-matched the old strings should read the flags instead. `"Version X is unobfuscated; mapping graph is empty because the runtime already uses deobfuscated names."` is now `mappingContext.unobfuscatedRuntime: true`, and `"Version X is unobfuscated; validated symbol existence against runtime bytecode."` is now `mappingContext.runtimeValidated: true` (`get-class-api-matrix`, which has no `mappingContext`, reports a top-level `unobfuscatedRuntime: true`). No information is removed: the flags carry the same facts, the full former wording stays documented in `docs/tool-reference.md`, and all other warnings are untouched.
31
+
32
+ ### Fixed
33
+
34
+ - Loom split-source workspaces (a `minecraft-common` / `minecraft-clientOnly` sources-jar pair with no merged jar) no longer lose one half's classes to single-jar selection: version-target resolution with project Loom sources indexes the best-scored jar of the other half too (`provenance.companionSourceJars` names it), so client-only classes like `net.minecraft.client.renderer.block.model.ItemTransform` are indexed and findable via symbol search. Class-not-found errors for version targets additionally carry an `exampleCalls` retry using `scope: "vanilla"` (existing enum values only), whose client-jar decompilation also contains client-only classes.
35
+ - Inherited-member expansion no longer emits `Could not resolve super class ...` warnings for platform packages that are never inside a Minecraft jar (`java.*`, `javax.*`, `jdk.*`, `sun.*`, and the separately-shipped `com.mojang.serialization.*` — each verified against real 26.x and 1.21.10 jars). Suppression is allowlist-only: resolution failures for real Minecraft classes, including `com.mojang.blaze3d.*` client-jar content, stay visible.
36
+ - `check-symbol-exists` no longer reports false negatives for fields and classes on unobfuscated versions: the runtime-bytecode fallback introduced in 6.0.0 only re-checked `not_found` verdicts for methods, so a mapping graph lacking a record for a real field like `EntityType.ITEM` returned `not_found` without consulting the jar. Every `not_found` verdict is now runtime-checked (genuinely-missing symbols stay `not_found`), and bytecode-derived response contexts report `mappingNamespace: "mojang"` on unobfuscated versions instead of a hardcoded `"obfuscated"`.
37
+ - Dependency version detection probes the snake_case transforms of hyphenated artifact names (`fabric_api_version` and the compound `fabric_api_fabric_api_version` for `net.fabricmc.fabric-api:fabric-api`). Previously only submodules received a snake_case umbrella fallback key, so resolving the umbrella artifact itself against a standard Fabric template that declares `fabric_api_version` failed with `ERR_DEPENDENCY_VERSION_UNRESOLVED`.
38
+ - Umbrella submodule dependency targets (e.g. `net.fabricmc.fabric-api:fabric-gametest-api-v1`) no longer copy the umbrella's version into the submodule coordinate, which synthesized nonexistent coordinates like `fabric-gametest-api-v1:0.153.0+26.2` and failed downstream. Resolution now adopts the single cached modules-2 version, or — when several are cached — the version named by the cached umbrella POM's `<dependency>` entry (recorded as `provenance.submoduleVersionSource: "umbrella-pom"`). Anything still ambiguous fails closed with `ERR_DEPENDENCY_VERSION_UNRESOLVED` and `candidatesSeen` instead of guessing.
39
+
10
40
  ## [6.1.1] - 2026-07-04
11
41
 
12
42
  ### Fixed
package/README.md CHANGED
@@ -147,7 +147,7 @@ All six return `result.summary` first and can include `summary.nextActions` when
147
147
  | `inspect-minecraft` | versions, artifacts, classes, files, and source search |
148
148
  | `analyze-symbol` | symbol existence checks, mapping conversion, lifecycle tracing, and workspace symbol resolution |
149
149
  | `compare-minecraft` | version-pair diffs, class diffs, registry diffs, and migration-oriented overviews |
150
- | `analyze-mod` | mod metadata, decompile/search flows, class source, and safe remap preview/apply |
150
+ | `analyze-mod` | mod metadata, decompile/search flows, class source, bytecode member queries, and safe remap preview/apply |
151
151
  | `validate-project` | workspace summaries plus direct Mixin, Access Widener, and Access Transformer validation |
152
152
  | `manage-cache` | cache inventory, verification, and preview/apply cleanup workflows |
153
153
 
@@ -163,6 +163,8 @@ These notes cover high-frequency decisions during onboarding. For the full pitfa
163
163
  - `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.
164
164
  - `validate-mixin` and `validate-project` keep `mapping-health` lightweight for `obfuscated` and `mojang` validation, avoiding full Tiny mapping graph loads unless `intermediary` or `yarn` namespaces are requested.
165
165
  - `validate-project task="project-summary"` uses a lightweight artifact probe for `tasks["minecraft.artifact.resolved"]`; it does not decompile Minecraft or rebuild the source index just to report per-probe status. Set `VALIDATE_PROJECT_TASKS_OFF=1` to omit the additive `tasks` field.
166
+ - `validate-project` has a supervisor-owned end-to-end deadline of 120 seconds, including queue time. Set `MCP_VALIDATE_PROJECT_TIMEOUT_MS` to an ASCII-decimal value from `10000` through `600000` to override it. A timeout returns `ERR_TOOL_TIMEOUT`; a running timeout restarts the isolated worker before queued calls resume, while a queue timeout leaves the current worker untouched.
167
+ - Queued calls resume only after the replacement worker completes initialization replay. If replacement startup or replay fails, queued tool calls terminate with `ERR_WORKER_RESTART` instead of waiting indefinitely. If unresolved process-tree cleanup fills the supervisor's two live-generation slots, new requests fail with the existing restart envelope and unavailable notifications are warning-dropped until cleanup or reconnect. On POSIX, an already-gone process group counts as cleaned up rather than leaving a stale cleanup token.
166
168
  - If a workspace was built with `GRADLE_USER_HOME=/tmp/...` or another isolated Gradle home, pass that path as `gradleUserHome` so source, mapping, runtime, and project validation lookups use the same Loom cache instead of stale caches under the MCP process home.
167
169
  - `manage-cache` reports corrupt Mojang binary-remap cache directories under `cacheKinds: ["binary-remap"]` with `status: "corrupt"`, and can delete them by `selector.artifactId` in preview/apply workflows.
168
170
 
@@ -262,7 +264,7 @@ Start with these top-level workflow tools unless you already know the exact spec
262
264
  | `inspect-minecraft` | Inspect versions, artifacts, classes, files, source text, and workspace-aware lookup flows |
263
265
  | `analyze-symbol` | Handle symbol existence checks, namespace mapping, lifecycle tracing, workspace symbol resolution, and API overviews |
264
266
  | `compare-minecraft` | Compare version pairs, class diffs, registry diffs, and migration-oriented summaries |
265
- | `analyze-mod` | Summarize mod metadata, decompile and search mod code, inspect class source, and preview or apply remaps |
267
+ | `analyze-mod` | Summarize mod metadata, decompile and search mod code, inspect class source, read class members from bytecode, and preview or apply remaps |
266
268
  | `validate-project` | Summarize workspaces and run direct Mixin, Access Widener, or Access Transformer validation |
267
269
  | `manage-cache` | List, verify, and preview or apply cache cleanup and rebuild operations |
268
270
  <!-- END GENERATED TOOL TABLE: top-level-workflow-tools -->
@@ -1,6 +1,6 @@
1
1
  import { type PathRuntimeInfo } from "./path-converter.js";
2
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
+ export declare const PUBLIC_CACHE_KINDS: readonly ["artifact-index", "downloads", "mapping", "registry", "decompiled-source", "mod-remap", "binary-remap", "nested-jar", "workspace"];
4
4
  export type PublicCacheKind = (typeof PUBLIC_CACHE_KINDS)[number];
5
5
  export declare const CACHE_HEALTH_STATES: readonly ["healthy", "partial", "stale", "orphaned", "corrupt", "in_use"];
6
6
  export type CacheHealthState = (typeof CACHE_HEALTH_STATES)[number];
@@ -14,6 +14,7 @@ export const PUBLIC_CACHE_KINDS = [
14
14
  "decompiled-source",
15
15
  "mod-remap",
16
16
  "binary-remap",
17
+ "nested-jar",
17
18
  "workspace"
18
19
  ];
19
20
  export const CACHE_HEALTH_STATES = [
@@ -44,6 +45,8 @@ function kindRoot(config, cacheKind) {
44
45
  return join(config.cacheDir, "remapped-mods");
45
46
  case "binary-remap":
46
47
  return join(config.cacheDir, "remapped");
48
+ case "nested-jar":
49
+ return join(config.cacheDir, "nested-jars");
47
50
  case "workspace":
48
51
  return "<in-memory:workspace-context-cache>";
49
52
  }
@@ -1,7 +1,7 @@
1
1
  import type { AnalyzeModOptions, ModAnalysisResult } from "../mod-analyzer.js";
2
2
  import { z } from "zod";
3
3
  export declare const analyzeModShape: {
4
- task: z.ZodEnum<["summary", "decompile", "search", "class-source", "remap"]>;
4
+ task: z.ZodEnum<["summary", "decompile", "search", "class-source", "members", "remap"]>;
5
5
  subject: z.ZodDiscriminatedUnion<"kind", [z.ZodObject<{
6
6
  kind: z.ZodLiteral<"jar">;
7
7
  jarPath: z.ZodString;
@@ -38,7 +38,7 @@ export declare const analyzeModShape: {
38
38
  include: z.ZodOptional<z.ZodArray<z.ZodEnum<[string, ...string[]]>, "many">>;
39
39
  };
40
40
  export declare const analyzeModSchema: z.ZodEffects<z.ZodObject<{
41
- task: z.ZodEnum<["summary", "decompile", "search", "class-source", "remap"]>;
41
+ task: z.ZodEnum<["summary", "decompile", "search", "class-source", "members", "remap"]>;
42
42
  subject: z.ZodDiscriminatedUnion<"kind", [z.ZodObject<{
43
43
  kind: z.ZodLiteral<"jar">;
44
44
  jarPath: z.ZodString;
@@ -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
- task: "search" | "summary" | "class-source" | "remap" | "decompile";
78
+ task: "search" | "summary" | "class-source" | "members" | "remap" | "decompile";
79
79
  subject: {
80
80
  kind: "jar";
81
81
  jarPath: string;
@@ -96,7 +96,7 @@ export declare const analyzeModSchema: z.ZodEffects<z.ZodObject<{
96
96
  outputJar?: string | undefined;
97
97
  maxFiles?: number | undefined;
98
98
  }, {
99
- task: "search" | "summary" | "class-source" | "remap" | "decompile";
99
+ task: "search" | "summary" | "class-source" | "members" | "remap" | "decompile";
100
100
  subject: {
101
101
  kind: "jar";
102
102
  jarPath: string;
@@ -119,7 +119,7 @@ export declare const analyzeModSchema: z.ZodEffects<z.ZodObject<{
119
119
  maxFiles?: number | undefined;
120
120
  }>, {
121
121
  limit: number;
122
- task: "search" | "summary" | "class-source" | "remap" | "decompile";
122
+ task: "search" | "summary" | "class-source" | "members" | "remap" | "decompile";
123
123
  subject: {
124
124
  kind: "jar";
125
125
  jarPath: string;
@@ -140,7 +140,7 @@ export declare const analyzeModSchema: z.ZodEffects<z.ZodObject<{
140
140
  outputJar?: string | undefined;
141
141
  maxFiles?: number | undefined;
142
142
  }, {
143
- task: "search" | "summary" | "class-source" | "remap" | "decompile";
143
+ task: "search" | "summary" | "class-source" | "members" | "remap" | "decompile";
144
144
  subject: {
145
145
  kind: "jar";
146
146
  jarPath: string;
@@ -196,6 +196,12 @@ type AnalyzeModDeps = {
196
196
  }) => Promise<Record<string, unknown> & {
197
197
  warnings?: string[];
198
198
  }>;
199
+ getModClassMembers: (input: {
200
+ jarPath: string;
201
+ className: string;
202
+ }) => Promise<Record<string, unknown> & {
203
+ warnings?: string[];
204
+ }>;
199
205
  };
200
206
  export declare class AnalyzeModService {
201
207
  private readonly deps;
@@ -20,7 +20,7 @@ const subjectSchema = z.discriminatedUnion("kind", [
20
20
  })
21
21
  ]);
22
22
  export const analyzeModShape = {
23
- task: z.enum(["summary", "decompile", "search", "class-source", "remap"]),
23
+ task: z.enum(["summary", "decompile", "search", "class-source", "members", "remap"]),
24
24
  subject: subjectSchema,
25
25
  query: nonEmptyString.optional(),
26
26
  searchType: z.enum(["class", "method", "field", "content", "all"]).default("all"),
@@ -43,11 +43,11 @@ export const analyzeModSchema = z.object(analyzeModShape).superRefine((value, ct
43
43
  message: `${value.task} requires subject.kind=jar.`
44
44
  });
45
45
  }
46
- if (value.task === "class-source" && value.subject.kind !== "class") {
46
+ if ((value.task === "class-source" || value.task === "members") && value.subject.kind !== "class") {
47
47
  ctx.addIssue({
48
48
  code: z.ZodIssueCode.custom,
49
49
  path: ["subject", "kind"],
50
- message: "class-source requires subject.kind=class."
50
+ message: `${value.task} requires subject.kind=class.`
51
51
  });
52
52
  }
53
53
  if (value.task === "search" && !value.query) {
@@ -206,6 +206,40 @@ export class AnalyzeModService {
206
206
  warnings: Array.isArray(output.warnings) ? output.warnings : []
207
207
  };
208
208
  }
209
+ case "members": {
210
+ if (input.subject.kind !== "class") {
211
+ throw createError({
212
+ code: ERROR_CODES.INVALID_INPUT,
213
+ message: "members requires subject.kind=class."
214
+ });
215
+ }
216
+ const output = await this.deps.getModClassMembers({
217
+ jarPath: input.subject.jarPath,
218
+ className: input.subject.className
219
+ });
220
+ return {
221
+ ...buildEntryToolResult({
222
+ task: "members",
223
+ detail,
224
+ include,
225
+ summary: {
226
+ status: "ok",
227
+ headline: `Read bytecode members of ${input.subject.className}.`,
228
+ subject: createSummarySubject({
229
+ task: "members",
230
+ kind: input.subject.kind,
231
+ jarPath: input.subject.jarPath,
232
+ className: input.subject.className
233
+ })
234
+ },
235
+ blocks: {
236
+ members: output
237
+ },
238
+ alwaysBlocks: ["members"]
239
+ }),
240
+ warnings: Array.isArray(output.warnings) ? output.warnings : []
241
+ };
242
+ }
209
243
  case "remap": {
210
244
  const normalizedInputJar = normalizePathForHost(input.subject.jarPath, undefined, "jarPath");
211
245
  const analysis = await this.deps.analyzeModJar(normalizedInputJar);
@@ -180,6 +180,7 @@ export declare const analyzeSymbolSchema: z.ZodEffects<z.ZodObject<{
180
180
  }>;
181
181
  export type AnalyzeSymbolInput = z.infer<typeof analyzeSymbolSchema>;
182
182
  type AnalyzeSymbolDeps = {
183
+ detectProjectMinecraftVersion: (projectPath: string) => Promise<string | undefined>;
183
184
  checkSymbolExists: (input: {
184
185
  version: string;
185
186
  kind: "class" | "field" | "method";
@@ -254,5 +255,6 @@ export declare class AnalyzeSymbolService {
254
255
  execute(input: AnalyzeSymbolInput): Promise<Record<string, unknown> & {
255
256
  warnings?: string[];
256
257
  }>;
258
+ private dispatch;
257
259
  }
258
260
  export {};
@@ -112,11 +112,11 @@ const LIFECYCLE_ONLY_FIELDS = [
112
112
  "includeSnapshots"
113
113
  ];
114
114
  export const analyzeSymbolSchema = z.object(analyzeSymbolShape).superRefine((value, ctx) => {
115
- if (value.task !== "lifecycle" && !value.version) {
115
+ if (value.task !== "lifecycle" && !value.version && !value.projectPath) {
116
116
  ctx.addIssue({
117
117
  code: z.ZodIssueCode.custom,
118
118
  path: ["version"],
119
- message: "version is required for non-lifecycle tasks."
119
+ message: "version is required for non-lifecycle tasks (or pass projectPath so the version can be inferred from the workspace)."
120
120
  });
121
121
  }
122
122
  if (value.task === "lifecycle" && !value.version && !value.toVersion) {
@@ -189,6 +189,41 @@ export class AnalyzeSymbolService {
189
189
  const detail = resolveDetail(input.detail);
190
190
  const include = resolveInclude(input.include);
191
191
  const { kind: subjectKind, warning: inferenceWarning } = inferSubjectKind(input.subject);
192
+ // Omitted version with a supplied projectPath: infer from the workspace.
193
+ // Inference always carries provenance (versionInference block + warning);
194
+ // an explicit version is never overridden.
195
+ let versionInference;
196
+ if (input.task !== "lifecycle" && !input.version && input.projectPath) {
197
+ const detected = await this.deps.detectProjectMinecraftVersion(input.projectPath);
198
+ if (!detected) {
199
+ throw createError({
200
+ code: ERROR_CODES.WORKSPACE_VERSION_UNRESOLVED,
201
+ message: `Could not infer a Minecraft version from ${input.projectPath}.`,
202
+ details: {
203
+ projectPath: input.projectPath,
204
+ nextAction: "Declare minecraft_version (or mc_version) in gradle.properties, or pass version explicitly."
205
+ }
206
+ });
207
+ }
208
+ versionInference = {
209
+ version: detected,
210
+ source: `projectPath:gradle.properties (${input.projectPath})`
211
+ };
212
+ input = { ...input, version: detected };
213
+ }
214
+ const inferenceWarnings = versionInference
215
+ ? [
216
+ `version was inferred from the workspace: ${versionInference.version} (source: ${versionInference.source}).`
217
+ ]
218
+ : [];
219
+ const result = await this.dispatch(input, detail, include, subjectKind, inferenceWarning);
220
+ if (versionInference) {
221
+ result.versionInference = versionInference;
222
+ result.warnings = [...(result.warnings ?? []), ...inferenceWarnings];
223
+ }
224
+ return result;
225
+ }
226
+ async dispatch(input, detail, include, subjectKind, inferenceWarning) {
192
227
  switch (input.task) {
193
228
  case "exists": {
194
229
  const output = await this.deps.checkSymbolExists({
@@ -1015,6 +1015,10 @@ export type InspectMinecraftDeps = {
1015
1015
  cursor?: string;
1016
1016
  }) => Promise<ListArtifactFilesOutput>;
1017
1017
  detectProjectMinecraftVersion: (projectPath: string) => Promise<string | undefined>;
1018
+ listWorkspaceContexts: () => Array<{
1019
+ projectPath: string;
1020
+ minecraftVersion?: string;
1021
+ }>;
1018
1022
  };
1019
1023
  export declare function requireWorkspaceClassFocus(subject: Subject): WorkspaceClassFocus;
1020
1024
  export declare function requireWorkspaceSearchFocus(subject: Subject): WorkspaceSearchFocus;
@@ -392,6 +392,31 @@ export async function resolveArtifactReference(deps, subject, task) {
392
392
  }
393
393
  if (subject.kind === "class" || subject.kind === "file" || subject.kind === "search") {
394
394
  if (!subject.artifact) {
395
+ // Guardrailed auto-resolution: an omitted subject.artifact resolves
396
+ // through the workspace ONLY when exactly one workspace is known, and
397
+ // the response always carries a provenance warning. An explicit
398
+ // subject.artifact is never overridden (this branch requires absence).
399
+ const workspaces = deps.listWorkspaceContexts();
400
+ const unique = workspaces.length === 1 ? workspaces[0] : undefined;
401
+ if (unique) {
402
+ const version = unique.minecraftVersion ??
403
+ (await deps.detectProjectMinecraftVersion(unique.projectPath));
404
+ if (version) {
405
+ const artifact = await deps.resolveArtifact({
406
+ target: { kind: "version", value: version },
407
+ projectPath: unique.projectPath
408
+ });
409
+ return {
410
+ artifactId: artifact.artifactId,
411
+ artifact,
412
+ version,
413
+ warnings: [
414
+ `subject.artifact was omitted; auto-resolved through the unique known workspace ${unique.projectPath} (Minecraft ${version}). Pass subject.artifact to target a different artifact.`,
415
+ ...artifact.warnings
416
+ ]
417
+ };
418
+ }
419
+ }
395
420
  const suggestedTask = task
396
421
  ?? (subject.kind === "class"
397
422
  ? "class-overview"
@@ -403,6 +428,9 @@ export async function resolveArtifactReference(deps, subject, task) {
403
428
  message: `${subject.kind} subject requires artifact context.`,
404
429
  details: {
405
430
  nextAction: "Add subject.artifact or use subject.kind=workspace so inspect-minecraft can resolve the artifact first.",
431
+ ...(workspaces.length > 1
432
+ ? { workspaceCandidates: workspaces.map((workspace) => workspace.projectPath) }
433
+ : {}),
406
434
  ...(await buildArtifactContextSuggestedCall(deps, suggestedTask, subject))
407
435
  }
408
436
  });
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  import { type CacheRegistry } from "../cache-registry.js";
3
3
  export declare const manageCacheShape: {
4
4
  action: z.ZodEnum<["summary", "list", "inspect", "delete", "prune", "rebuild", "verify"]>;
5
- cacheKinds: z.ZodOptional<z.ZodArray<z.ZodEnum<["artifact-index", "downloads", "mapping", "registry", "decompiled-source", "mod-remap", "binary-remap", "workspace"]>, "many">>;
5
+ cacheKinds: z.ZodOptional<z.ZodArray<z.ZodEnum<["artifact-index", "downloads", "mapping", "registry", "decompiled-source", "mod-remap", "binary-remap", "nested-jar", "workspace"]>, "many">>;
6
6
  selector: z.ZodOptional<z.ZodObject<{
7
7
  artifactId: z.ZodOptional<z.ZodString>;
8
8
  version: z.ZodOptional<z.ZodString>;
@@ -42,7 +42,7 @@ export declare const manageCacheShape: {
42
42
  };
43
43
  export declare const manageCacheSchema: z.ZodObject<{
44
44
  action: z.ZodEnum<["summary", "list", "inspect", "delete", "prune", "rebuild", "verify"]>;
45
- cacheKinds: z.ZodOptional<z.ZodArray<z.ZodEnum<["artifact-index", "downloads", "mapping", "registry", "decompiled-source", "mod-remap", "binary-remap", "workspace"]>, "many">>;
45
+ cacheKinds: z.ZodOptional<z.ZodArray<z.ZodEnum<["artifact-index", "downloads", "mapping", "registry", "decompiled-source", "mod-remap", "binary-remap", "nested-jar", "workspace"]>, "many">>;
46
46
  selector: z.ZodOptional<z.ZodObject<{
47
47
  artifactId: z.ZodOptional<z.ZodString>;
48
48
  version: z.ZodOptional<z.ZodString>;
@@ -86,7 +86,7 @@ export declare const manageCacheSchema: z.ZodObject<{
86
86
  cursor?: string | undefined;
87
87
  detail?: "summary" | "standard" | "full" | undefined;
88
88
  include?: string[] | undefined;
89
- cacheKinds?: ("workspace" | "artifact-index" | "downloads" | "mapping" | "registry" | "decompiled-source" | "mod-remap" | "binary-remap")[] | undefined;
89
+ cacheKinds?: ("workspace" | "artifact-index" | "downloads" | "mapping" | "registry" | "decompiled-source" | "mod-remap" | "binary-remap" | "nested-jar")[] | undefined;
90
90
  selector?: {
91
91
  version?: string | undefined;
92
92
  projectPath?: string | undefined;
@@ -105,7 +105,7 @@ export declare const manageCacheSchema: z.ZodObject<{
105
105
  detail?: "summary" | "standard" | "full" | undefined;
106
106
  include?: string[] | undefined;
107
107
  executionMode?: "preview" | "apply" | undefined;
108
- cacheKinds?: ("workspace" | "artifact-index" | "downloads" | "mapping" | "registry" | "decompiled-source" | "mod-remap" | "binary-remap")[] | undefined;
108
+ cacheKinds?: ("workspace" | "artifact-index" | "downloads" | "mapping" | "registry" | "decompiled-source" | "mod-remap" | "binary-remap" | "nested-jar")[] | undefined;
109
109
  selector?: {
110
110
  version?: string | undefined;
111
111
  projectPath?: string | undefined;
@@ -38,6 +38,10 @@ export type RetryClass = "transient" | "permanent" | "server" | "environment" |
38
38
  * - `environment`: a server capability is missing (Java, decompiler, remapper).
39
39
  */
40
40
  export type IssueOrigin = "code_issue" | "tool_issue" | "environment";
41
+ export type DidYouMeanCandidate = {
42
+ className: string;
43
+ matchReason: string;
44
+ };
41
45
  export type ProblemDetails = {
42
46
  type: string;
43
47
  title: string;
@@ -51,9 +55,18 @@ export type ProblemDetails = {
51
55
  hints?: string[];
52
56
  suggestedCall?: SuggestedCall;
53
57
  exampleCalls?: ExampleCall[];
58
+ /** Ranked near-miss candidates for a class/symbol that was not found. */
59
+ didYouMean?: DidYouMeanCandidate[];
54
60
  failedStage?: string;
55
61
  context?: Record<string, string | number | boolean>;
56
62
  };
63
+ /**
64
+ * Validates and extracts a `didYouMean` array from error details. Like
65
+ * `suggestedCall`, it travels as a dedicated typed field — the primitive-only
66
+ * context allowlist is not loosened for it. Malformed payloads are dropped
67
+ * whole rather than partially published.
68
+ */
69
+ export declare function extractDidYouMean(details: unknown): DidYouMeanCandidate[] | undefined;
57
70
  export declare function statusForErrorCode(code: string): number;
58
71
  /**
59
72
  * Single source of truth mapping an error code to its {@link RetryClass}. Used
@@ -1,5 +1,30 @@
1
1
  import { ERROR_CODES, isAppError } from "./errors.js";
2
2
  import { log } from "./logger.js";
3
+ const MAX_DID_YOU_MEAN_ENTRIES = 16;
4
+ /**
5
+ * Validates and extracts a `didYouMean` array from error details. Like
6
+ * `suggestedCall`, it travels as a dedicated typed field — the primitive-only
7
+ * context allowlist is not loosened for it. Malformed payloads are dropped
8
+ * whole rather than partially published.
9
+ */
10
+ export function extractDidYouMean(details) {
11
+ const raw = details?.didYouMean;
12
+ if (!Array.isArray(raw)) {
13
+ return undefined;
14
+ }
15
+ const cleaned = [];
16
+ for (const entry of raw) {
17
+ if (!entry || typeof entry !== "object") {
18
+ return undefined;
19
+ }
20
+ const { className, matchReason } = entry;
21
+ if (typeof className !== "string" || typeof matchReason !== "string") {
22
+ return undefined;
23
+ }
24
+ cleaned.push({ className, matchReason });
25
+ }
26
+ return cleaned.slice(0, MAX_DID_YOU_MEAN_ENTRIES);
27
+ }
3
28
  export function statusForErrorCode(code) {
4
29
  if (code === ERROR_CODES.BATCH_ABORTED) {
5
30
  return 412;
@@ -9,6 +34,9 @@ export function statusForErrorCode(code) {
9
34
  // not an internal server failure.
10
35
  return 408;
11
36
  }
37
+ if (code === ERROR_CODES.TOOL_TIMEOUT) {
38
+ return 408;
39
+ }
12
40
  if (code === ERROR_CODES.INVALID_INPUT ||
13
41
  code === ERROR_CODES.COORDINATE_PARSE_FAILED ||
14
42
  code === ERROR_CODES.INVALID_LINE_RANGE ||
@@ -35,7 +63,8 @@ export function statusForErrorCode(code) {
35
63
  code === ERROR_CODES.DECOMPILE_DISABLED ||
36
64
  code === ERROR_CODES.REMAP_FAILED ||
37
65
  code === ERROR_CODES.WORKSPACE_VERSION_UNRESOLVED ||
38
- code === ERROR_CODES.DEPENDENCY_VERSION_UNRESOLVED) {
66
+ code === ERROR_CODES.DEPENDENCY_VERSION_UNRESOLVED ||
67
+ code === ERROR_CODES.NESTED_JAR_AMBIGUOUS) {
39
68
  return 422;
40
69
  }
41
70
  if (code === ERROR_CODES.REMAPPER_UNAVAILABLE ||
@@ -82,6 +111,7 @@ const RETRY_CLASS_PERMANENT = new Set([
82
111
  ERROR_CODES.REMAP_FAILED,
83
112
  ERROR_CODES.WORKSPACE_VERSION_UNRESOLVED,
84
113
  ERROR_CODES.DEPENDENCY_VERSION_UNRESOLVED,
114
+ ERROR_CODES.NESTED_JAR_AMBIGUOUS,
85
115
  ERROR_CODES.PROVENANCE_INCOMPLETE,
86
116
  ERROR_CODES.BATCH_ABORTED
87
117
  ]);
@@ -145,7 +175,8 @@ const ISSUE_ORIGIN_CODE = new Set([
145
175
  ERROR_CODES.JAR_NOT_FOUND,
146
176
  ERROR_CODES.VERSION_NOT_FOUND,
147
177
  ERROR_CODES.WORKSPACE_VERSION_UNRESOLVED,
148
- ERROR_CODES.DEPENDENCY_VERSION_UNRESOLVED
178
+ ERROR_CODES.DEPENDENCY_VERSION_UNRESOLVED,
179
+ ERROR_CODES.NESTED_JAR_AMBIGUOUS
149
180
  ]);
150
181
  /**
151
182
  * Single source of truth mapping an error code to its {@link IssueOrigin}.
@@ -243,6 +274,7 @@ export function errorToBatchEntryProblem(caughtError, instance, options) {
243
274
  const baseHints = extractHints(caughtError.details);
244
275
  const fieldErrors = extractFieldErrors(caughtError.details);
245
276
  const context = extractAllowlistedContext(caughtError.details);
277
+ const didYouMean = extractDidYouMean(caughtError.details);
246
278
  return {
247
279
  type: `https://minecraft-modding-mcp.dev/problems/${caughtError.code.toLowerCase()}`,
248
280
  title: "Tool execution error",
@@ -255,6 +287,7 @@ export function errorToBatchEntryProblem(caughtError, instance, options) {
255
287
  ...(fieldErrors ? { fieldErrors } : {}),
256
288
  ...(baseHints ? { hints: baseHints } : {}),
257
289
  ...(options?.suggestedCall ? { suggestedCall: options.suggestedCall } : {}),
290
+ ...(didYouMean ? { didYouMean } : {}),
258
291
  ...(context ? { context } : {})
259
292
  };
260
293
  }
package/dist/errors.d.ts CHANGED
@@ -31,10 +31,12 @@ export declare const ERROR_CODES: {
31
31
  readonly REMAP_FAILED: "ERR_REMAP_FAILED";
32
32
  readonly REMAPPER_UNAVAILABLE: "ERR_REMAPPER_UNAVAILABLE";
33
33
  readonly WORKER_RESTART: "ERR_WORKER_RESTART";
34
+ readonly TOOL_TIMEOUT: "ERR_TOOL_TIMEOUT";
34
35
  readonly MIXIN_PARSE_FAILED: "ERR_MIXIN_PARSE_FAILED";
35
36
  readonly STAGE_BUDGET_PRE_PARSE: "ERR_STAGE_BUDGET_PRE_PARSE";
36
37
  readonly WORKSPACE_VERSION_UNRESOLVED: "ERR_WORKSPACE_VERSION_UNRESOLVED";
37
38
  readonly DEPENDENCY_VERSION_UNRESOLVED: "ERR_DEPENDENCY_VERSION_UNRESOLVED";
39
+ readonly NESTED_JAR_AMBIGUOUS: "ERR_NESTED_JAR_AMBIGUOUS";
38
40
  readonly BATCH_ABORTED: "ERR_BATCH_ABORTED";
39
41
  readonly INTERNAL: "ERR_INTERNAL";
40
42
  };
package/dist/errors.js CHANGED
@@ -31,10 +31,12 @@ export const ERROR_CODES = {
31
31
  REMAP_FAILED: "ERR_REMAP_FAILED",
32
32
  REMAPPER_UNAVAILABLE: "ERR_REMAPPER_UNAVAILABLE",
33
33
  WORKER_RESTART: "ERR_WORKER_RESTART",
34
+ TOOL_TIMEOUT: "ERR_TOOL_TIMEOUT",
34
35
  MIXIN_PARSE_FAILED: "ERR_MIXIN_PARSE_FAILED",
35
36
  STAGE_BUDGET_PRE_PARSE: "ERR_STAGE_BUDGET_PRE_PARSE",
36
37
  WORKSPACE_VERSION_UNRESOLVED: "ERR_WORKSPACE_VERSION_UNRESOLVED",
37
38
  DEPENDENCY_VERSION_UNRESOLVED: "ERR_DEPENDENCY_VERSION_UNRESOLVED",
39
+ NESTED_JAR_AMBIGUOUS: "ERR_NESTED_JAR_AMBIGUOUS",
38
40
  BATCH_ABORTED: "ERR_BATCH_ABORTED",
39
41
  INTERNAL: "ERR_INTERNAL"
40
42
  };