@adhisang/minecraft-modding-mcp 6.1.0 → 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.
- package/CHANGELOG.md +37 -0
- package/README.md +4 -2
- package/dist/cache-registry.d.ts +1 -1
- package/dist/cache-registry.js +3 -0
- package/dist/entry-tools/analyze-mod-service.d.ts +12 -6
- package/dist/entry-tools/analyze-mod-service.js +37 -3
- package/dist/entry-tools/analyze-symbol-service.d.ts +2 -0
- package/dist/entry-tools/analyze-symbol-service.js +37 -2
- package/dist/entry-tools/inspect-minecraft/internal.d.ts +4 -0
- package/dist/entry-tools/inspect-minecraft/internal.js +28 -0
- package/dist/entry-tools/manage-cache-service.d.ts +4 -4
- package/dist/error-mapping.d.ts +13 -0
- package/dist/error-mapping.js +35 -2
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +2 -0
- package/dist/index.js +37 -17
- package/dist/mapping/internal-types.d.ts +7 -0
- package/dist/mapping/types.d.ts +18 -0
- package/dist/mapping-service.js +16 -3
- package/dist/minecraft-explorer-service.d.ts +4 -0
- package/dist/minecraft-explorer-service.js +156 -17
- package/dist/mod-analyzer.d.ts +7 -0
- package/dist/mod-analyzer.js +28 -7
- package/dist/nbt/typed-json.js +4 -1
- package/dist/source/artifact-resolver.d.ts +2 -0
- package/dist/source/artifact-resolver.js +24 -1
- package/dist/source/class-source/members-builder.d.ts +4 -0
- package/dist/source/class-source/members-builder.js +3 -1
- package/dist/source/class-source.d.ts +2 -1
- package/dist/source/class-source.js +154 -17
- package/dist/source/did-you-mean.d.ts +14 -0
- package/dist/source/did-you-mean.js +79 -0
- package/dist/source/file-access.js +159 -3
- package/dist/source/indexer.js +72 -2
- package/dist/source/lifecycle/runtime-check.js +15 -7
- package/dist/source/nested-jars.d.ts +59 -0
- package/dist/source/nested-jars.js +198 -0
- package/dist/source/workspace-target.js +5 -2
- package/dist/source-jar-reader.d.ts +16 -0
- package/dist/source-jar-reader.js +82 -0
- package/dist/source-service.d.ts +36 -0
- package/dist/source-service.js +49 -6
- package/dist/stage-emitter.js +24 -8
- package/dist/stdio-supervisor.d.ts +92 -9
- package/dist/stdio-supervisor.js +915 -103
- package/dist/tool-contract-manifest.js +1 -1
- package/dist/tool-guidance.js +3 -1
- package/dist/tool-schemas.d.ts +1337 -149
- package/dist/tool-schemas.js +38 -7
- package/dist/types.d.ts +23 -0
- package/dist/workspace-mapping-service.d.ts +1 -0
- package/dist/workspace-mapping-service.js +120 -8
- package/docs/tool-reference.md +19 -3
- package/package.json +1 -1
package/dist/tool-schemas.js
CHANGED
|
@@ -161,6 +161,7 @@ export const getClassMembersShape = {
|
|
|
161
161
|
access: memberAccessSchema.default("public"),
|
|
162
162
|
includeSynthetic: z.boolean().default(false),
|
|
163
163
|
includeInherited: z.boolean().default(false),
|
|
164
|
+
includeAnnotations: z.boolean().default(false).describe("Opt-in: include runtime-visible member annotations (e.g. @Deprecated) on each member."),
|
|
164
165
|
memberPattern: optionalNonEmptyString.describe(MEMBER_PATTERN_DESCRIPTION),
|
|
165
166
|
projection: memberProjectionSchema.optional().describe(MEMBER_PROJECTION_DESCRIPTION),
|
|
166
167
|
maxMembers: optionalPositiveInt.describe("default 150, max 5000. Page beyond the first 150 with cursor."),
|
|
@@ -348,8 +349,34 @@ export const batchMappingsShape = {
|
|
|
348
349
|
entries: z.array(batchMappingsEntrySchema).min(1).max(50)
|
|
349
350
|
};
|
|
350
351
|
export const batchMappingsSchema = z.object(batchMappingsShape);
|
|
352
|
+
// Exactly one of artifactId/target addresses the artifact on flat tools.
|
|
353
|
+
// `target` is additive: existing flat-artifactId calls stay valid.
|
|
354
|
+
function requireExactlyOneArtifactRef(value, ctx) {
|
|
355
|
+
if (value.artifactId && value.target) {
|
|
356
|
+
ctx.addIssue({
|
|
357
|
+
code: z.ZodIssueCode.custom,
|
|
358
|
+
path: ["target"],
|
|
359
|
+
message: "artifactId and target are mutually exclusive."
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
if (!value.artifactId && !value.target) {
|
|
363
|
+
ctx.addIssue({
|
|
364
|
+
code: z.ZodIssueCode.custom,
|
|
365
|
+
path: ["artifactId"],
|
|
366
|
+
message: "Either artifactId or target must be provided."
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
export const findClassShape = {
|
|
371
|
+
className: nonEmptyString.describe("Simple name (e.g. Blocks) or fully-qualified name (e.g. net.minecraft.world.level.block.Blocks)"),
|
|
372
|
+
artifactId: optionalNonEmptyString,
|
|
373
|
+
target: sourceLookupTargetSchema.optional().describe(SOURCE_LOOKUP_TARGET_DESCRIPTION),
|
|
374
|
+
limit: optionalPositiveInt.describe("default 20, max 200")
|
|
375
|
+
};
|
|
376
|
+
export const findClassSchema = z.object(findClassShape).superRefine(requireExactlyOneArtifactRef);
|
|
351
377
|
export const searchClassSourceShape = {
|
|
352
|
-
artifactId:
|
|
378
|
+
artifactId: optionalNonEmptyString,
|
|
379
|
+
target: sourceLookupTargetSchema.optional().describe(SOURCE_LOOKUP_TARGET_DESCRIPTION),
|
|
353
380
|
query: nonEmptyString,
|
|
354
381
|
intent: searchIntentSchema.optional(),
|
|
355
382
|
match: searchMatchSchema.optional(),
|
|
@@ -366,6 +393,7 @@ export const searchClassSourceShape = {
|
|
|
366
393
|
include: responseIncludeParam
|
|
367
394
|
};
|
|
368
395
|
export const searchClassSourceSchema = z.object(searchClassSourceShape).superRefine((value, ctx) => {
|
|
396
|
+
requireExactlyOneArtifactRef(value, ctx);
|
|
369
397
|
if (value.symbolKind && value.intent && value.intent !== "symbol") {
|
|
370
398
|
ctx.addIssue({
|
|
371
399
|
code: z.ZodIssueCode.custom,
|
|
@@ -375,20 +403,22 @@ export const searchClassSourceSchema = z.object(searchClassSourceShape).superRef
|
|
|
375
403
|
}
|
|
376
404
|
});
|
|
377
405
|
export const getArtifactFileShape = {
|
|
378
|
-
artifactId:
|
|
406
|
+
artifactId: optionalNonEmptyString,
|
|
407
|
+
target: sourceLookupTargetSchema.optional().describe(SOURCE_LOOKUP_TARGET_DESCRIPTION),
|
|
379
408
|
filePath: nonEmptyString,
|
|
380
409
|
maxBytes: optionalPositiveInt
|
|
381
410
|
};
|
|
382
|
-
export const getArtifactFileSchema = z.object(getArtifactFileShape);
|
|
411
|
+
export const getArtifactFileSchema = z.object(getArtifactFileShape).superRefine(requireExactlyOneArtifactRef);
|
|
383
412
|
export const listArtifactFilesShape = {
|
|
384
|
-
artifactId:
|
|
413
|
+
artifactId: optionalNonEmptyString,
|
|
414
|
+
target: sourceLookupTargetSchema.optional().describe(SOURCE_LOOKUP_TARGET_DESCRIPTION),
|
|
385
415
|
prefix: optionalNonEmptyString,
|
|
386
416
|
limit: optionalPositiveInt,
|
|
387
417
|
cursor: optionalNonEmptyString,
|
|
388
418
|
detail: detailParam("standard"),
|
|
389
419
|
include: responseIncludeParam
|
|
390
420
|
};
|
|
391
|
-
export const listArtifactFilesSchema = z.object(listArtifactFilesShape);
|
|
421
|
+
export const listArtifactFilesSchema = z.object(listArtifactFilesShape).superRefine(requireExactlyOneArtifactRef);
|
|
392
422
|
export const traceSymbolLifecycleShape = {
|
|
393
423
|
symbol: nonEmptyString.describe("fully.qualified.Class.method"),
|
|
394
424
|
descriptor: optionalDescriptorString.describe('optional JVM descriptor, e.g. "(I)V". Empty strings are treated as omitted.'),
|
|
@@ -719,10 +749,11 @@ export const jsonToNbtShape = {
|
|
|
719
749
|
};
|
|
720
750
|
export const jsonToNbtSchema = z.object(jsonToNbtShape);
|
|
721
751
|
export const indexArtifactShape = {
|
|
722
|
-
artifactId:
|
|
752
|
+
artifactId: optionalNonEmptyString,
|
|
753
|
+
target: sourceLookupTargetSchema.optional().describe(SOURCE_LOOKUP_TARGET_DESCRIPTION),
|
|
723
754
|
force: z.boolean().default(false)
|
|
724
755
|
};
|
|
725
|
-
export const indexArtifactSchema = z.object(indexArtifactShape);
|
|
756
|
+
export const indexArtifactSchema = z.object(indexArtifactShape).superRefine(requireExactlyOneArtifactRef);
|
|
726
757
|
export const validateMixinShape = {
|
|
727
758
|
input: z.discriminatedUnion("mode", [
|
|
728
759
|
z.object({
|
package/dist/types.d.ts
CHANGED
|
@@ -60,6 +60,8 @@ export interface DependencyResolutionProvenance {
|
|
|
60
60
|
candidatesSeen?: string[];
|
|
61
61
|
attempts?: string[];
|
|
62
62
|
cacheHit: boolean;
|
|
63
|
+
/** Set when a submodule version was adopted from the cached umbrella POM. */
|
|
64
|
+
submoduleVersionSource?: "umbrella-pom";
|
|
63
65
|
}
|
|
64
66
|
export interface ArtifactProvenance {
|
|
65
67
|
target: SourceTargetInput;
|
|
@@ -76,6 +78,27 @@ export interface ArtifactProvenance {
|
|
|
76
78
|
workspaceResolution?: WorkspaceResolutionProvenance;
|
|
77
79
|
dependencyResolution?: DependencyResolutionProvenance;
|
|
78
80
|
warnings?: string[];
|
|
81
|
+
/**
|
|
82
|
+
* In-archive paths of bundled Jar-in-Jar nested jars, recorded once at
|
|
83
|
+
* ingest for shell jars (near-zero own classes). Class-family lookups
|
|
84
|
+
* redirect into these instead of dead-ending in decompilation.
|
|
85
|
+
*/
|
|
86
|
+
nestedJars?: string[];
|
|
87
|
+
/**
|
|
88
|
+
* Set on responses that were served by redirecting a class lookup into a
|
|
89
|
+
* nested jar of a shell artifact.
|
|
90
|
+
*/
|
|
91
|
+
nestedJar?: {
|
|
92
|
+
entryName: string;
|
|
93
|
+
shellArtifactId: string;
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Additional sources jars indexed into this artifact alongside
|
|
97
|
+
* resolvedFrom.sourceJarPath — the other half of a Loom split-source pair
|
|
98
|
+
* (minecraft-common / minecraft-clientOnly), so client-only classes are not
|
|
99
|
+
* lost to single-jar selection.
|
|
100
|
+
*/
|
|
101
|
+
companionSourceJars?: string[];
|
|
79
102
|
}
|
|
80
103
|
export interface RuntimeValidationProvenance<TMapping extends RuntimeValidationNamespace = RuntimeValidationNamespace> {
|
|
81
104
|
version: string;
|
|
@@ -59,21 +59,39 @@ function buildDependencyPropertyKeys(group, name) {
|
|
|
59
59
|
const camelName = camelCaseDependencyName(name);
|
|
60
60
|
const groupSegment = lastGroupSegment(group);
|
|
61
61
|
const camelGroupName = camelCaseDependencyName(`${groupSegment}_${name}`);
|
|
62
|
+
const snakeName = name.replace(/-/g, "_");
|
|
63
|
+
const snakeGroupSegment = groupSegment.replace(/-/g, "_");
|
|
64
|
+
// Hyphenated artifact names (e.g. fabric-api) are declared in
|
|
65
|
+
// gradle.properties as snake_case keys (fabric_api_version), so the base
|
|
66
|
+
// enumeration must probe the snake_case transforms too — including for the
|
|
67
|
+
// umbrella artifact itself, which never reaches the umbrella fallback below.
|
|
62
68
|
const keys = [
|
|
63
69
|
`${name}_version`,
|
|
70
|
+
`${snakeName}_version`,
|
|
64
71
|
`${camelName}Version`,
|
|
65
72
|
`${groupSegment}_${name}_version`,
|
|
73
|
+
`${snakeGroupSegment}_${snakeName}_version`,
|
|
66
74
|
`${camelGroupName}Version`
|
|
67
75
|
];
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
return dedupeKeys(keys);
|
|
77
|
+
}
|
|
78
|
+
// Umbrella properties (fabric_api_version / fabricApiVersion) declare the
|
|
79
|
+
// UMBRELLA package's version, not a submodule's. They are never adopted as a
|
|
80
|
+
// submodule version directly — the umbrella version differs from every
|
|
81
|
+
// submodule version (e.g. fabric-api 0.153.0+26.2 vs
|
|
82
|
+
// fabric-screen-handler-api-v1 2.0.5+...). They only locate the cached
|
|
83
|
+
// umbrella POM, which names the real per-submodule versions.
|
|
84
|
+
function buildUmbrellaPropertyKeys(group, name) {
|
|
85
|
+
const groupSegment = lastGroupSegment(group);
|
|
86
|
+
if (groupSegment === name) {
|
|
87
|
+
return [];
|
|
76
88
|
}
|
|
89
|
+
return dedupeKeys([
|
|
90
|
+
`${groupSegment.replace(/-/g, "_")}_version`,
|
|
91
|
+
`${camelCaseDependencyName(groupSegment)}Version`
|
|
92
|
+
]);
|
|
93
|
+
}
|
|
94
|
+
function dedupeKeys(keys) {
|
|
77
95
|
const seen = new Set();
|
|
78
96
|
const deduped = [];
|
|
79
97
|
for (const key of keys) {
|
|
@@ -84,6 +102,89 @@ function buildDependencyPropertyKeys(group, name) {
|
|
|
84
102
|
}
|
|
85
103
|
return deduped;
|
|
86
104
|
}
|
|
105
|
+
function readPomDependencyVersion(pomContent, group, name) {
|
|
106
|
+
const blocks = pomContent.match(/<dependency>[\s\S]*?<\/dependency>/g) ?? [];
|
|
107
|
+
for (const block of blocks) {
|
|
108
|
+
const groupId = block.match(/<groupId>\s*([^<]+?)\s*<\/groupId>/)?.[1];
|
|
109
|
+
const artifactId = block.match(/<artifactId>\s*([^<]+?)\s*<\/artifactId>/)?.[1];
|
|
110
|
+
if (groupId !== group || artifactId !== name) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
// A matching block without a literal <version> (e.g. a managed entry
|
|
114
|
+
// relying on inheritance) must not end the scan — a later block for the
|
|
115
|
+
// same artifact may carry the concrete version.
|
|
116
|
+
const version = block.match(/<version>\s*([^<]+?)\s*<\/version>/)?.[1];
|
|
117
|
+
if (version) {
|
|
118
|
+
return version;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
// When modules-2 caches several versions of a submodule, the cached umbrella
|
|
124
|
+
// POM (modules-2/files-2.1/<group>/<umbrella>/<version>/<hash>/<umbrella>-<version>.pom)
|
|
125
|
+
// is the only local evidence of which one the declared umbrella version maps
|
|
126
|
+
// to. Adoption is fail-closed: no umbrella property, no readable POM, no
|
|
127
|
+
// matching <dependency> entry, or a POM version absent from the cached
|
|
128
|
+
// candidates all leave the resolution unresolved instead of guessing.
|
|
129
|
+
async function adoptSubmoduleVersionFromUmbrellaPom(args) {
|
|
130
|
+
const { group, name, propsContent, candidates, candidatesSeen, attempts } = args;
|
|
131
|
+
const groupSegment = lastGroupSegment(group);
|
|
132
|
+
const umbrellaKeys = buildUmbrellaPropertyKeys(group, name);
|
|
133
|
+
if (umbrellaKeys.length === 0) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
let umbrellaVersion;
|
|
137
|
+
if (propsContent !== undefined) {
|
|
138
|
+
for (const key of umbrellaKeys) {
|
|
139
|
+
attempts.push(`umbrella-property:${key}`);
|
|
140
|
+
const value = readPropertyValue(propsContent, key);
|
|
141
|
+
if (value && isSafeMavenVersionToken(value)) {
|
|
142
|
+
umbrellaVersion = value;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (!umbrellaVersion) {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
const umbrellaDir = resolve(resolveGradleUserHome(), "caches", "modules-2", "files-2.1", group, groupSegment, umbrellaVersion);
|
|
151
|
+
attempts.push(`umbrella-pom:${umbrellaDir}`);
|
|
152
|
+
let hashDirs = [];
|
|
153
|
+
try {
|
|
154
|
+
hashDirs = await readdir(umbrellaDir);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
const pomFileName = `${groupSegment}-${umbrellaVersion}.pom`;
|
|
160
|
+
for (const hashDir of hashDirs) {
|
|
161
|
+
const pomPath = resolve(umbrellaDir, hashDir, pomFileName);
|
|
162
|
+
let pomContent;
|
|
163
|
+
try {
|
|
164
|
+
pomContent = await readFile(pomPath, "utf8");
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const version = readPomDependencyVersion(pomContent, group, name);
|
|
170
|
+
if (!version || !isSafeMavenVersionToken(version)) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (!candidates.includes(version)) {
|
|
174
|
+
attempts.push(`umbrella-pom:${pomPath}:names-uncached-version:${version}`);
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
resolved: true,
|
|
179
|
+
version,
|
|
180
|
+
source: `umbrella-pom:${pomPath}`,
|
|
181
|
+
candidatesSeen,
|
|
182
|
+
attempts,
|
|
183
|
+
submoduleVersionSource: "umbrella-pom"
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
87
188
|
function readPropertyValue(content, key) {
|
|
88
189
|
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
89
190
|
const pattern = new RegExp(`^\\s*${escaped}\\s*=\\s*(.+?)\\s*$`, "m");
|
|
@@ -326,6 +427,17 @@ export class WorkspaceMappingService {
|
|
|
326
427
|
return { resolved: false, candidatesSeen, attempts };
|
|
327
428
|
}
|
|
328
429
|
if (sorted.length > 1) {
|
|
430
|
+
const pomAdoption = await adoptSubmoduleVersionFromUmbrellaPom({
|
|
431
|
+
group,
|
|
432
|
+
name,
|
|
433
|
+
propsContent,
|
|
434
|
+
candidates: sorted,
|
|
435
|
+
candidatesSeen,
|
|
436
|
+
attempts
|
|
437
|
+
});
|
|
438
|
+
if (pomAdoption) {
|
|
439
|
+
return pomAdoption;
|
|
440
|
+
}
|
|
329
441
|
return { resolved: false, candidatesSeen, attempts };
|
|
330
442
|
}
|
|
331
443
|
const chosen = sorted[0];
|
package/docs/tool-reference.md
CHANGED
|
@@ -37,6 +37,7 @@ Start here when you are not sure which tool to reach for. In every row, the left
|
|
|
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
|
+
- `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.
|
|
40
41
|
- 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`.
|
|
41
42
|
- 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.
|
|
42
43
|
- `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.
|
|
@@ -92,18 +93,27 @@ same dependency, call `resolve-artifact` once and reuse the returned
|
|
|
92
93
|
|
|
93
94
|
Workspace detection is memoised in a process-resident `WorkspaceContextCache` (16-entry LRU, 5-minute TTL). The cache is observable through `manage-cache` with `cacheKinds: ["workspace"]`, and individual entries can be invalidated via `selector.projectPath`.
|
|
94
95
|
|
|
95
|
-
`target.kind="dependency"` resolution probes
|
|
96
|
+
`target.kind="dependency"` resolution probes up to six de-duplicated `gradle.properties` keys in order — `name_version`, `snake_case(name)_version`, `camelCaseVersion`, `lastSegment(group)_name_version`, `snake_case(lastSegment(group)_name)_version`, and `camelCase(lastSegment(group)_name)Version`. Hyphens become underscores in the snake_case forms (`fabric-api` probes `fabric_api_version`); for hyphen-less names the snake_case forms deduplicate into the raw keys, leaving four. The probe falls back to the modules-2 cache layout `~/.gradle/caches/modules-2/files-2.1/<group>/<name>/`. Version tokens that contain path separators, `..`, NUL, control characters, or any character outside `[A-Za-z0-9._+-]` are rejected; in `gradle.properties` the rejection is recorded under `attempts[]` as `gradle.properties:<key>:rejected-unsafe-version` and the next key is tried. Snapshot and dev directories are excluded by default. The modules-2 fallback resolves directly when exactly one valid entry remains. When several entries remain and the dependency is a submodule of an umbrella package (artifact name differs from the group's last segment, e.g. `net.fabricmc.fabric-api:fabric-screen-handler-api-v1`), the declared umbrella version property (`fabric_api_version` / `fabricApiVersion`) locates the cached umbrella POM and the submodule adopts the version that POM names — the resolving response records `provenance.submoduleVersionSource: "umbrella-pom"` with the POM path in `provenance.source` (workspace-context-cache hits within the TTL return the cached version with `provenance.source: "workspace-context-cache"` instead). Umbrella properties are never adopted verbatim as a submodule's version. Every remaining ambiguity raises `ERR_DEPENDENCY_VERSION_UNRESOLVED` with `candidatesSeen` so a global cache cannot supply a version the workspace did not declare.
|
|
96
97
|
|
|
97
98
|
## Common Pitfalls
|
|
98
99
|
|
|
100
|
+
- Flat-`artifactId` tools (`find-class`, `get-artifact-file`, `list-artifact-files`, `search-class-source`, `index-artifact`) also accept the shared `target` shape (`{ kind: "version" | "jar" | "coordinate" | "artifact" | "dependency", ... }`) instead of `artifactId` — exactly one of the two must be supplied. The target is resolved (and ingested if needed) before the lookup, so a fresh `resolve-artifact` round-trip is unnecessary. `{ kind: "artifact", artifactId }` passes through directly. Targets that need workspace context beyond their own fields — `kind: "workspace"`, or `kind: "dependency"` without an explicit `version` — cannot supply a `projectPath` through these tools; resolve them with `resolve-artifact` first and pass the `artifactId`.
|
|
101
|
+
- `analyze-symbol` infers an omitted `version` from `projectPath` (gradle.properties `minecraft_version`/`mc_version`); the response then carries `versionInference { version, source }` and a warning. An explicit `version` always wins. `inspect-minecraft` direct subjects without `subject.artifact` auto-resolve only when exactly one workspace is known to the process (provenance warning attached); several candidates are refused with `workspaceCandidates`.
|
|
102
|
+
|
|
103
|
+
- Loom split-source workspaces publish a version as a `minecraft-common` / `minecraft-clientOnly` sources-jar pair with no merged jar. Version-target resolution indexes both halves (the companion jar appears in `provenance.companionSourceJars`), so client-only classes are queryable under the merged scope. If a class still cannot be found, the error's `exampleCalls` carries a `scope: "vanilla"` retry — the decompiled client jar also contains client-only classes.
|
|
99
104
|
- `mapping="mojang"` requires source-backed artifacts on legacy obfuscated versions. For unobfuscated releases such as `26.1+`, the runtime/decompile path is accepted directly for version and versioned-coordinate targets. When source jars are not available but the version's Mojang tiny mappings, the tiny-remapper jar, and `MappingService.checkMappingHealth` are all healthy, `resolve-artifact` with `target.kind="version"` will transparently tiny-remap the binary jar (`obfuscated -> mojang`) and decompile the result, and the response carries `qualityFlags` `"binary-remapped"` and `"decompiled"` plus `provenance.transformChain` `"binary-remap:obf->mojang"` and `"decompile:vineflower"`. Coordinate and jar targets are not eligible for this fallback and still surface `ERR_MAPPING_NOT_APPLIED`. Source-backed and obfuscated artifacts keep their existing `artifactId` hashes — only the new mojang-remapped variant lives in a separate cache slot.
|
|
100
105
|
- Mojang binary-remap cache entries live under `<cacheDir>/remapped`. `manage-cache` lists valid files plus corrupt directories and leftover temp entries under `cacheKinds: ["binary-remap"]`; corrupt entries carry `status: "corrupt"` and `meta.artifactId`, so callers can preview or apply cleanup with `selector.artifactId`.
|
|
101
|
-
- `
|
|
106
|
+
- Jar-in-Jar shell jars (near-zero own classes with bundled `META-INF/jars/*.jar`, e.g. the Fabric API umbrella jar) no longer dead-end in `ERR_DECOMPILER_FAILED`. `resolve-artifact` detects the shell, skips decompilation, and returns the artifact with `qualityFlags: ["shell-jar"]` and the bundled inventory in `provenance.nestedJars`; `analyze-mod-jar` reports the same inventory as `nestedJars`. `get-class-source` / `get-class-members` lookups against a shell automatically redirect to the single nested jar containing the class and mark the response with `provenance.nestedJar` (`entryName` + `shellArtifactId`); a class found in several nested jars raises `ERR_NESTED_JAR_AMBIGUOUS` with `nestedJarCandidates` and per-candidate `suggestedCall` examples instead of picking one. Class-not-found errors on a shell carry the `nestedJars` inventory. The redirect is deliberately single-level: a nested jar that is itself a shell surfaces its own inventory when resolved directly as its own artifact. Extracted nested jars are content-addressed under `<cacheDir>/nested-jars`, observable through `manage-cache` with `cacheKinds: ["nested-jar"]`.
|
|
107
|
+
- `list-artifact-files` indexes Java source paths only — `assets/` and `data/` prefixes list nothing. Text files under those prefixes ARE retrievable by exact path with `get-artifact-file`: when the index has no row, the tool reads the entry directly from the backing jar (`deliveryMode: "jar-read-through"` marks such responses). This works for any artifact with a backing jar — vanilla client jars and mod jars alike; for Jar-in-Jar shells it reads the shell's own entries only (resolve a nested jar as its own artifact to reach its resources). Read-through delivery is text-only with a 512 KiB per-file cap (`truncated: true` beyond it); binary entries (e.g. `.png`, `.ogg`) answer with size metadata plus `contentOmittedReason` instead of content. Traversal-shaped paths (`..`, absolute) are rejected with `ERR_INVALID_INPUT`, and a miss returns `ERR_FILE_NOT_FOUND` with `nearbyPaths` naming same-named entries elsewhere in the jar (directory layouts move between versions, e.g. `assets/minecraft/models/item/*` → `assets/minecraft/items/*`).
|
|
102
108
|
- `search-class-source` defaults to `queryMode="auto"`. Use `queryMode="literal"` for explicit substring scans. `match="regex"` enforces `query.length <= 200` and caps results at `100`.
|
|
103
109
|
- `search-class-source` returns compact hits only. Use `get-artifact-file` or `get-class-source` to inspect returned files.
|
|
104
110
|
- `find-class` and `get-class-source` on `mapping="obfuscated"` expect Mojang obfuscated names. Deobfuscated queries warn and usually need `mapping="mojang"` or a `find-mapping` step first.
|
|
111
|
+
- `get-class-members` exposes `annotationDefault` on annotation-type members (the `default` value of an `@interface` member) whenever the classfile carries it, and accepts `includeAnnotations: true` to additionally list runtime-visible member annotations such as `@java.lang.Deprecated` per member. The leaner `names`/`signatures` projections drop annotation fields. `analyze-mod` `task="members"` includes both without a flag.
|
|
112
|
+
- On unobfuscated versions, `find-mapping`, `resolve-method-mapping-exact`, and `check-symbol-exists` report two structured `mappingContext` flags instead of per-response warning sentences (`get-class-api-matrix` reports a top-level `unobfuscatedRuntime: true` — its non-mojang columns are empty by design there): `unobfuscatedRuntime: true` replaces "Version X is unobfuscated; mapping graph is empty because the runtime already uses deobfuscated names.", and `runtimeValidated: true` replaces "Version X is unobfuscated; validated symbol existence against runtime bytecode." Do not pattern-match the old sentences.
|
|
105
113
|
- `check-symbol-exists` defaults to strict FQCN class lookup. Use `nameMode="auto"` for short class names.
|
|
114
|
+
- On unobfuscated versions, every `not_found` and `mapping_unavailable` verdict from the mapping graph — classes and fields included, not just methods — is re-validated against runtime bytecode before being returned, so a graph that lacks a record for a real symbol (e.g. `EntityType.ITEM`) no longer produces false negatives. Genuinely-missing symbols still return `not_found` after the runtime check. Bytecode-derived response contexts report `mappingNamespace: "mojang"` on unobfuscated versions instead of a hardcoded `"obfuscated"`.
|
|
106
115
|
- `check-symbol-exists` can use `signatureMode="name-only"` for overload discovery, but exact `descriptor` matching is still the most reliable path.
|
|
116
|
+
- Inherited-member expansion (`get-class-members` with `includeInherited: true`, and the `check-symbol-exists` runtime-bytecode fallback that reuses it) suppresses super-class and interface resolution warnings for platform packages that are never inside a Minecraft jar (`java.*`, `javax.*`, `jdk.*`, `sun.*`, `com.mojang.serialization.*`). A warning about any other unresolved super type — including `com.mojang.blaze3d.*` — is a real signal.
|
|
107
117
|
- `analyze-symbol task="api-overview"` inherits `sourceMapping` as the default `classNameMapping`; it falls back to `obfuscated` only when neither value is provided.
|
|
108
118
|
- `find-mapping` accepts short class ids such as `dhl` only when `sourceMapping="obfuscated"`. Other class lookup paths still validate class names as fully-qualified.
|
|
109
119
|
- `find-mapping` still rejects the public namespace name `official`, but upstream Tiny files that use `official` internally are now bridged to the supported `obfuscated` graph automatically.
|
|
@@ -247,7 +257,8 @@ Translate symbols across mapping namespaces with one shared Minecraft version. P
|
|
|
247
257
|
|
|
248
258
|
`ProblemDetails.code` may carry the codes below in addition to the existing tool-specific values.
|
|
249
259
|
|
|
250
|
-
- `ERR_WORKER_RESTART` — Synthetic envelope produced
|
|
260
|
+
- `ERR_WORKER_RESTART` — Synthetic envelope produced when the worker exits during `tools/call`, replacement startup or initialization replay fails, or live process-tree cleanup leaves the supervisor unable to start a worker. The response is a `CallToolResult` with `isError: true` and `structuredContent.error.code === "ERR_WORKER_RESTART"`. `structuredContent.meta.synthetic === true` and `meta.restart` records `tool` / `durationMs` / `lastStage` / `lastStageElapsedMs` / `lastStageMeta` / `exit` / `retryRecommendation` so the caller can decide whether to retry, narrow the input, clear caches, or report a bug. Non-`tools/call` requests receive the legacy raw JSON-RPC `-32603` envelope.
|
|
261
|
+
- `ERR_TOOL_TIMEOUT` — Synthetic `CallToolResult` produced only for `validate-project` when its supervisor-owned end-to-end deadline expires. The default is 120,000 ms and includes supervisor queue time. `meta.timeout.phase` is `"queue"` when the call expired before dispatch and `"running"` after dispatch. A queue timeout does not restart the worker; a running timeout isolates the worker process tree, initiates replacement, and sets `meta.timeout.workerRestartInitiated: true`. Cancellation suppresses the timeout result while retaining the deadline for worker cleanup.
|
|
251
262
|
- `ERR_MIXIN_PARSE_FAILED` — Reserved code for `validate-mixin` parse-stage failures in single / inline mode. Today the runtime parser is permissive (no hard parse failure path), but emitting this code keeps the contract stable for future strict-parse modes.
|
|
252
263
|
- `ERR_STAGE_BUDGET_PRE_PARSE` — Raised when a pre-parse stage (`resolve`, `mapping-health`, or `parse` itself) exhausts its independent soft-deadline before validation can proceed. The error carries `failedStage: <stage name>`, `meta.stageBudgetExhausted: true`, and the `budgetMs` / `elapsedMs` for the offending stage. Recovery: shrink `mixinConfigPath` (e.g. validate one config file at a time) or set `MIXIN_STAGE_BUDGETS_OFF=1` to disable budgets for diagnostic reruns.
|
|
253
264
|
- `ERR_WORKSPACE_VERSION_UNRESOLVED` — Raised by `resolve-artifact`, `get-class-source`, and `get-class-members` when `target.kind="workspace"` cannot detect a Minecraft version from `gradle.properties`. Both `strict: true` and `strict: false` raise; `details.strict` records which value was supplied. The error carries `details.projectPath` and a `suggestedCall` with `target: { kind: "version", value: "<your-mc-version>" }` so the caller can fall back to an explicit version target.
|
|
@@ -264,6 +275,9 @@ The optional `error.exampleCalls?: Array<{ tool: string; params: Record<string,
|
|
|
264
275
|
## Meta fields
|
|
265
276
|
|
|
266
277
|
- `meta.restart` (synthetic worker-restart responses only) — emitted exclusively when the supervisor returns an `ERR_WORKER_RESTART` envelope. Shape: `{ tool, durationMs, lastStage, lastStageElapsedMs, lastStageMeta, exit: { code, signal }, retryRecommendation }`. `retryRecommendation` is one of `"narrow-query" | "clear-cache" | "report-bug" | "same-request"`, decided by the supervisor from `lastStage`, `exit.signal`, and the recent restart cadence (3 restarts within 60 s of the same tool → `"report-bug"`).
|
|
278
|
+
- `meta.timeout` (`ERR_TOOL_TIMEOUT` only) — exact shape: `{ tool: "validate-project", phase: "queue" | "running", durationMs, deadlineMs, lastStage, lastStageElapsedMs, lastStageMeta, redactedToolArgs, redactedToolArgsModified, retryRecommendation, workerRestartInitiated }`. Stage and argument diagnostics use the supervisor's bounded redaction rules. `workerRestartInitiated` reports initiation only; it does not claim that replacement startup or initialization replay completed.
|
|
279
|
+
- `meta.queue` (supervisor queue overflow only) — `{ reason: "supervisor-request-queue", maxQueued: 2, queuedCount: 2 }`. The FIFO retains at most two total worker-bound requests; a queued `validate-project` barrier consumes one slot, while a running barrier is outside the FIFO. Overflowing `tools/call` receives a synthetic `ERR_LIMIT_EXCEEDED` result. An overflowing non-tool request receives raw JSON-RPC `-32000` with message `"MCP supervisor request queue is full."`; notifications do not consume slots.
|
|
280
|
+
- Replacement startup/replay uses a 10–30 second internal watchdog and bounded retry backoff. Successful replay releases queued work. Spawn, pre-ready, replay, or watchdog failure terminalizes queued requests with the existing worker-restart response shapes. At the two-slot live-generation cap, new worker-bound requests, including `initialize`, fail immediately instead of entering the FIFO; notifications that cannot be delivered are emitted as structured supervisor warnings and dropped. Tree-cleanup helper attempts are bounded to five seconds so recovery and shutdown cannot wait forever on `taskkill` or an equivalent platform operation. On POSIX, `ESRCH` while signaling the saved process group means the group is already gone and is treated as completed cleanup.
|
|
267
281
|
- `meta.stageBudgetExhausted` (`ERR_STAGE_BUDGET_PRE_PARSE` errors only) — set to `true` to flag that the failure is budget-driven rather than a genuine resolve / mapping-health / parse error. Pair with `failedStage` and `error.detail` (`"Stage <name> exhausted budget before parse completed."`) for diagnostics. The companion fields `meta.budgetMs` (the stage budget that was exceeded) and `meta.elapsedMs` (the actual stage elapsed time) are emitted alongside it on the same envelope so callers can decide between retry, input-shrink (`mixinConfigPath`), or `MIXIN_STAGE_BUDGETS_OFF=1` rollback without parsing message text.
|
|
268
282
|
- `validate-mixin` batch-mode entries (`results[i]`, when invoked with `input.mode = "paths" | "config" | "project"`) preserve typed error metadata when an entry fails: optional `errorCode` (e.g. `"ERR_STAGE_BUDGET_PRE_PARSE"`) and `errorDetails` (the `failedStage` / `stageBudgetExhausted` / `budgetMs` / `elapsedMs` shape from the underlying AppError) sit alongside the legacy `error` string. The shape is additive — callers that only read `error` continue to see the same human-readable message.
|
|
269
283
|
|
|
@@ -294,6 +308,7 @@ These environment variables are read once at worker startup and provide rollback
|
|
|
294
308
|
- `analyze-symbol` `subject.kind` accepts `"symbol"` to auto-detect the concrete kind from the selector (owner+descriptor ⇒ method, owner only ⇒ field, otherwise ⇒ class); the inferred kind is surfaced as a warning. For `task="api-overview"` the inferred kind is always class.
|
|
295
309
|
- Start with `compare-minecraft` for version-pair, class diff, registry diff, and migration-summary flows before using `compare-versions`, `diff-class-signatures`, or `get-registry-data` directly.
|
|
296
310
|
- Start with `analyze-mod` for metadata-first mod inspection and safe remap preview/apply flows before using `analyze-mod-jar`, `decompile-mod-jar`, `get-mod-class-source`, `search-mod-source`, or `remap-mod-jar` directly.
|
|
311
|
+
- `analyze-mod` `task="members"` (subject `{ kind: "class", jarPath, className }`) reads a mod class's constructors/fields/methods (all access levels, including private and protected) straight from bytecode — no decompiler runs on this path, unlike `task="class-source"` which costs a full decompile. Responses mark `extractionMethod: "bytecode-only"`; a class missing from the jar returns `ERR_CLASS_NOT_FOUND`.
|
|
297
312
|
- Start with `validate-project` for workspace summaries and direct Mixin, Access Widener, or Access Transformer validation before using `validate-mixin`, `validate-access-widener`, or `validate-access-transformer` directly.
|
|
298
313
|
- `validate-project task="project-summary"` discovers mixins and access wideners by default. Add `discover: ["access-transformers"]` when you also want Access Transformer files included in the workspace summary.
|
|
299
314
|
- `validate-project task="project-summary"` returns an additive `tasks` field alongside the existing aggregate `result.summary` / `result.project` blocks. The headline `result.summary.status` is unchanged; the new field reports per-probe status so a `status: "blocked"` headline still preserves which probes succeeded.
|
|
@@ -477,6 +492,7 @@ Path-based overrides treat blank values and the literal strings `undefined` and
|
|
|
477
492
|
| Variable | Default | Description |
|
|
478
493
|
| --- | --- | --- |
|
|
479
494
|
| `MCP_SUPERVISOR_DEBUG` | unset | Set to `1` to emit verbose stdio supervisor diagnostics |
|
|
495
|
+
| `MCP_VALIDATE_PROJECT_TIMEOUT_MS` | `120000` | End-to-end `validate-project` deadline, including queue time. Accepts ASCII decimal digits only and must be from `10000` through `600000`; invalid values use the default. |
|
|
480
496
|
|
|
481
497
|
Internal worker-mode environment variables are reserved for the transport implementation and are intentionally omitted from the public reference.
|
|
482
498
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhisang/minecraft-modding-mcp",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.2.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",
|