@adhisang/minecraft-modding-mcp 6.2.0 → 6.3.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.
package/CHANGELOG.md CHANGED
@@ -5,7 +5,16 @@ 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
- ## [Unreleased]
8
+ ## [6.3.0] - 2026-07-18
9
+
10
+ ### Added
11
+
12
+ - `find-class` accepts top-level `projectPath` for workspace-relative `target.kind="workspace"` and dependency targets using `versionFromProject`, and searches simple or qualified class names across nested `.class` inventories when the resolved artifact is a Jar-in-Jar shell such as the Fabric API umbrella JAR. Nested matches retain the existing response shape, deduplicate repeated qualified names, and honor `limit`; top-level matches remain directly usable by the source/member tools.
13
+
14
+ ### Fixed
15
+
16
+ - `inspect-minecraft` now describes `task`, structured workspace `subject.focus`, and each class/search/file focus branch in `tools/list`. A string focus remains invalid instead of being guessed or coerced, but its `ERR_INVALID_INPUT` response now includes three schema-validated `exampleCalls` that preserve valid workspace context and show the exact object shapes for retrying.
17
+ - `find-class` no longer interprets a native dependency or Jar-in-Jar shell miss as evidence of obfuscated Minecraft runtime names, so it does not recommend `mapping="mojang"` for those artifacts. Dotted inner-class names discovered in a shell also continue through `get-class-source` instead of missing the binary `$` entry.
9
18
 
10
19
  ## [6.2.0] - 2026-07-11
11
20
 
package/README.md CHANGED
@@ -156,7 +156,7 @@ All six return `result.summary` first and can include `summary.nextActions` when
156
156
  These notes cover high-frequency decisions during onboarding. For the full pitfall list, exact contracts, migration notes, and environment variables, see [docs/tool-reference.md](docs/tool-reference.md).
157
157
 
158
158
  - `search-class-source` defaults to `queryMode="auto"` and keeps separator queries such as `foo.bar`, `foo_bar`, and `foo$bar` on the indexed path. Use `queryMode="literal"` for an explicit full substring scan.
159
- - If you do not already have an artifact, prefer `subject.kind="workspace"` for `inspect-minecraft` instead of guessing artifact details. When artifact context is the only missing input, a retryable `suggestedCall` preserves the requested task.
159
+ - If you do not already have an artifact, prefer `subject.kind="workspace"` for `inspect-minecraft` instead of guessing artifact details. `subject.focus` is an object, not a string: use `{ "kind": "class", "className": "..." }`, `{ "kind": "search", "query": "..." }`, or `{ "kind": "file", "filePath": "..." }`. `task="auto"` dispatches only from `subject.kind` and `focus.kind`; it is not a natural-language planner. Invalid string focus returns class/search/file `exampleCalls`, while missing artifact context keeps the requested task in a retryable `suggestedCall`.
160
160
  - `trace-symbol-lifecycle` expects `Class.method` in `symbol`. Keep exact overload matching in the separate `descriptor` field.
161
161
  - For unobfuscated releases such as `26.1+`, `check-symbol-exists` and `analyze-symbol task="exists"` validate `mojang` lookups against runtime bytecode when no mapping graph exists, and return `mapping_unavailable` when the runtime JAR itself is unreachable.
162
162
  - `analyze-mod` and `validate-project` require structured `subject` objects and canonical `include` groups; stale string-subject or domain-include payloads return `ERR_INVALID_INPUT` with a retryable `suggestedCall`.
@@ -278,7 +278,7 @@ Tools for browsing Minecraft versions, resolving source artifacts, and reading o
278
278
  | --- | --- |
279
279
  | `list-versions` | List available Minecraft versions from Mojang metadata and local cache |
280
280
  | `resolve-artifact` | Resolve source artifacts from versions, JAR paths, or Maven coordinates |
281
- | `find-class` | Find simple or fully-qualified class names inside an artifact |
281
+ | `find-class` | Find simple or fully-qualified class names, including classes bundled in nested JARs |
282
282
  | `get-class-source` | Read class source from an artifact or resolve the backing artifact on demand |
283
283
  | `get-class-members` | List constructors, fields, and methods from bytecode |
284
284
  | `search-class-source` | Search indexed class source by symbol, text, or path |
@@ -287,6 +287,8 @@ Tools for browsing Minecraft versions, resolving source artifacts, and reading o
287
287
  | `index-artifact` | Rebuild indexed metadata for an existing artifact |
288
288
  <!-- END GENERATED TOOL TABLE: source-exploration -->
289
289
 
290
+ `find-class` accepts either an `artifactId` or the shared object `target` shape. For a workspace-relative dependency, pass `target: { kind: "dependency", group, name, versionFromProject: true }` with top-level `projectPath`. Fabric-style umbrella JARs are searched through their nested `.class` inventories; a top-level class match can then be passed to `get-class-source` or `get-class-members`, which resolve the containing nested JAR. Dotted inner-class matches are also readable through `get-class-source`. An empty result still means that the requested class name is absent from the resolved dependency version.
291
+
290
292
  For unobfuscated releases such as `26.1+`, `mapping="mojang"` uses the runtime/decompile path directly and skips Loom source-jar discovery, while `intermediary` and `yarn` fall back to `obfuscated` with a warning.
291
293
 
292
294
  ### Version Comparison & Symbol Tracking
@@ -415,12 +417,16 @@ pnpm test
415
417
 
416
418
  Run these when relevant:
417
419
 
420
+ - `pnpm test:file <path...>` for focused ordinary suites
421
+ - `pnpm test:grep <name-pattern>` for recursive ordinary suites filtered by test name
418
422
  - `pnpm test:manual:stdio-smoke` for MCP transport, registration, or manual workflow changes
419
423
  - `pnpm test:manual:package-smoke` when checking packaged install and distribution behavior
420
424
  - `pnpm test:perf` for search, index, or performance-sensitive changes
421
425
  - `pnpm test:coverage` or `pnpm test:coverage:lcov` for coverage checks (`lines=80`, `branches=70`, `functions=80`)
422
426
  - `pnpm validate` for the full local validation suite
423
427
 
428
+ Ordinary `.test.ts` files live in domain directories under `tests/` (for example `source-service/`, `entry-tools/`, `mapping/`, `mixin/`, `integration/mcp-tools/`, `contracts/`, and `utils/`). Keep helper-only modules in `tests/helpers`; manual smoke, performance, resource, and smoke-only files remain in their existing special directories.
429
+
424
430
  ## License
425
431
 
426
432
  [MIT](LICENSE)
@@ -90,10 +90,10 @@ export declare const analyzeSymbolSchema: z.ZodEffects<z.ZodObject<{
90
90
  detail?: "summary" | "standard" | "full" | undefined;
91
91
  include?: string[] | undefined;
92
92
  targetMapping?: "obfuscated" | "mojang" | "intermediary" | "yarn" | undefined;
93
+ gradleUserHome?: string | undefined;
93
94
  maxVersions?: number | undefined;
94
95
  sourceMapping?: "obfuscated" | "mojang" | "intermediary" | "yarn" | undefined;
95
96
  classNameMapping?: "obfuscated" | "mojang" | "intermediary" | "yarn" | undefined;
96
- gradleUserHome?: string | undefined;
97
97
  includeSnapshots?: boolean | undefined;
98
98
  fromVersion?: string | undefined;
99
99
  toVersion?: string | undefined;
@@ -114,10 +114,10 @@ export declare const analyzeSymbolSchema: z.ZodEffects<z.ZodObject<{
114
114
  detail?: "summary" | "standard" | "full" | undefined;
115
115
  include?: string[] | undefined;
116
116
  targetMapping?: "obfuscated" | "mojang" | "intermediary" | "yarn" | undefined;
117
+ gradleUserHome?: string | undefined;
117
118
  maxVersions?: number | undefined;
118
119
  sourceMapping?: "obfuscated" | "mojang" | "intermediary" | "yarn" | undefined;
119
120
  classNameMapping?: "obfuscated" | "mojang" | "intermediary" | "yarn" | undefined;
120
- gradleUserHome?: string | undefined;
121
121
  nameMode?: "auto" | "fqcn" | undefined;
122
122
  includeSnapshots?: boolean | undefined;
123
123
  fromVersion?: string | undefined;
@@ -142,10 +142,10 @@ export declare const analyzeSymbolSchema: z.ZodEffects<z.ZodObject<{
142
142
  detail?: "summary" | "standard" | "full" | undefined;
143
143
  include?: string[] | undefined;
144
144
  targetMapping?: "obfuscated" | "mojang" | "intermediary" | "yarn" | undefined;
145
+ gradleUserHome?: string | undefined;
145
146
  maxVersions?: number | undefined;
146
147
  sourceMapping?: "obfuscated" | "mojang" | "intermediary" | "yarn" | undefined;
147
148
  classNameMapping?: "obfuscated" | "mojang" | "intermediary" | "yarn" | undefined;
148
- gradleUserHome?: string | undefined;
149
149
  includeSnapshots?: boolean | undefined;
150
150
  fromVersion?: string | undefined;
151
151
  toVersion?: string | undefined;
@@ -166,10 +166,10 @@ export declare const analyzeSymbolSchema: z.ZodEffects<z.ZodObject<{
166
166
  detail?: "summary" | "standard" | "full" | undefined;
167
167
  include?: string[] | undefined;
168
168
  targetMapping?: "obfuscated" | "mojang" | "intermediary" | "yarn" | undefined;
169
+ gradleUserHome?: string | undefined;
169
170
  maxVersions?: number | undefined;
170
171
  sourceMapping?: "obfuscated" | "mojang" | "intermediary" | "yarn" | undefined;
171
172
  classNameMapping?: "obfuscated" | "mojang" | "intermediary" | "yarn" | undefined;
172
- gradleUserHome?: string | undefined;
173
173
  nameMode?: "auto" | "fqcn" | undefined;
174
174
  includeSnapshots?: boolean | undefined;
175
175
  fromVersion?: string | undefined;
@@ -401,6 +401,7 @@ export declare const subjectSchema: z.ZodDiscriminatedUnion<"kind", [z.ZodObject
401
401
  scope?: "vanilla" | "merged" | "loader" | undefined;
402
402
  preferProjectVersion?: boolean | undefined;
403
403
  strictVersion?: boolean | undefined;
404
+ gradleUserHome?: string | undefined;
404
405
  artifact?: {
405
406
  artifactId: string;
406
407
  type: "resolved-id";
@@ -411,7 +412,6 @@ export declare const subjectSchema: z.ZodDiscriminatedUnion<"kind", [z.ZodObject
411
412
  };
412
413
  type: "resolve-target";
413
414
  } | undefined;
414
- gradleUserHome?: string | undefined;
415
415
  }, {
416
416
  kind: "class";
417
417
  className: string;
@@ -420,6 +420,7 @@ export declare const subjectSchema: z.ZodDiscriminatedUnion<"kind", [z.ZodObject
420
420
  scope?: "vanilla" | "merged" | "loader" | undefined;
421
421
  preferProjectVersion?: boolean | undefined;
422
422
  strictVersion?: boolean | undefined;
423
+ gradleUserHome?: string | undefined;
423
424
  artifact?: {
424
425
  artifactId: string;
425
426
  type: "resolved-id";
@@ -430,7 +431,6 @@ export declare const subjectSchema: z.ZodDiscriminatedUnion<"kind", [z.ZodObject
430
431
  };
431
432
  type: "resolve-target";
432
433
  } | undefined;
433
- gradleUserHome?: string | undefined;
434
434
  }>, z.ZodObject<{
435
435
  kind: z.ZodLiteral<"file">;
436
436
  filePath: z.ZodString;
@@ -1044,7 +1044,7 @@ export declare function resolveWorkspaceArtifactReference(deps: InspectMinecraft
1044
1044
  version?: string;
1045
1045
  warnings: string[];
1046
1046
  }>;
1047
- export declare function resolveTask(task: InspectMinecraftTask | undefined, subject: Subject | undefined): "search" | "class-source" | "artifact" | "file" | "versions" | "class-members" | "class-overview" | "list-files";
1047
+ export declare function resolveTask(task: InspectMinecraftTask | undefined, subject: Subject | undefined): "search" | "class-source" | "file" | "class-overview" | "class-members" | "artifact" | "versions" | "list-files";
1048
1048
  export declare function summarizeRequestedSubject(subject: Subject): Record<string, unknown>;
1049
1049
  export declare function taskForSubject(subject: Subject): ConcreteInspectMinecraftTask;
1050
1050
  export declare function invalidTaskSubjectError(task: ConcreteInspectMinecraftTask, subject: Subject): never;
@@ -30,27 +30,27 @@ export const artifactRefSchema = z.discriminatedUnion("type", [
30
30
  ]);
31
31
  export const workspaceFocusSchema = z.discriminatedUnion("kind", [
32
32
  z.object({
33
- kind: z.literal("class"),
34
- className: nonEmptyString,
35
- artifact: artifactRefSchema.optional()
36
- }),
33
+ kind: z.literal("class").describe("Select class inspection."),
34
+ className: nonEmptyString.describe("Fully-qualified class name to inspect."),
35
+ artifact: artifactRefSchema.optional().describe("Optional explicit artifact override; otherwise resolve it from the workspace.")
36
+ }).describe('Class focus object: {"kind":"class","className":"net.minecraft.world.item.Item"}. task=auto dispatches it to class-overview; select class-source or class-members explicitly when needed.'),
37
37
  z.object({
38
- kind: z.literal("file"),
39
- filePath: nonEmptyString,
40
- artifact: artifactRefSchema.optional()
41
- }),
38
+ kind: z.literal("file").describe("Select an artifact-relative file read."),
39
+ filePath: nonEmptyString.describe("Artifact-relative file path to read."),
40
+ artifact: artifactRefSchema.optional().describe("Optional explicit artifact override; otherwise resolve it from the workspace.")
41
+ }).describe('File focus object: {"kind":"file","filePath":"net/minecraft/world/item/Item.java"}. task=auto dispatches it to file.'),
42
42
  z.object({
43
- kind: z.literal("search"),
44
- query: nonEmptyString,
45
- artifact: artifactRefSchema.optional(),
43
+ kind: z.literal("search").describe("Select source search."),
44
+ query: nonEmptyString.describe("Source, symbol, or path query to search for."),
45
+ artifact: artifactRefSchema.optional().describe("Optional explicit artifact override; otherwise resolve it from the workspace."),
46
46
  intent: z.enum(["symbol", "text", "path"]).optional(),
47
47
  match: z.enum(["exact", "prefix", "contains", "regex"]).optional(),
48
48
  symbolKind: z.enum(["class", "interface", "enum", "record", "method", "field"]).optional(),
49
49
  packagePrefix: nonEmptyString.optional(),
50
50
  fileGlob: nonEmptyString.optional(),
51
51
  queryMode: z.enum(["auto", "token", "literal"]).default("auto")
52
- })
53
- ]);
52
+ }).describe('Search focus object: {"kind":"search","query":"CreativeModeTab"}. task=auto dispatches it to search.')
53
+ ]).describe('Structured workspace focus. Object, not string. Choose {kind:"class",className}, {kind:"file",filePath}, or {kind:"search",query}.');
54
54
  export const subjectSchema = z.discriminatedUnion("kind", [
55
55
  z.object({
56
56
  kind: z.literal("version"),
@@ -108,8 +108,8 @@ export const subjectSchema = z.discriminatedUnion("kind", [
108
108
  preferProjectVersion: z.boolean().optional(),
109
109
  strictVersion: z.boolean().optional(),
110
110
  focus: workspaceFocusSchema.optional()
111
- })
112
- ]);
111
+ }).describe("Workspace subject. Resolves Minecraft artifact context from projectPath; add a structured focus object for class, file, or search work.")
112
+ ]).describe("Structured inspection subject. Use kind=workspace with a structured focus object when the workspace should resolve artifact context.");
113
113
  export function hasPartialVanillaCoverage(artifact) {
114
114
  return artifact?.qualityFlags.includes("partial-source-no-net-minecraft") === true
115
115
  || artifact?.artifactContents.sourceCoverage === "partial";
@@ -158,6 +158,7 @@ export declare const inspectMinecraftShape: {
158
158
  scope?: "vanilla" | "merged" | "loader" | undefined;
159
159
  preferProjectVersion?: boolean | undefined;
160
160
  strictVersion?: boolean | undefined;
161
+ gradleUserHome?: string | undefined;
161
162
  artifact?: {
162
163
  artifactId: string;
163
164
  type: "resolved-id";
@@ -168,7 +169,6 @@ export declare const inspectMinecraftShape: {
168
169
  };
169
170
  type: "resolve-target";
170
171
  } | undefined;
171
- gradleUserHome?: string | undefined;
172
172
  }, {
173
173
  kind: "class";
174
174
  className: string;
@@ -177,6 +177,7 @@ export declare const inspectMinecraftShape: {
177
177
  scope?: "vanilla" | "merged" | "loader" | undefined;
178
178
  preferProjectVersion?: boolean | undefined;
179
179
  strictVersion?: boolean | undefined;
180
+ gradleUserHome?: string | undefined;
180
181
  artifact?: {
181
182
  artifactId: string;
182
183
  type: "resolved-id";
@@ -187,7 +188,6 @@ export declare const inspectMinecraftShape: {
187
188
  };
188
189
  type: "resolve-target";
189
190
  } | undefined;
190
- gradleUserHome?: string | undefined;
191
191
  }>, z.ZodObject<{
192
192
  kind: z.ZodLiteral<"file">;
193
193
  filePath: z.ZodString;
@@ -821,6 +821,7 @@ export declare const inspectMinecraftSchema: z.ZodEffects<z.ZodObject<{
821
821
  scope?: "vanilla" | "merged" | "loader" | undefined;
822
822
  preferProjectVersion?: boolean | undefined;
823
823
  strictVersion?: boolean | undefined;
824
+ gradleUserHome?: string | undefined;
824
825
  artifact?: {
825
826
  artifactId: string;
826
827
  type: "resolved-id";
@@ -831,7 +832,6 @@ export declare const inspectMinecraftSchema: z.ZodEffects<z.ZodObject<{
831
832
  };
832
833
  type: "resolve-target";
833
834
  } | undefined;
834
- gradleUserHome?: string | undefined;
835
835
  }, {
836
836
  kind: "class";
837
837
  className: string;
@@ -840,6 +840,7 @@ export declare const inspectMinecraftSchema: z.ZodEffects<z.ZodObject<{
840
840
  scope?: "vanilla" | "merged" | "loader" | undefined;
841
841
  preferProjectVersion?: boolean | undefined;
842
842
  strictVersion?: boolean | undefined;
843
+ gradleUserHome?: string | undefined;
843
844
  artifact?: {
844
845
  artifactId: string;
845
846
  type: "resolved-id";
@@ -850,7 +851,6 @@ export declare const inspectMinecraftSchema: z.ZodEffects<z.ZodObject<{
850
851
  };
851
852
  type: "resolve-target";
852
853
  } | undefined;
853
- gradleUserHome?: string | undefined;
854
854
  }>, z.ZodObject<{
855
855
  kind: z.ZodLiteral<"file">;
856
856
  filePath: z.ZodString;
@@ -1329,7 +1329,7 @@ export declare const inspectMinecraftSchema: z.ZodEffects<z.ZodObject<{
1329
1329
  includeSnapshots: boolean;
1330
1330
  limit?: number | undefined;
1331
1331
  cursor?: string | undefined;
1332
- task?: "search" | "class-source" | "artifact" | "auto" | "file" | "versions" | "class-members" | "class-overview" | "list-files" | undefined;
1332
+ task?: "search" | "class-source" | "file" | "auto" | "class-overview" | "class-members" | "artifact" | "versions" | "list-files" | undefined;
1333
1333
  detail?: "summary" | "standard" | "full" | undefined;
1334
1334
  subject?: {
1335
1335
  kind: "version";
@@ -1366,6 +1366,7 @@ export declare const inspectMinecraftSchema: z.ZodEffects<z.ZodObject<{
1366
1366
  scope?: "vanilla" | "merged" | "loader" | undefined;
1367
1367
  preferProjectVersion?: boolean | undefined;
1368
1368
  strictVersion?: boolean | undefined;
1369
+ gradleUserHome?: string | undefined;
1369
1370
  artifact?: {
1370
1371
  artifactId: string;
1371
1372
  type: "resolved-id";
@@ -1376,7 +1377,6 @@ export declare const inspectMinecraftSchema: z.ZodEffects<z.ZodObject<{
1376
1377
  };
1377
1378
  type: "resolve-target";
1378
1379
  } | undefined;
1379
- gradleUserHome?: string | undefined;
1380
1380
  } | {
1381
1381
  kind: "file";
1382
1382
  filePath: string;
@@ -1468,7 +1468,7 @@ export declare const inspectMinecraftSchema: z.ZodEffects<z.ZodObject<{
1468
1468
  }, {
1469
1469
  limit?: number | undefined;
1470
1470
  cursor?: string | undefined;
1471
- task?: "search" | "class-source" | "artifact" | "auto" | "file" | "versions" | "class-members" | "class-overview" | "list-files" | undefined;
1471
+ task?: "search" | "class-source" | "file" | "auto" | "class-overview" | "class-members" | "artifact" | "versions" | "list-files" | undefined;
1472
1472
  detail?: "summary" | "standard" | "full" | undefined;
1473
1473
  subject?: {
1474
1474
  kind: "version";
@@ -1505,6 +1505,7 @@ export declare const inspectMinecraftSchema: z.ZodEffects<z.ZodObject<{
1505
1505
  scope?: "vanilla" | "merged" | "loader" | undefined;
1506
1506
  preferProjectVersion?: boolean | undefined;
1507
1507
  strictVersion?: boolean | undefined;
1508
+ gradleUserHome?: string | undefined;
1508
1509
  artifact?: {
1509
1510
  artifactId: string;
1510
1511
  type: "resolved-id";
@@ -1515,7 +1516,6 @@ export declare const inspectMinecraftSchema: z.ZodEffects<z.ZodObject<{
1515
1516
  };
1516
1517
  type: "resolve-target";
1517
1518
  } | undefined;
1518
- gradleUserHome?: string | undefined;
1519
1519
  } | {
1520
1520
  kind: "file";
1521
1521
  filePath: string;
@@ -1609,7 +1609,7 @@ export declare const inspectMinecraftSchema: z.ZodEffects<z.ZodObject<{
1609
1609
  includeSnapshots: boolean;
1610
1610
  limit?: number | undefined;
1611
1611
  cursor?: string | undefined;
1612
- task?: "search" | "class-source" | "artifact" | "auto" | "file" | "versions" | "class-members" | "class-overview" | "list-files" | undefined;
1612
+ task?: "search" | "class-source" | "file" | "auto" | "class-overview" | "class-members" | "artifact" | "versions" | "list-files" | undefined;
1613
1613
  detail?: "summary" | "standard" | "full" | undefined;
1614
1614
  subject?: {
1615
1615
  kind: "version";
@@ -1646,6 +1646,7 @@ export declare const inspectMinecraftSchema: z.ZodEffects<z.ZodObject<{
1646
1646
  scope?: "vanilla" | "merged" | "loader" | undefined;
1647
1647
  preferProjectVersion?: boolean | undefined;
1648
1648
  strictVersion?: boolean | undefined;
1649
+ gradleUserHome?: string | undefined;
1649
1650
  artifact?: {
1650
1651
  artifactId: string;
1651
1652
  type: "resolved-id";
@@ -1656,7 +1657,6 @@ export declare const inspectMinecraftSchema: z.ZodEffects<z.ZodObject<{
1656
1657
  };
1657
1658
  type: "resolve-target";
1658
1659
  } | undefined;
1659
- gradleUserHome?: string | undefined;
1660
1660
  } | {
1661
1661
  kind: "file";
1662
1662
  filePath: string;
@@ -1748,7 +1748,7 @@ export declare const inspectMinecraftSchema: z.ZodEffects<z.ZodObject<{
1748
1748
  }, {
1749
1749
  limit?: number | undefined;
1750
1750
  cursor?: string | undefined;
1751
- task?: "search" | "class-source" | "artifact" | "auto" | "file" | "versions" | "class-members" | "class-overview" | "list-files" | undefined;
1751
+ task?: "search" | "class-source" | "file" | "auto" | "class-overview" | "class-members" | "artifact" | "versions" | "list-files" | undefined;
1752
1752
  detail?: "summary" | "standard" | "full" | undefined;
1753
1753
  subject?: {
1754
1754
  kind: "version";
@@ -1785,6 +1785,7 @@ export declare const inspectMinecraftSchema: z.ZodEffects<z.ZodObject<{
1785
1785
  scope?: "vanilla" | "merged" | "loader" | undefined;
1786
1786
  preferProjectVersion?: boolean | undefined;
1787
1787
  strictVersion?: boolean | undefined;
1788
+ gradleUserHome?: string | undefined;
1788
1789
  artifact?: {
1789
1790
  artifactId: string;
1790
1791
  type: "resolved-id";
@@ -1795,7 +1796,6 @@ export declare const inspectMinecraftSchema: z.ZodEffects<z.ZodObject<{
1795
1796
  };
1796
1797
  type: "resolve-target";
1797
1798
  } | undefined;
1798
- gradleUserHome?: string | undefined;
1799
1799
  } | {
1800
1800
  kind: "file";
1801
1801
  filePath: string;
@@ -14,7 +14,7 @@ import { handleListFiles } from "./inspect-minecraft/handlers/list-files.js";
14
14
  const INCLUDE_GROUPS = ["warnings", "provenance", "candidates", "members", "descriptors", "source", "files", "samples", "artifact", "timings"];
15
15
  const nonEmptyString = z.string().trim().min(1);
16
16
  export const inspectMinecraftShape = {
17
- task: z.enum(TASKS).optional(),
17
+ task: z.enum(TASKS).optional().describe("Workflow task. auto dispatches only from subject.kind and workspace focus.kind; it is not a natural-language planner."),
18
18
  subject: subjectSchema.optional(),
19
19
  includeSnapshots: z.boolean().default(false),
20
20
  detail: detailSchema.optional(),
package/dist/index.js CHANGED
@@ -114,7 +114,7 @@ const workspaceMappingService = new WorkspaceMappingService();
114
114
  const inspectMinecraftService = new InspectMinecraftService({
115
115
  listVersions: (input) => sourceService.listVersions(input),
116
116
  resolveArtifact: (input) => sourceService.resolveArtifact(input),
117
- findClass: (input) => Promise.resolve(sourceService.findClass(input)),
117
+ findClass: (input) => sourceService.findClassIncludingNested(input),
118
118
  checkSymbolExists: (input) => sourceService.checkSymbolExists(input),
119
119
  getClassSource: (input) => sourceService.getClassSource(input),
120
120
  getClassMembers: (input) => sourceService.getClassMembers(input),
@@ -472,7 +472,7 @@ expertTool("list-versions", "List available Minecraft versions from Mojang manif
472
472
  limit: input.limit
473
473
  })));
474
474
  registerToolSchema("list-versions", listVersionsSchema);
475
- server.tool("inspect-minecraft", "Top-level workflow tool for version discovery, artifact resolution, class inspection, source search, file reads, and file listings.", inspectMinecraftShape, { readOnlyHint: true }, async (args) => runTool("inspect-minecraft", args, inspectMinecraftSchema, async (input) => inspectMinecraftService.execute(input)));
475
+ server.tool("inspect-minecraft", "Top-level workflow tool for version discovery, artifact resolution, class inspection, source search, file reads, and file listings. Workspace subject.focus is a structured class/file/search object, never a string; task=auto dispatches from subject.kind and focus.kind.", inspectMinecraftShape, { readOnlyHint: true }, async (args) => runTool("inspect-minecraft", args, inspectMinecraftSchema, async (input) => inspectMinecraftService.execute(input)));
476
476
  registerToolSchema("inspect-minecraft", inspectMinecraftSchema);
477
477
  server.tool("analyze-symbol", "Top-level workflow tool for symbol existence, mapping, lifecycle, workspace analysis, and API overview. subject.kind='symbol' auto-detects class/field/method from the selector.", analyzeSymbolShape, { readOnlyHint: true }, async (args) => runTool("analyze-symbol", args, analyzeSymbolSchema, async (input) => analyzeSymbolService.execute(input)));
478
478
  registerToolSchema("analyze-symbol", analyzeSymbolSchema);
@@ -542,15 +542,14 @@ async function resolveFlatArtifactId(input) {
542
542
  if (target?.kind === "artifact" && target.artifactId) {
543
543
  return target.artifactId;
544
544
  }
545
+ const projectPath = input.projectPath ?? target?.projectPath;
545
546
  const resolved = await sourceService.resolveArtifact({
546
547
  target: target,
547
- ...(target && "projectPath" in target && target.projectPath
548
- ? { projectPath: target.projectPath }
549
- : {})
548
+ ...(projectPath ? { projectPath } : {})
550
549
  });
551
550
  return resolved.artifactId;
552
551
  }
553
- expertTool("find-class", "Resolve a simple or qualified class name to fully-qualified class names within an artifact. Use this before get-class-source when you only have a simple name.", findClassShape, { readOnlyHint: true }, async (args) => runTool("find-class", args, findClassSchema, async (input) => sourceService.findClass({
552
+ expertTool("find-class", "Resolve a simple or qualified class name to fully-qualified class names within an artifact. Use this before get-class-source when you only have a simple name.", findClassShape, { readOnlyHint: true }, async (args) => runTool("find-class", args, findClassSchema, async (input) => sourceService.findClassIncludingNested({
554
553
  className: input.className,
555
554
  artifactId: await resolveFlatArtifactId(input),
556
555
  limit: input.limit
@@ -59,5 +59,6 @@ export declare function buildDecompiledFallback(svc: SourceService, artifactId:
59
59
  truncated: boolean;
60
60
  } | undefined;
61
61
  export declare function findClass(svc: SourceService, input: FindClassInput): FindClassOutput;
62
+ export declare function findClassIncludingNested(svc: SourceService, input: FindClassInput): Promise<FindClassOutput>;
62
63
  export declare function getClassSource(svc: SourceService, input: GetClassSourceInput): Promise<GetClassSourceOutput>;
63
64
  export declare function getClassMembers(svc: SourceService, input: GetClassMembersInput): Promise<GetClassMembersOutput>;
@@ -8,7 +8,7 @@ import { buildClassSourceSnippet } from "./class-source/snippet-builder.js";
8
8
  import { remapAndCountMembers, sliceMembersWithLimit, projectMembersForWire, projectMembersByLevel } from "./class-source/members-builder.js";
9
9
  import { collectDidYouMeanCandidates } from "./did-you-mean.js";
10
10
  import { matchesMemberPattern } from "./member-pattern.js";
11
- import { resolveUniqueNestedJarForClass } from "./nested-jars.js";
11
+ import { findNestedJarClasses, resolveUniqueNestedJarForClass } from "./nested-jars.js";
12
12
  import { buildPageContextKey, encodeOffsetCursor, resolveCursorOffset } from "../page-cursor.js";
13
13
  import { dedupeQualityFlags, normalizeMapping, normalizeOptionalString, normalizePathStyle } from "./shared-utils.js";
14
14
  import { isUnobfuscatedVersion } from "../version-service.js";
@@ -82,6 +82,14 @@ function obfuscatedNamespaceHint(className) {
82
82
  function hasPartialNetMinecraftCoverage(qualityFlags) {
83
83
  return qualityFlags.includes("partial-source-no-net-minecraft");
84
84
  }
85
+ function shouldSuggestObfuscatedMapping(artifact, className) {
86
+ const nativeDependency = artifact.provenance?.dependencyResolution != null;
87
+ const shellArtifact = artifact.qualityFlags.includes("shell-jar");
88
+ return (artifact.mappingApplied === "obfuscated" &&
89
+ !nativeDependency &&
90
+ !shellArtifact &&
91
+ looksLikeDeobfuscatedClassName(className));
92
+ }
85
93
  function classNameToClassPath(className) {
86
94
  const normalized = normalizePathStyle(className.trim()).replace(/\//g, ".");
87
95
  const segments = normalized.split(".").filter((segment) => segment.length > 0);
@@ -398,7 +406,7 @@ export function findClass(svc, input) {
398
406
  if (filteredMatches.length === 0 && partialVanillaLookup) {
399
407
  warnings.push(`Artifact source coverage is partial and excludes net.minecraft; returning non-vanilla matches for "${className}" would be misleading. Use get-class-source/get-class-members for binary fallback or get-class-api-matrix for mapped API inspection.`);
400
408
  }
401
- if (filteredMatches.length === 0 && artifact.mappingApplied === "obfuscated" && looksLikeDeobfuscatedClassName(className)) {
409
+ if (filteredMatches.length === 0 && shouldSuggestObfuscatedMapping(artifact, className)) {
402
410
  warnings.push(`No exact class symbol matched "${className}". ${obfuscatedNamespaceHint(className)}`);
403
411
  }
404
412
  return { matches: filteredMatches, total: filteredMatches.length, warnings };
@@ -432,11 +440,39 @@ export function findClass(svc, input) {
432
440
  if (filteredMatches.length === 0 && partialVanillaLookup) {
433
441
  warnings.push(`Artifact source coverage is partial and excludes net.minecraft; returning non-vanilla matches for "${className}" would be misleading. Use get-class-source/get-class-members for binary fallback or get-class-api-matrix for mapped API inspection.`);
434
442
  }
435
- if (filteredMatches.length === 0 && artifact.mappingApplied === "obfuscated" && looksLikeDeobfuscatedClassName(className)) {
443
+ if (filteredMatches.length === 0 && shouldSuggestObfuscatedMapping(artifact, className)) {
436
444
  warnings.push(`No exact class symbol matched "${className}". ${obfuscatedNamespaceHint(className)}`);
437
445
  }
438
446
  return { matches: filteredMatches, total: filteredMatches.length, warnings };
439
447
  }
448
+ export async function findClassIncludingNested(svc, input) {
449
+ const indexed = findClass(svc, input);
450
+ if (indexed.total > 0) {
451
+ return indexed;
452
+ }
453
+ const artifact = svc.getArtifact(input.artifactId.trim());
454
+ const inventory = artifact.provenance?.nestedJars;
455
+ if (!artifact.qualityFlags.includes("shell-jar") ||
456
+ !artifact.binaryJarPath ||
457
+ !inventory ||
458
+ inventory.length === 0) {
459
+ return indexed;
460
+ }
461
+ const limit = Math.max(1, Math.min(input.limit ?? 20, 200));
462
+ const matches = await findNestedJarClasses({
463
+ cacheDir: svc.config.cacheDir,
464
+ outerJarPath: artifact.binaryJarPath,
465
+ outerSignature: artifact.artifactId,
466
+ inventory,
467
+ className: input.className,
468
+ limit
469
+ });
470
+ return {
471
+ matches,
472
+ total: matches.length,
473
+ warnings: indexed.warnings
474
+ };
475
+ }
440
476
  export async function getClassSource(svc, input) {
441
477
  const className = input.className.trim();
442
478
  if (!className) {
@@ -9,6 +9,25 @@ export interface NestedJarMatch {
9
9
  entryName: string;
10
10
  extractedPath: string;
11
11
  }
12
+ export interface NestedClassMatch {
13
+ qualifiedName: string;
14
+ filePath: string;
15
+ line: number;
16
+ symbolKind: "class";
17
+ }
18
+ /**
19
+ * Finds exact class-name matches across a shell's nested bytecode inventory.
20
+ * Results use the same binary-backed class representation as entry-tool search:
21
+ * an inferred outer Java path, line 1, and the broad class symbol kind.
22
+ */
23
+ export declare function findNestedJarClasses(args: {
24
+ cacheDir: string;
25
+ outerJarPath: string;
26
+ outerSignature: string;
27
+ inventory: string[];
28
+ className: string;
29
+ limit: number;
30
+ }): Promise<NestedClassMatch[]>;
12
31
  /**
13
32
  * Shell detection with both required signals: near-zero own classes AND
14
33
  * bundled nested jars (META-INF/jars scan or fabric.mod.json "jars"
@@ -4,6 +4,7 @@ import { dirname, join } from "node:path";
4
4
  import { buildSuggestedCall } from "../build-suggested-call.js";
5
5
  import { ERROR_CODES, createError } from "../errors.js";
6
6
  import { collectNestedJars } from "../mod-analyzer.js";
7
+ import { isSecureJarEntryPath } from "../path-resolver.js";
7
8
  import { listJarEntries, readJarEntryAsBuffer, readJarEntryAsUtf8 } from "../source-jar-reader.js";
8
9
  /**
9
10
  * A jar with at most this many own `.class` entries can qualify as a
@@ -29,6 +30,84 @@ function rememberClassSet(key, value) {
29
30
  classSetCache.delete(oldest);
30
31
  }
31
32
  }
33
+ async function loadNestedJarClassSet(args) {
34
+ let extractedPath;
35
+ try {
36
+ extractedPath = await extractNestedJar(args.cacheDir, args.outerJarPath, args.outerSignature, args.entryName);
37
+ }
38
+ catch {
39
+ return undefined;
40
+ }
41
+ let classSet = classSetCache.get(extractedPath);
42
+ if (!classSet) {
43
+ try {
44
+ const entries = await listJarEntries(extractedPath);
45
+ classSet = new Set(entries.filter((entry) => entry.endsWith(".class") && isSecureJarEntryPath(entry)));
46
+ }
47
+ catch {
48
+ return undefined;
49
+ }
50
+ rememberClassSet(extractedPath, classSet);
51
+ }
52
+ return { extractedPath, classSet };
53
+ }
54
+ function nestedClassMatch(entry) {
55
+ const internalName = entry.slice(0, -".class".length);
56
+ if (internalName.startsWith("META-INF/versions/")) {
57
+ return undefined;
58
+ }
59
+ const binarySimpleName = internalName.split("/").at(-1) ?? internalName;
60
+ if (binarySimpleName === "module-info" || binarySimpleName === "package-info") {
61
+ return undefined;
62
+ }
63
+ const innerSegments = binarySimpleName.split("$").slice(1);
64
+ if (innerSegments.some((segment) => segment.length === 0 || /^\d/.test(segment))) {
65
+ return undefined;
66
+ }
67
+ return {
68
+ qualifiedName: internalName.replaceAll("/", ".").replaceAll("$", "."),
69
+ filePath: `${internalName.split("$")[0]}.java`,
70
+ line: 1,
71
+ symbolKind: "class"
72
+ };
73
+ }
74
+ /**
75
+ * Finds exact class-name matches across a shell's nested bytecode inventory.
76
+ * Results use the same binary-backed class representation as entry-tool search:
77
+ * an inferred outer Java path, line 1, and the broad class symbol kind.
78
+ */
79
+ export async function findNestedJarClasses(args) {
80
+ const normalizedQuery = args.className.trim().replaceAll("/", ".").replaceAll("$", ".");
81
+ const isQualified = normalizedQuery.includes(".");
82
+ const matches = new Map();
83
+ inventory: for (const entryName of args.inventory) {
84
+ const loaded = await loadNestedJarClassSet({
85
+ cacheDir: args.cacheDir,
86
+ outerJarPath: args.outerJarPath,
87
+ outerSignature: args.outerSignature,
88
+ entryName
89
+ });
90
+ if (!loaded) {
91
+ continue;
92
+ }
93
+ for (const classEntry of [...loaded.classSet].sort((left, right) => left.localeCompare(right))) {
94
+ const match = nestedClassMatch(classEntry);
95
+ if (!match) {
96
+ continue;
97
+ }
98
+ const simpleName = match.qualifiedName.split(".").at(-1) ?? match.qualifiedName;
99
+ if ((isQualified && match.qualifiedName !== normalizedQuery) ||
100
+ (!isQualified && simpleName !== normalizedQuery)) {
101
+ continue;
102
+ }
103
+ matches.set(match.qualifiedName, match);
104
+ if (isQualified || matches.size >= args.limit) {
105
+ break inventory;
106
+ }
107
+ }
108
+ }
109
+ return [...matches.values()];
110
+ }
32
111
  /**
33
112
  * Shell detection with both required signals: near-zero own classes AND
34
113
  * bundled nested jars (META-INF/jars scan or fabric.mod.json "jars"
@@ -128,29 +207,19 @@ export async function extractNestedJar(cacheDir, outerJarPath, outerSignature, e
128
207
  */
129
208
  export async function findNestedJarsContainingClass(args) {
130
209
  const classEntry = `${args.internalName}.class`;
210
+ const qualifiedName = args.internalName.replaceAll("/", ".").replaceAll("$", ".");
131
211
  const matches = [];
132
212
  for (const entryName of args.inventory) {
133
- let extractedPath;
134
- try {
135
- extractedPath = await extractNestedJar(args.cacheDir, args.outerJarPath, args.outerSignature, entryName);
136
- }
137
- catch {
138
- // Unsafe or unreadable nested entries never become candidates.
139
- continue;
140
- }
141
- let classSet = classSetCache.get(extractedPath);
142
- if (!classSet) {
143
- try {
144
- const entries = await listJarEntries(extractedPath);
145
- classSet = new Set(entries.filter((entry) => entry.endsWith(".class")));
146
- }
147
- catch {
148
- continue;
149
- }
150
- rememberClassSet(extractedPath, classSet);
151
- }
152
- if (classSet.has(classEntry)) {
153
- matches.push({ entryName, extractedPath });
213
+ const loaded = await loadNestedJarClassSet({
214
+ cacheDir: args.cacheDir,
215
+ outerJarPath: args.outerJarPath,
216
+ outerSignature: args.outerSignature,
217
+ entryName
218
+ });
219
+ const containsClass = loaded?.classSet.has(classEntry) || [...(loaded?.classSet ?? [])].some((entry) => entry.endsWith(".class") &&
220
+ entry.slice(0, -".class".length).replaceAll("/", ".").replaceAll("$", ".") === qualifiedName);
221
+ if (loaded && containsClass) {
222
+ matches.push({ entryName, extractedPath: loaded.extractedPath });
154
223
  }
155
224
  }
156
225
  return matches;
@@ -670,6 +670,7 @@ export declare class SourceService {
670
670
  traceSymbolLifecycle(input: TraceSymbolLifecycleInput): Promise<TraceSymbolLifecycleOutput>;
671
671
  diffClassSignatures(input: DiffClassSignaturesInput): Promise<DiffClassSignaturesOutput>;
672
672
  findClass(input: FindClassInput): FindClassOutput;
673
+ findClassIncludingNested(input: FindClassInput): Promise<FindClassOutput>;
673
674
  getClassSource(input: GetClassSourceInput): Promise<GetClassSourceOutput>;
674
675
  getClassMembers(input: GetClassMembersInput): Promise<GetClassMembersOutput>;
675
676
  validateMixin(input: ValidateMixinInput, options?: ValidateMixinOptions): Promise<ValidateMixinOutput>;
@@ -215,6 +215,9 @@ export class SourceService {
215
215
  findClass(input) {
216
216
  return classSource.findClass(this, input);
217
217
  }
218
+ async findClassIncludingNested(input) {
219
+ return classSource.findClassIncludingNested(this, input);
220
+ }
218
221
  async getClassSource(input) {
219
222
  return classSource.getClassSource(this, input);
220
223
  }
@@ -32,7 +32,7 @@ const SECTION_ROWS = {
32
32
  en: [
33
33
  "| `list-versions` | List available Minecraft versions from Mojang metadata and local cache |",
34
34
  "| `resolve-artifact` | Resolve source artifacts from versions, JAR paths, or Maven coordinates |",
35
- "| `find-class` | Find simple or fully-qualified class names inside an artifact |",
35
+ "| `find-class` | Find simple or fully-qualified class names, including classes bundled in nested JARs |",
36
36
  "| `get-class-source` | Read class source from an artifact or resolve the backing artifact on demand |",
37
37
  "| `get-class-members` | List constructors, fields, and methods from bytecode |",
38
38
  "| `search-class-source` | Search indexed class source by symbol, text, or path |",
@@ -656,7 +656,103 @@ function gatedGuidance(tool, hints, params) {
656
656
  primaryDropped: !validated.suggestedCall
657
657
  };
658
658
  }
659
+ function buildInspectWorkspaceSubject(source) {
660
+ const subject = {
661
+ kind: "workspace",
662
+ projectPath: asNonEmptyString(source.projectPath) ?? "<workspace-path>"
663
+ };
664
+ if (typeof source.mapping === "string" &&
665
+ ["obfuscated", "mojang", "intermediary", "yarn"].includes(source.mapping)) {
666
+ subject.mapping = source.mapping;
667
+ }
668
+ if (typeof source.scope === "string" &&
669
+ ["vanilla", "merged", "loader"].includes(source.scope)) {
670
+ subject.scope = source.scope;
671
+ }
672
+ const gradleUserHome = asNonEmptyString(source.gradleUserHome);
673
+ if (gradleUserHome) {
674
+ subject.gradleUserHome = gradleUserHome;
675
+ }
676
+ if (typeof source.preferProjectVersion === "boolean") {
677
+ subject.preferProjectVersion = source.preferProjectVersion;
678
+ }
679
+ if (typeof source.strictVersion === "boolean") {
680
+ subject.strictVersion = source.strictVersion;
681
+ }
682
+ return subject;
683
+ }
684
+ function inspectTaskForFocus(task, focusKind) {
685
+ if (focusKind === "class" &&
686
+ (task === "auto" || task === "class-overview" || task === "class-source" || task === "class-members")) {
687
+ return task;
688
+ }
689
+ if (focusKind === "search" && (task === "auto" || task === "search")) {
690
+ return task;
691
+ }
692
+ if (focusKind === "file" && (task === "auto" || task === "file")) {
693
+ return task;
694
+ }
695
+ return "auto";
696
+ }
697
+ function buildInspectMinecraftInvalidFocusGuidance(normalizedInput) {
698
+ const input = asObjectRecord(normalizedInput);
699
+ const originalSubject = asObjectRecord(input?.subject);
700
+ if (originalSubject?.kind !== "workspace" ||
701
+ originalSubject.focus === undefined ||
702
+ asObjectRecord(originalSubject.focus)) {
703
+ return undefined;
704
+ }
705
+ const requestedTask = asNonEmptyString(input?.task);
706
+ const workspaceSubject = buildInspectWorkspaceSubject(originalSubject);
707
+ const examples = [
708
+ {
709
+ params: {
710
+ task: inspectTaskForFocus(requestedTask, "class"),
711
+ subject: {
712
+ ...workspaceSubject,
713
+ focus: { kind: "class", className: "<fully-qualified-class-name>" }
714
+ }
715
+ },
716
+ reason: "Use class focus for class overview, source, or member inspection."
717
+ },
718
+ {
719
+ params: {
720
+ task: inspectTaskForFocus(requestedTask, "search"),
721
+ subject: {
722
+ ...workspaceSubject,
723
+ focus: { kind: "search", query: "<search-query>" }
724
+ }
725
+ },
726
+ reason: "Use search focus for symbol, text, or path search."
727
+ },
728
+ {
729
+ params: {
730
+ task: inspectTaskForFocus(requestedTask, "file"),
731
+ subject: {
732
+ ...workspaceSubject,
733
+ focus: { kind: "file", filePath: "<artifact-relative-file-path>" }
734
+ }
735
+ },
736
+ reason: "Use file focus for an artifact-relative file read."
737
+ }
738
+ ];
739
+ const validated = buildSuggestedCall({
740
+ tool: "inspect-minecraft",
741
+ params: undefined,
742
+ examples
743
+ });
744
+ return {
745
+ hints: [
746
+ "inspect-minecraft subject.focus must be a structured object, not a string.",
747
+ "Choose focus.kind=class with className, search with query, or file with filePath; task=auto dispatches from that kind and does not interpret prose."
748
+ ],
749
+ ...(validated.exampleCalls ? { exampleCalls: validated.exampleCalls } : {})
750
+ };
751
+ }
659
752
  export function buildInvalidInputGuidance(tool, normalizedInput) {
753
+ if (tool === "inspect-minecraft") {
754
+ return buildInspectMinecraftInvalidFocusGuidance(normalizedInput);
755
+ }
660
756
  if (tool === "validate-mixin") {
661
757
  return gatedGuidance(tool, [
662
758
  "validate-mixin.input must be an object with input.mode = \"inline\" | \"path\" | \"paths\" | \"config\" | \"project\".",
@@ -719,6 +815,13 @@ export function mapErrorToProblem(caughtError, requestId, context) {
719
815
  }
720
816
  if (isAppError(caughtError)) {
721
817
  const { suggestedCall, exampleCalls, primaryDropped } = extractValidatedSuggestionAndExamples(caughtError.details);
818
+ const invalidInputGuidance = context?.tool === "inspect-minecraft" && caughtError.code === ERROR_CODES.INVALID_INPUT
819
+ ? buildInvalidInputGuidance(context.tool, context.normalizedInput)
820
+ : undefined;
821
+ const effectiveSuggestedCall = invalidInputGuidance
822
+ ? invalidInputGuidance.suggestedCall
823
+ : suggestedCall;
824
+ const effectiveExampleCalls = invalidInputGuidance?.exampleCalls ?? exampleCalls;
722
825
  const sanitizedContext = extractAllowlistedContext(caughtError.details);
723
826
  const extractedDidYouMean = extractDidYouMean(caughtError.details);
724
827
  let failedStage = extractFailedStageFromDetails(caughtError.details);
@@ -727,10 +830,13 @@ export function mapErrorToProblem(caughtError, requestId, context) {
727
830
  && caughtError.code === ERROR_CODES.INVALID_INPUT) {
728
831
  failedStage = "input-validation";
729
832
  }
730
- const baseHints = toHints(caughtError.details);
731
- const hintsWithFallback = primaryDropped && !suggestedCall
732
- ? [...(baseHints ?? []), VALIDATION_FALLBACK_HINT]
733
- : baseHints;
833
+ const baseHints = [
834
+ ...(toHints(caughtError.details) ?? []),
835
+ ...(invalidInputGuidance?.hints ?? [])
836
+ ];
837
+ const hintsWithFallback = primaryDropped && !effectiveSuggestedCall
838
+ ? [...baseHints, VALIDATION_FALLBACK_HINT]
839
+ : baseHints.length > 0 ? baseHints : undefined;
734
840
  return {
735
841
  type: `https://minecraft-modding-mcp.dev/problems/${caughtError.code.toLowerCase()}`,
736
842
  title: "Tool execution error",
@@ -742,8 +848,8 @@ export function mapErrorToProblem(caughtError, requestId, context) {
742
848
  issueOrigin: issueOriginForErrorCode(caughtError.code),
743
849
  fieldErrors: extractFieldErrorsFromDetails(caughtError.details),
744
850
  hints: hintsWithFallback,
745
- ...(suggestedCall ? { suggestedCall } : {}),
746
- ...(exampleCalls ? { exampleCalls } : {}),
851
+ ...(effectiveSuggestedCall ? { suggestedCall: effectiveSuggestedCall } : {}),
852
+ ...(effectiveExampleCalls ? { exampleCalls: effectiveExampleCalls } : {}),
747
853
  ...(extractedDidYouMean ? { didYouMean: extractedDidYouMean } : {}),
748
854
  ...(failedStage ? { failedStage } : {}),
749
855
  ...(sanitizedContext ? { context: sanitizedContext } : {})
@@ -2529,6 +2529,7 @@ export declare const findClassShape: {
2529
2529
  kind: "artifact";
2530
2530
  artifactId: string;
2531
2531
  }>]>>;
2532
+ projectPath: z.ZodOptional<z.ZodString>;
2532
2533
  limit: z.ZodOptional<z.ZodNumber>;
2533
2534
  };
2534
2535
  export declare const findClassSchema: z.ZodEffects<z.ZodObject<{
@@ -2601,9 +2602,11 @@ export declare const findClassSchema: z.ZodEffects<z.ZodObject<{
2601
2602
  kind: "artifact";
2602
2603
  artifactId: string;
2603
2604
  }>]>>;
2605
+ projectPath: z.ZodOptional<z.ZodString>;
2604
2606
  limit: z.ZodOptional<z.ZodNumber>;
2605
2607
  }, "strip", z.ZodTypeAny, {
2606
2608
  className: string;
2609
+ projectPath?: string | undefined;
2607
2610
  artifactId?: string | undefined;
2608
2611
  limit?: number | undefined;
2609
2612
  target?: {
@@ -2631,6 +2634,7 @@ export declare const findClassSchema: z.ZodEffects<z.ZodObject<{
2631
2634
  } | undefined;
2632
2635
  }, {
2633
2636
  className: string;
2637
+ projectPath?: string | undefined;
2634
2638
  artifactId?: string | undefined;
2635
2639
  limit?: number | undefined;
2636
2640
  target?: {
@@ -2658,6 +2662,7 @@ export declare const findClassSchema: z.ZodEffects<z.ZodObject<{
2658
2662
  } | undefined;
2659
2663
  }>, {
2660
2664
  className: string;
2665
+ projectPath?: string | undefined;
2661
2666
  artifactId?: string | undefined;
2662
2667
  limit?: number | undefined;
2663
2668
  target?: {
@@ -2685,6 +2690,7 @@ export declare const findClassSchema: z.ZodEffects<z.ZodObject<{
2685
2690
  } | undefined;
2686
2691
  }, {
2687
2692
  className: string;
2693
+ projectPath?: string | undefined;
2688
2694
  artifactId?: string | undefined;
2689
2695
  limit?: number | undefined;
2690
2696
  target?: {
@@ -3596,8 +3602,8 @@ export declare const traceSymbolLifecycleSchema: z.ZodObject<{
3596
3602
  descriptor?: string | undefined;
3597
3603
  mapping?: "obfuscated" | "mojang" | "intermediary" | "yarn" | undefined;
3598
3604
  sourcePriority?: "loom-first" | "maven-first" | undefined;
3599
- maxVersions?: number | undefined;
3600
3605
  gradleUserHome?: string | undefined;
3606
+ maxVersions?: number | undefined;
3601
3607
  includeSnapshots?: boolean | undefined;
3602
3608
  fromVersion?: string | undefined;
3603
3609
  toVersion?: string | undefined;
@@ -4053,10 +4059,10 @@ export declare const nbtToJsonSchema: z.ZodObject<{
4053
4059
  compression: z.ZodDefault<z.ZodEnum<["none", "gzip", "auto"]>>;
4054
4060
  }, "strip", z.ZodTypeAny, {
4055
4061
  nbtBase64: string;
4056
- compression: "none" | "gzip" | "auto";
4062
+ compression: "auto" | "none" | "gzip";
4057
4063
  }, {
4058
4064
  nbtBase64: string;
4059
- compression?: "none" | "gzip" | "auto" | undefined;
4065
+ compression?: "auto" | "none" | "gzip" | undefined;
4060
4066
  }>;
4061
4067
  export declare const nbtApplyJsonPatchShape: {
4062
4068
  typedJson: z.ZodUnknown;
@@ -371,6 +371,7 @@ export const findClassShape = {
371
371
  className: nonEmptyString.describe("Simple name (e.g. Blocks) or fully-qualified name (e.g. net.minecraft.world.level.block.Blocks)"),
372
372
  artifactId: optionalNonEmptyString,
373
373
  target: sourceLookupTargetSchema.optional().describe(SOURCE_LOOKUP_TARGET_DESCRIPTION),
374
+ projectPath: optionalNonEmptyString.describe("Workspace root for dependency or workspace target resolution."),
374
375
  limit: optionalPositiveInt.describe("default 20, max 200")
375
376
  };
376
377
  export const findClassSchema = z.object(findClassShape).superRefine(requireExactlyOneArtifactRef);
package/docs/README-ja.md CHANGED
@@ -405,12 +405,16 @@ pnpm test
405
405
 
406
406
  必要に応じて実行:
407
407
 
408
+ - `pnpm test:file <path...>`: 通常テストをファイル指定で絞り込む場合
409
+ - `pnpm test:grep <name-pattern>`: 通常テストを再帰的に集め、テスト名で絞り込む場合
408
410
  - `pnpm test:manual:stdio-smoke`: MCP トランスポート、登録、手動ワークフローの変更時
409
411
  - `pnpm test:manual:package-smoke`: パッケージインストールや配布形態の検証時
410
412
  - `pnpm test:perf`: 検索、インデックス、性能に影響する変更時
411
413
  - `pnpm test:coverage` または `pnpm test:coverage:lcov`: カバレッジ確認時(`lines=80`, `branches=70`, `functions=80`)
412
414
  - `pnpm validate`: ローカルの完全検証スイートを実行する場合
413
415
 
416
+ 通常の `.test.ts` ファイルは `tests/` 配下のドメイン別ディレクトリ(例: `source-service/`, `entry-tools/`, `mapping/`, `mixin/`, `integration/mcp-tools/`, `contracts/`, `utils/`)に置きます。ヘルパー専用モジュールは `tests/helpers` に残し、手動 smoke、性能、リソース、smoke 専用ファイルは既存の専用ディレクトリに残します。
417
+
414
418
  ## ライセンス
415
419
 
416
420
  [MIT](../LICENSE)
@@ -32,12 +32,13 @@ Start here when you are not sure which tool to reach for. In every row, the left
32
32
  - Start with the top-level workflow tools when possible. `inspect-minecraft`, `analyze-symbol`, `compare-minecraft`, `analyze-mod`, `validate-project`, and `manage-cache` cover the common workflows and return summary-first results with follow-up hints.
33
33
  - `resolve-artifact` uses `target: { kind, value }`. `kind` is one of `"version"`, `"jar"`, `"coordinate"`, `"workspace"`, or `"dependency"` (see "Workspace and dependency target shapes" below).
34
34
  - `get-class-source` and `get-class-members` use `target: { kind, value }` — the same `kind`-based shape as `resolve-artifact` (`"version"`, `"jar"`, `"coordinate"`, `"workspace"`, `"dependency"`), plus `target: { kind: "artifact", artifactId }` to reuse an already-resolved artifact.
35
- - `find-class`, `search-class-source`, `list-artifact-files`, and `find-mapping` keep their existing input shapes (an `artifactId` or a `version`); they do not currently accept `target.kind="workspace"` or `target.kind="dependency"`.
35
+ - `find-class`, `search-class-source`, `get-artifact-file`, `list-artifact-files`, and `index-artifact` accept either an `artifactId` or the shared object `target` shape. `find-class` also accepts top-level `projectPath`, which supplies the workspace context required by `target.kind="workspace"` and dependency targets using `versionFromProject`. For another flat tool whose target needs workspace context, resolve the artifact first and pass its `artifactId`.
36
36
  - `validate-mixin` and `validate-project task="mixin"` use `input.mode="inline" | "path" | "paths" | "config" | "project"`.
37
37
  - Positive integer tool arguments accept numeric strings such as `"10"` for documented top-level parameters.
38
38
  - When a parameter has a fixed safe default, `tools/list` exposes it through the JSON Schema `default` field so clients can rely on schema metadata instead of prose notes.
39
39
  - Retryable `suggestedCall` payloads omit parameters when the supplied value already matches the tool default, keeping recovery calls smaller without changing behavior.
40
40
  - `ERR_CLASS_NOT_FOUND` errors from the class tools carry a top-level `didYouMean` array (parallel to `suggestedCall`) with ranked near-miss candidates from the artifact's symbol index: each entry is `{ className, matchReason }` where `matchReason` is `"exact-simple-name"` (same simple name in another package — the moved-class case, ranked first), `"case-insensitive"`, or `"edit-distance:N"`. Candidates are hints, never assertions that the class exists at the suggested location; the array is empty when the index has nothing usable. All same-simple-name FQNs are enumerated rather than collapsed.
41
+ - `find-class` searches the nested `.class` inventories of Jar-in-Jar shell artifacts such as the Fabric API umbrella JAR. It accepts simple or qualified names, returns dotted names for inner classes, deduplicates a class bundled more than once, and honors `limit`. The returned source path is inferred from the outer class. For top-level matches, `get-class-source` and `get-class-members` resolve the actual containing nested JAR before reading content; dotted inner-class matches are also readable through `get-class-source`.
41
42
  - Source-oriented tools expose `artifactContents` so callers can tell whether the backing artifact is a `source-jar` or a `decompiled-binary`. `get-class-source`, `get-class-members`, `search-class-source`, and `get-artifact-file` also expose `returnedNamespace`.
42
43
  - Cache-backed source, mapping, validation, batch, and workflow tools accept `gradleUserHome?: string` when they need Loom cache data. Use it for builds that used an isolated `GRADLE_USER_HOME`; the server searches `<gradleUserHome>/loom-cache` and `<gradleUserHome>/caches/fabric-loom` before the MCP process default. The value selects a Gradle User Home, not arbitrary Loom cache roots.
43
44
  - `get-class-members` returns `decompiledFallback` (with `constructors`, `fields`, `methods`, each entry is `{ name, line, kind }`) and `decompiledMemberCounts` whenever bytecode enumeration yields zero but the decompiled source for the class is already indexed. The bytecode-derived `members` / `counts` are preserved as-is; the fallback is additive and carries no descriptor or access modifier. `qualityFlags` gains `"members-from-decompiled-source"` in that case. Use `get-class-source` for descriptors and full context.
@@ -59,6 +60,75 @@ Start here when you are not sure which tool to reach for. In every row, the left
59
60
  - Heavy analysis tools are serialized in-process to protect stdio stability. Queue overflow returns `ERR_LIMIT_EXCEEDED`.
60
61
  - All tools and JSON resources use the standard `{ result?, error?, meta }` envelope. `class-source` and `artifact-file` resources return raw text on success and structured JSON on failure.
61
62
 
63
+ ## inspect-minecraft workspace focus
64
+
65
+ Use `subject.kind="workspace"` when `inspect-minecraft` should resolve Minecraft artifact context from a project. Its `focus` is a structured object, not a string:
66
+
67
+ | Focus shape | `task="auto"` dispatch | Explicit tasks |
68
+ | --- | --- | --- |
69
+ | `focus: { kind: "class", className: "..." }` | `class-overview` | `class-overview`, `class-source`, `class-members` |
70
+ | `focus: { kind: "search", query: "..." }` | `search` | `search` |
71
+ | `focus: { kind: "file", filePath: "..." }` | `file` | `file` |
72
+
73
+ `task="auto"` is structured dispatch based on `subject.kind` and `focus.kind`; it is not a natural-language planner and does not interpret prose. A string `focus` remains invalid and returns `ERR_INVALID_INPUT` with three schema-validated class/search/file `exampleCalls`; the server never guesses which object shape the text meant.
74
+
75
+ Class source from a workspace:
76
+
77
+ ```json
78
+ {
79
+ "tool": "inspect-minecraft",
80
+ "arguments": {
81
+ "task": "class-source",
82
+ "subject": {
83
+ "kind": "workspace",
84
+ "projectPath": "/path/to/workspace",
85
+ "focus": {
86
+ "kind": "class",
87
+ "className": "net.minecraft.world.item.Item"
88
+ }
89
+ }
90
+ }
91
+ }
92
+ ```
93
+
94
+ Source search from a workspace:
95
+
96
+ ```json
97
+ {
98
+ "tool": "inspect-minecraft",
99
+ "arguments": {
100
+ "task": "auto",
101
+ "subject": {
102
+ "kind": "workspace",
103
+ "projectPath": "/path/to/workspace",
104
+ "focus": {
105
+ "kind": "search",
106
+ "query": "CreativeModeTab"
107
+ }
108
+ }
109
+ }
110
+ }
111
+ ```
112
+
113
+ Artifact-relative file read from a workspace:
114
+
115
+ ```json
116
+ {
117
+ "tool": "inspect-minecraft",
118
+ "arguments": {
119
+ "task": "auto",
120
+ "subject": {
121
+ "kind": "workspace",
122
+ "projectPath": "/path/to/workspace",
123
+ "focus": {
124
+ "kind": "file",
125
+ "filePath": "net/minecraft/world/item/Item.java"
126
+ }
127
+ }
128
+ }
129
+ }
130
+ ```
131
+
62
132
  ## Workspace and dependency target shapes
63
133
 
64
134
  `resolve-artifact`, `get-class-source`, and `get-class-members` accept two synthesizing `target.kind` values in addition to the canonical `"version"` / `"jar"` / `"coordinate"` shapes. The synthesizer rewrites the call into one of those canonical shapes before downstream resolution, so behaviour from the resolver onward is unchanged.
@@ -288,7 +358,7 @@ These environment variables are read once at worker startup and provide rollback
288
358
  | Env | Effect | Acceptance test |
289
359
  |---|---|---|
290
360
  | `MIXIN_STAGE_BUDGETS_OFF=1` | Sets every `validate-mixin` stage budget (including the per-target soft cap) to `Number.POSITIVE_INFINITY`. Restores the pre-budget run-to-completion behaviour. | `tests/source-service-validate-mixin-budget.test.ts` (`MIXIN_STAGE_BUDGETS_OFF=1 disables all budgets`) |
291
- | `SUPERVISOR_STRUCTURED_RESTART_OFF=1` | Suppresses synthetic `CallToolResult` envelopes; `tools/call` requests killed by a worker exit fall back to the legacy raw JSON-RPC `-32603` error. | `tests/stdio-supervisor.test.ts` (`buildWorkerRestartReply returns raw -32603 when structuredRestartDisabled`) |
361
+ | `SUPERVISOR_STRUCTURED_RESTART_OFF=1` | Suppresses synthetic `CallToolResult` envelopes; `tools/call` requests killed by a worker exit fall back to the legacy raw JSON-RPC `-32603` error. | `tests/stdio/stdio-supervisor.test.ts` (`buildWorkerRestartReply returns raw -32603 when structuredRestartDisabled`) |
292
362
  | `MIXIN_STAGE_PROGRESS_OFF=1` | Replaces the worker stage emitter with a no-op so `$/stageUpdate` notifications are never sent. Use as a fallback when the active SDK build does not surface `extra.requestId`. | `tests/stage-emitter.test.ts` (`makeStageEmitter is a no-op when disabled option is true`) |
293
363
  | `WORKSPACE_TARGET_OFF=1` | Rejects `target.kind="workspace"` on `resolve-artifact`, `get-class-source`, and `get-class-members` with `ERR_INVALID_INPUT`. Restores the pre-workspace-target behaviour where callers must always supply `target.kind="version"`/`"jar"`/`"coordinate"`. | `tests/source-service-workspace-target.test.ts` (`synthesizeWorkspaceTarget rejects target.kind=workspace when WORKSPACE_TARGET_OFF is set`) |
294
364
  | `DEPENDENCY_TARGET_OFF=1` | Rejects `target.kind="dependency"` on the same three tools with `ERR_INVALID_INPUT`. | `tests/source-service-dependency-target.test.ts` (`synthesizeDependencyTarget rejects target.kind=dependency when DEPENDENCY_TARGET_OFF is set`) |
@@ -419,7 +489,7 @@ If callers accidentally append an inline signature suffix to `trace-symbol-lifec
419
489
 
420
490
  For decompile-only `ERR_MAPPING_NOT_APPLIED` failures, error details include `artifactOrigin`, `nextAction`, and `suggestedCall` so clients can recover without guessing.
421
491
 
422
- If `find-class` or `get-class-source` returns no hit on an `obfuscated` artifact for names like `net.minecraft.world.item.Item`, the tool warns that `obfuscated` means Mojang's runtime names and recommends retrying with `mapping="mojang"` or translating via `find-mapping`.
492
+ If `find-class` or `get-class-source` returns no hit on an obfuscated Minecraft runtime artifact for names like `net.minecraft.world.item.Item`, the tool warns that `obfuscated` means Mojang's runtime names and recommends retrying with `mapping="mojang"` or translating via `find-mapping`. `find-class` does not issue that advice for dependency-resolved or Jar-in-Jar shell artifacts, whose native names are not evidence of Minecraft obfuscation.
423
493
 
424
494
  Method descriptor precision is best on Tiny-backed paths (`intermediary` and `yarn`). For `obfuscated <-> mojang`, Mojang `client_mappings` do not carry JVM descriptors, so descriptor queries may fall back to name matching and emit a warning.
425
495
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhisang/minecraft-modding-mcp",
3
- "version": "6.2.0",
3
+ "version": "6.3.0",
4
4
  "description": "MCP server for AI-assisted Minecraft modding: inspect decompiled source, resolve Mojang/Yarn/Intermediary mappings, diff versions, analyze Fabric/Forge/NeoForge mod JARs, and validate Mixin, Access Widener, and Access Transformer files.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -23,11 +23,11 @@
23
23
  "prepack": "npm run build",
24
24
  "start": "node dist/cli.js",
25
25
  "check": "tsc --noEmit -p tsconfig.json",
26
- "test": "node --test --import tsx tests/*.test.ts",
26
+ "test": "node scripts/run-tests.mjs",
27
27
  "test:file": "node --test --import tsx",
28
- "test:grep": "node --test --import tsx --test-name-pattern",
29
- "test:coverage": "node --test --import tsx --experimental-test-coverage --test-coverage-lines=80 --test-coverage-branches=70 --test-coverage-functions=80 tests/*.test.ts",
30
- "test:coverage:lcov": "node --input-type=module -e \"import { mkdirSync } from 'node:fs'; mkdirSync('coverage', { recursive: true });\" && node --test --import tsx --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=coverage/lcov.info --test-coverage-lines=80 --test-coverage-branches=70 --test-coverage-functions=80 tests/*.test.ts",
28
+ "test:grep": "node scripts/run-tests.mjs --test-name-pattern",
29
+ "test:coverage": "node scripts/run-tests.mjs --coverage",
30
+ "test:coverage:lcov": "node scripts/run-tests.mjs --coverage --lcov coverage/lcov.info",
31
31
  "test:perf": "node --test --test-concurrency=1 --import tsx tests/perf/*.perf.ts",
32
32
  "test:perf:update-baseline": "UPDATE_PERF_BASELINE=1 node --test --test-concurrency=1 --import tsx tests/perf/*.perf.ts",
33
33
  "test:perf:strict": "STRICT_PERF=1 node --test --test-concurrency=1 --import tsx tests/perf/*.perf.ts",