@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.
Files changed (54) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +4 -2
  3. package/dist/cache-registry.d.ts +1 -1
  4. package/dist/cache-registry.js +3 -0
  5. package/dist/entry-tools/analyze-mod-service.d.ts +12 -6
  6. package/dist/entry-tools/analyze-mod-service.js +37 -3
  7. package/dist/entry-tools/analyze-symbol-service.d.ts +2 -0
  8. package/dist/entry-tools/analyze-symbol-service.js +37 -2
  9. package/dist/entry-tools/inspect-minecraft/internal.d.ts +4 -0
  10. package/dist/entry-tools/inspect-minecraft/internal.js +28 -0
  11. package/dist/entry-tools/manage-cache-service.d.ts +4 -4
  12. package/dist/error-mapping.d.ts +13 -0
  13. package/dist/error-mapping.js +35 -2
  14. package/dist/errors.d.ts +2 -0
  15. package/dist/errors.js +2 -0
  16. package/dist/index.js +37 -17
  17. package/dist/mapping/internal-types.d.ts +7 -0
  18. package/dist/mapping/types.d.ts +18 -0
  19. package/dist/mapping-service.js +16 -3
  20. package/dist/minecraft-explorer-service.d.ts +4 -0
  21. package/dist/minecraft-explorer-service.js +156 -17
  22. package/dist/mod-analyzer.d.ts +7 -0
  23. package/dist/mod-analyzer.js +28 -7
  24. package/dist/nbt/typed-json.js +4 -1
  25. package/dist/source/artifact-resolver.d.ts +2 -0
  26. package/dist/source/artifact-resolver.js +24 -1
  27. package/dist/source/class-source/members-builder.d.ts +4 -0
  28. package/dist/source/class-source/members-builder.js +3 -1
  29. package/dist/source/class-source.d.ts +2 -1
  30. package/dist/source/class-source.js +154 -17
  31. package/dist/source/did-you-mean.d.ts +14 -0
  32. package/dist/source/did-you-mean.js +79 -0
  33. package/dist/source/file-access.js +159 -3
  34. package/dist/source/indexer.js +72 -2
  35. package/dist/source/lifecycle/runtime-check.js +15 -7
  36. package/dist/source/nested-jars.d.ts +59 -0
  37. package/dist/source/nested-jars.js +198 -0
  38. package/dist/source/workspace-target.js +5 -2
  39. package/dist/source-jar-reader.d.ts +16 -0
  40. package/dist/source-jar-reader.js +82 -0
  41. package/dist/source-service.d.ts +36 -0
  42. package/dist/source-service.js +49 -6
  43. package/dist/stage-emitter.js +24 -8
  44. package/dist/stdio-supervisor.d.ts +92 -9
  45. package/dist/stdio-supervisor.js +915 -103
  46. package/dist/tool-contract-manifest.js +1 -1
  47. package/dist/tool-guidance.js +3 -1
  48. package/dist/tool-schemas.d.ts +1337 -149
  49. package/dist/tool-schemas.js +38 -7
  50. package/dist/types.d.ts +23 -0
  51. package/dist/workspace-mapping-service.d.ts +1 -0
  52. package/dist/workspace-mapping-service.js +120 -8
  53. package/docs/tool-reference.md +19 -3
  54. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { makeStageEmitter } from "./stage-emitter.js";
4
- import { ZodError, z } from "zod";
4
+ import { ZodError } from "zod";
5
5
  import { CompatStdioServerTransport } from "./compat-stdio-transport.js";
6
6
  import { objectResult } from "./mcp-helpers.js";
7
7
  import { prepareToolInput } from "./tool-input.js";
@@ -34,7 +34,7 @@ import { buildEntryToolMeta } from "./entry-tools/response-contract.js";
34
34
  import { registerToolSchema } from "./tool-schema-registry.js";
35
35
  import { buildSuggestedCall } from "./build-suggested-call.js";
36
36
  import { applyErrorMetaExtensions, mapErrorToProblem } from "./tool-guidance.js";
37
- import { nonEmptyString, optionalPositiveInt, analyzeModJarSchema, analyzeModJarShape, batchClassMembersSchema, batchClassMembersShape, batchClassSourceSchema, batchClassSourceShape, batchMappingsSchema, batchMappingsShape, batchSymbolExistsSchema, batchSymbolExistsShape, checkSymbolExistsSchema, checkSymbolExistsShape, compareVersionsSchema, compareVersionsShape, decompileModJarSchema, decompileModJarShape, diffClassSignaturesSchema, diffClassSignaturesShape, emptySchema, findMappingSchema, findMappingShape, getArtifactFileSchema, getArtifactFileShape, getClassApiMatrixSchema, getClassApiMatrixShape, getClassMembersSchema, getClassMembersShape, getClassSourceSchema, getClassSourceShape, getModClassSourceSchema, getModClassSourceShape, getRegistryDataSchema, getRegistryDataShape, indexArtifactSchema, indexArtifactShape, jsonToNbtSchema, jsonToNbtShape, listArtifactFilesSchema, listArtifactFilesShape, listVersionsSchema, listVersionsShape, nbtApplyJsonPatchSchema, nbtApplyJsonPatchShape, nbtToJsonSchema, nbtToJsonShape, remapModJarSchema, remapModJarShape, resolveArtifactSchema, resolveArtifactShape, resolveMethodMappingExactSchema, resolveMethodMappingExactShape, resolveWorkspaceSymbolSchema, resolveWorkspaceSymbolShape, searchClassSourceSchema, searchClassSourceShape, searchModSourceSchema, searchModSourceShape, traceSymbolLifecycleSchema, traceSymbolLifecycleShape, validateAccessTransformerSchema, validateAccessTransformerShape, validateAccessWidenerSchema, validateAccessWidenerShape, validateMixinSchema, validateMixinShape, verifyMixinTargetSchema, verifyMixinTargetShape } from "./tool-schemas.js";
37
+ import { analyzeModJarSchema, analyzeModJarShape, batchClassMembersSchema, batchClassMembersShape, batchClassSourceSchema, batchClassSourceShape, batchMappingsSchema, batchMappingsShape, batchSymbolExistsSchema, batchSymbolExistsShape, checkSymbolExistsSchema, checkSymbolExistsShape, compareVersionsSchema, compareVersionsShape, decompileModJarSchema, decompileModJarShape, diffClassSignaturesSchema, diffClassSignaturesShape, emptySchema, findMappingSchema, findMappingShape, getArtifactFileSchema, getArtifactFileShape, getClassApiMatrixSchema, getClassApiMatrixShape, getClassMembersSchema, getClassMembersShape, getClassSourceSchema, getClassSourceShape, getModClassSourceSchema, getModClassSourceShape, getRegistryDataSchema, getRegistryDataShape, indexArtifactSchema, indexArtifactShape, jsonToNbtSchema, jsonToNbtShape, listArtifactFilesSchema, listArtifactFilesShape, listVersionsSchema, listVersionsShape, nbtApplyJsonPatchSchema, nbtApplyJsonPatchShape, nbtToJsonSchema, nbtToJsonShape, remapModJarSchema, remapModJarShape, resolveArtifactSchema, resolveArtifactShape, resolveMethodMappingExactSchema, resolveMethodMappingExactShape, resolveWorkspaceSymbolSchema, resolveWorkspaceSymbolShape, searchClassSourceSchema, searchClassSourceShape, searchModSourceSchema, searchModSourceShape, traceSymbolLifecycleSchema, traceSymbolLifecycleShape, validateAccessTransformerSchema, validateAccessTransformerShape, validateAccessWidenerSchema, validateAccessWidenerShape, validateMixinSchema, validateMixinShape, verifyMixinTargetSchema, verifyMixinTargetShape, findClassShape, findClassSchema } from "./tool-schemas.js";
38
38
  export { mapErrorToProblem, applyErrorMetaExtensions };
39
39
  if (!process.env.NODE_ENV) {
40
40
  process.env.NODE_ENV = "production";
@@ -121,9 +121,11 @@ const inspectMinecraftService = new InspectMinecraftService({
121
121
  searchClassSource: (input) => sourceService.searchClassSource(input),
122
122
  getArtifactFile: (input) => sourceService.getArtifactFile(input),
123
123
  listArtifactFiles: (input) => sourceService.listArtifactFiles(input),
124
- detectProjectMinecraftVersion: (projectPath) => workspaceMappingService.detectProjectMinecraftVersion(projectPath)
124
+ detectProjectMinecraftVersion: (projectPath) => workspaceMappingService.detectProjectMinecraftVersion(projectPath),
125
+ listWorkspaceContexts: () => sourceService.workspaceContextCache.list()
125
126
  });
126
127
  const analyzeSymbolService = new AnalyzeSymbolService({
128
+ detectProjectMinecraftVersion: (projectPath) => workspaceMappingService.detectProjectMinecraftVersion(projectPath),
127
129
  checkSymbolExists: (input) => sourceService.checkSymbolExists(input),
128
130
  findMapping: (input) => sourceService.findMapping(input),
129
131
  resolveMethodMappingExact: (input) => sourceService.resolveMethodMappingExact(input),
@@ -141,7 +143,8 @@ const analyzeModService = new AnalyzeModService({
141
143
  decompileModJar: (input) => sourceService.decompileModJar(input),
142
144
  getModClassSource: (input) => sourceService.getModClassSource(input),
143
145
  searchModSource: (input) => sourceService.searchModSource(input),
144
- remapModJar: (input) => remapModJar(input, config)
146
+ remapModJar: (input) => remapModJar(input, config),
147
+ getModClassMembers: (input) => sourceService.getModClassMembers(input)
145
148
  });
146
149
  const validateProjectService = new ValidateProjectService({
147
150
  validateMixin: (input, options) => sourceService.validateMixin(input, options),
@@ -475,7 +478,7 @@ server.tool("analyze-symbol", "Top-level workflow tool for symbol existence, map
475
478
  registerToolSchema("analyze-symbol", analyzeSymbolSchema);
476
479
  server.tool("compare-minecraft", "Top-level workflow tool for version comparisons, class diffs, registry diffs, and migration overviews.", compareMinecraftShape, { readOnlyHint: true }, async (args) => runTool("compare-minecraft", args, compareMinecraftSchema, async (input) => compareMinecraftService.execute(input)));
477
480
  registerToolSchema("compare-minecraft", compareMinecraftSchema);
478
- server.tool("analyze-mod", "Top-level workflow tool for mod metadata inspection, decompile/search flows, class source, and safe remap previews/applies.", analyzeModShape, { readOnlyHint: false }, async (args) => runTool("analyze-mod", args, analyzeModSchema, async (input) => analyzeModService.execute(input)));
481
+ server.tool("analyze-mod", "Top-level workflow tool for mod metadata inspection, decompile/search flows, class source, bytecode-only class member reads, and safe remap previews/applies.", analyzeModShape, { readOnlyHint: false }, async (args) => runTool("analyze-mod", args, analyzeModSchema, async (input) => analyzeModService.execute(input)));
479
482
  registerToolSchema("analyze-mod", analyzeModSchema);
480
483
  server.tool("validate-project", "Top-level workflow tool for project summary, direct mixin validation, and access widener/access transformer validation.", validateProjectShape, { readOnlyHint: true }, async (args, extra) => runTool("validate-project", args, validateProjectSchema, async (input) => validateProjectService.execute(input, {
481
484
  stageEmitter: makeStageEmitter(extra)
@@ -524,15 +527,32 @@ expertTool("resolve-artifact", "Resolve a source artifact and return artifact me
524
527
  (Array.isArray(input.include) && input.include.includes("samples"))
525
528
  })));
526
529
  registerToolSchema("resolve-artifact", resolveArtifactSchema);
527
- const findClassShape = {
528
- className: nonEmptyString.describe("Simple name (e.g. Blocks) or fully-qualified name (e.g. net.minecraft.world.level.block.Blocks)"),
529
- artifactId: nonEmptyString,
530
- limit: optionalPositiveInt.describe("default 20, max 200")
531
- };
532
- const findClassSchema = z.object(findClassShape);
530
+ // Flat tools accept `target` as an additive alternative to `artifactId`
531
+ // (mutually exclusive, enforced by each schema). A target is resolved to its
532
+ // artifactId here before the underlying artifactId-based service call.
533
+ async function resolveFlatArtifactId(input) {
534
+ if (input.artifactId) {
535
+ return input.artifactId;
536
+ }
537
+ const target = input.target;
538
+ // {kind:"artifact"} reuses an already-resolved artifact without a resolve
539
+ // round-trip. Workspace-context kinds carry their own projectPath when the
540
+ // target shape provides one; kinds that require workspace context beyond
541
+ // that fail with the resolver's own projectPath guidance.
542
+ if (target?.kind === "artifact" && target.artifactId) {
543
+ return target.artifactId;
544
+ }
545
+ const resolved = await sourceService.resolveArtifact({
546
+ target: target,
547
+ ...(target && "projectPath" in target && target.projectPath
548
+ ? { projectPath: target.projectPath }
549
+ : {})
550
+ });
551
+ return resolved.artifactId;
552
+ }
533
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({
534
554
  className: input.className,
535
- artifactId: input.artifactId,
555
+ artifactId: await resolveFlatArtifactId(input),
536
556
  limit: input.limit
537
557
  })));
538
558
  registerToolSchema("find-class", findClassSchema);
@@ -596,7 +616,7 @@ expertTool("search-class-source", "Search indexed class source files for one art
596
616
  }
597
617
  : undefined;
598
618
  return sourceService.searchClassSource({
599
- artifactId: input.artifactId,
619
+ artifactId: await resolveFlatArtifactId(input),
600
620
  query: input.query,
601
621
  intent: input.intent,
602
622
  match: input.match,
@@ -611,13 +631,13 @@ expertTool("search-class-source", "Search indexed class source files for one art
611
631
  }));
612
632
  registerToolSchema("search-class-source", searchClassSourceSchema);
613
633
  expertTool("get-artifact-file", "Get full source file content by artifactId and file path.", getArtifactFileShape, { readOnlyHint: true }, async (args) => runTool("get-artifact-file", args, getArtifactFileSchema, async (input) => sourceService.getArtifactFile({
614
- artifactId: input.artifactId,
634
+ artifactId: await resolveFlatArtifactId(input),
615
635
  filePath: input.filePath,
616
636
  maxBytes: input.maxBytes
617
637
  })));
618
638
  registerToolSchema("get-artifact-file", getArtifactFileSchema);
619
639
  expertTool("list-artifact-files", "List source file paths in an artifact with optional prefix filter and cursor-based pagination.", listArtifactFilesShape, { readOnlyHint: true }, async (args) => runTool("list-artifact-files", args, listArtifactFilesSchema, async (input) => sourceService.listArtifactFiles({
620
- artifactId: input.artifactId,
640
+ artifactId: await resolveFlatArtifactId(input),
621
641
  prefix: input.prefix,
622
642
  limit: input.limit,
623
643
  cursor: input.cursor
@@ -727,8 +747,8 @@ server.tool("json-to-nbt", "Encode typed NBT JSON to Java Edition NBT binary pay
727
747
  compression: input.compression
728
748
  }, nbtLimits))));
729
749
  registerToolSchema("json-to-nbt", jsonToNbtSchema);
730
- expertTool("index-artifact", "Rebuild indexed files/symbols metadata for an existing artifactId. Does not resolve new artifacts.", indexArtifactShape, { readOnlyHint: false, idempotentHint: true }, async (args) => runTool("index-artifact", args, indexArtifactSchema, async (input) => sourceService.indexArtifact({
731
- artifactId: input.artifactId,
750
+ expertTool("index-artifact", "Rebuild indexed files/symbols metadata for an artifact addressed by artifactId or target. A new target is resolved and ingested first; force rebuilds an existing index.", indexArtifactShape, { readOnlyHint: false, idempotentHint: true }, async (args) => runTool("index-artifact", args, indexArtifactSchema, async (input) => sourceService.indexArtifact({
751
+ artifactId: await resolveFlatArtifactId(input),
732
752
  force: input.force
733
753
  })));
734
754
  registerToolSchema("index-artifact", indexArtifactSchema);
@@ -56,6 +56,13 @@ export type CandidateAccumulator = {
56
56
  };
57
57
  export type LoadedGraph = {
58
58
  version: string;
59
+ /**
60
+ * True when the runtime already uses deobfuscated names, so the graph is
61
+ * deliberately empty. Surfaces on outputs as
62
+ * mappingContext.unobfuscatedRuntime instead of a per-response warning
63
+ * sentence.
64
+ */
65
+ unobfuscatedRuntime?: boolean;
59
66
  priority: import("../types.js").MappingSourcePriority;
60
67
  mode: GraphLoadMode;
61
68
  pairs: Map<PairKey, PairRecord>;
@@ -38,6 +38,19 @@ export type SymbolResolutionOutput = {
38
38
  sourceMapping: SourceMapping;
39
39
  targetMapping?: SourceMapping;
40
40
  sourcePriorityApplied: MappingSourcePriority;
41
+ /**
42
+ * The runtime already uses deobfuscated names, so the mapping graph is
43
+ * deliberately empty. Replaces the former per-response warning
44
+ * "Version X is unobfuscated; mapping graph is empty because the runtime
45
+ * already uses deobfuscated names."
46
+ */
47
+ unobfuscatedRuntime?: boolean;
48
+ /**
49
+ * Symbol existence was validated against runtime bytecode. Replaces the
50
+ * former per-response warning "Version X is unobfuscated; validated
51
+ * symbol existence against runtime bytecode."
52
+ */
53
+ runtimeValidated?: boolean;
41
54
  };
42
55
  resolved: boolean;
43
56
  status: SymbolResolutionStatus;
@@ -122,6 +135,11 @@ export type ClassApiMatrixRow = {
122
135
  completeness: boolean;
123
136
  };
124
137
  export type ClassApiMatrixOutput = {
138
+ /**
139
+ * The runtime already uses deobfuscated names, so non-mojang matrix
140
+ * columns are empty by design (the mapping graph is deliberately empty).
141
+ */
142
+ unobfuscatedRuntime?: boolean;
125
143
  version: string;
126
144
  className: string;
127
145
  classNameMapping: SourceMapping;
@@ -126,6 +126,9 @@ export class MappingService {
126
126
  };
127
127
  }
128
128
  const graph = await this.loadGraph(version, priority, requiresOnlyObfuscatedMojangGraph(sourceMapping, targetMapping) ? "obfuscated-mojang-only" : "full", input.projectPath, input.gradleUserHome);
129
+ if (graph.unobfuscatedRuntime) {
130
+ mappingContext.unobfuscatedRuntime = true;
131
+ }
129
132
  const path = namespacePath(graph, sourceMapping, targetMapping);
130
133
  if (!path) {
131
134
  return {
@@ -352,6 +355,9 @@ export class MappingService {
352
355
  };
353
356
  }
354
357
  const graph = await this.loadGraph(version, priority, requiresOnlyObfuscatedMojangGraph(sourceMapping, targetMapping) ? "obfuscated-mojang-only" : "full", input.projectPath, input.gradleUserHome);
358
+ if (graph.unobfuscatedRuntime) {
359
+ mappingContext.unobfuscatedRuntime = true;
360
+ }
355
361
  const path = namespacePath(graph, sourceMapping, targetMapping);
356
362
  if (!path) {
357
363
  return {
@@ -469,6 +475,7 @@ export class MappingService {
469
475
  const priority = mappingPriorityFromInput(this.config.mappingSourcePriority, input.sourcePriority);
470
476
  const graph = await this.loadGraph(version, priority, "full", undefined, input.gradleUserHome);
471
477
  const warnings = [...graph.warnings];
478
+ const unobfuscatedRuntime = graph.unobfuscatedRuntime === true;
472
479
  const includeKinds = normalizeIncludedKinds(input.includeKinds);
473
480
  const pathCache = new Map();
474
481
  const resolvePath = (sourceMapping, targetMapping) => {
@@ -514,6 +521,7 @@ export class MappingService {
514
521
  },
515
522
  rows: [],
516
523
  rowCount: 0,
524
+ ...(unobfuscatedRuntime ? { unobfuscatedRuntime: true } : {}),
517
525
  warnings
518
526
  };
519
527
  }
@@ -638,6 +646,7 @@ export class MappingService {
638
646
  version,
639
647
  className,
640
648
  classNameMapping,
649
+ ...(unobfuscatedRuntime ? { unobfuscatedRuntime: true } : {}),
641
650
  classIdentity: {
642
651
  obfuscated: classByMapping.obfuscated?.symbol,
643
652
  mojang: classByMapping.mojang?.symbol,
@@ -727,6 +736,9 @@ export class MappingService {
727
736
  };
728
737
  })();
729
738
  const graph = await this.loadGraph(version, priority, sourceMapping === "mojang" ? "obfuscated-mojang-only" : "full", undefined, input.gradleUserHome);
739
+ if (graph.unobfuscatedRuntime) {
740
+ mappingContext.unobfuscatedRuntime = true;
741
+ }
730
742
  const warnings = [...graph.warnings];
731
743
  const records = collectTargetRecords(graph, sourceMapping);
732
744
  if (records.length === 0) {
@@ -1115,9 +1127,10 @@ export class MappingService {
1115
1127
  recordsByTarget: new Map(),
1116
1128
  exactRecordIndex: new Map(),
1117
1129
  classProjectionCache: new Map(),
1118
- warnings: [
1119
- `Version ${version} is unobfuscated; mapping graph is empty because the runtime already uses deobfuscated names.`
1120
- ]
1130
+ // No warning sentence: consumers surface this as the structured
1131
+ // mappingContext.unobfuscatedRuntime flag instead.
1132
+ unobfuscatedRuntime: true,
1133
+ warnings: []
1121
1134
  };
1122
1135
  }
1123
1136
  const graph = {
@@ -15,6 +15,10 @@ export interface SignatureMember {
15
15
  jvmDescriptor: string;
16
16
  accessFlags: number;
17
17
  isSynthetic: boolean;
18
+ /** Rendered default value of an annotation-type member (AnnotationDefault). */
19
+ annotationDefault?: string;
20
+ /** Rendered runtime-visible annotations on the member, e.g. "@java.lang.Deprecated". */
21
+ annotations?: string[];
18
22
  }
19
23
  export interface GetSignatureInput {
20
24
  fqn: string;
@@ -3,6 +3,7 @@ import { loadConfig } from "./config.js";
3
3
  import { artifactSignatureFromFile, normalizeJarPath } from "./path-resolver.js";
4
4
  import { createJarEntryReader } from "./source-jar-reader.js";
5
5
  import { matchesMemberPattern } from "./source/member-pattern.js";
6
+ import { isUnobfuscatedVersion } from "./version-service.js";
6
7
  const CLASSFILE_MAGIC = 0xcafebabe;
7
8
  const MAX_INHERITANCE_DEPTH = 64;
8
9
  const ACC_PUBLIC = 0x0001;
@@ -173,6 +174,21 @@ function toInternalName(fqn) {
173
174
  function extractVersionFromPath(inputPath) {
174
175
  return inputPath.match(/(\d+\.\d+(?:\.\d+)?)/)?.[1];
175
176
  }
177
+ // Super classes/interfaces from these packages are never inside a Minecraft
178
+ // jar, so failing to resolve them during inherited-member expansion is
179
+ // expected, not a signal. Suppression is allowlist-only and per-prefix
180
+ // verified: JDK packages by definition, com.mojang.serialization because the
181
+ // DataFixerUpper serialization library ships as its own dependency jar
182
+ // (checked against 26.x client-only/common and 1.21.10 client jars). A broad
183
+ // com.mojang.* entry would be wrong — com.mojang.blaze3d.* IS client-jar
184
+ // content and its resolution failures must stay visible.
185
+ const KNOWN_ABSENT_PLATFORM_PREFIXES = [
186
+ "java/",
187
+ "javax/",
188
+ "jdk/",
189
+ "sun/",
190
+ "com/mojang/serialization/"
191
+ ];
176
192
  class SignatureCacheStore {
177
193
  maxEntries;
178
194
  nodes = new Map();
@@ -305,16 +321,111 @@ function readOptionalClassName(cp, index) {
305
321
  }
306
322
  return readClassName(cp, index);
307
323
  }
324
+ function descriptorToClassName(descriptor) {
325
+ if (descriptor.startsWith("L") && descriptor.endsWith(";")) {
326
+ return descriptor.slice(1, -1).replace(/\//g, ".");
327
+ }
328
+ return descriptor;
329
+ }
330
+ function readNumericConstant(cp, index) {
331
+ const entry = cp[index];
332
+ if (entry && (entry.tag === 3 || entry.tag === 4 || entry.tag === 5 || entry.tag === 6)) {
333
+ return String(entry.numericValue);
334
+ }
335
+ return "<unknown-constant>";
336
+ }
337
+ // JVMS 4.7.16.1 element_value, rendered as compact Java-ish text. Every
338
+ // branch reads exactly its encoded bytes so nested/array values stay aligned.
339
+ function readElementValue(reader, cp) {
340
+ const tag = String.fromCharCode(reader.readU1());
341
+ switch (tag) {
342
+ case "B":
343
+ case "I":
344
+ case "S":
345
+ case "D":
346
+ case "F":
347
+ case "J":
348
+ return readNumericConstant(cp, reader.readU2());
349
+ case "C": {
350
+ const raw = readNumericConstant(cp, reader.readU2());
351
+ const code = Number(raw);
352
+ return Number.isFinite(code) ? `'${String.fromCharCode(code)}'` : raw;
353
+ }
354
+ case "Z": {
355
+ const raw = readNumericConstant(cp, reader.readU2());
356
+ return raw === "1" ? "true" : raw === "0" ? "false" : raw;
357
+ }
358
+ case "s":
359
+ return JSON.stringify(readUtf8(cp, reader.readU2()));
360
+ case "e": {
361
+ const typeDescriptor = readUtf8(cp, reader.readU2());
362
+ const constantName = readUtf8(cp, reader.readU2());
363
+ return `${descriptorToClassName(typeDescriptor)}.${constantName}`;
364
+ }
365
+ case "c":
366
+ return `${descriptorToClassName(readUtf8(cp, reader.readU2()))}.class`;
367
+ case "@":
368
+ return readAnnotationText(reader, cp);
369
+ case "[": {
370
+ const count = reader.readU2();
371
+ const parts = [];
372
+ for (let index = 0; index < count; index += 1) {
373
+ parts.push(readElementValue(reader, cp));
374
+ }
375
+ return `{${parts.join(", ")}}`;
376
+ }
377
+ default:
378
+ return "<unsupported-element-value>";
379
+ }
380
+ }
381
+ function readAnnotationText(reader, cp) {
382
+ const typeDescriptor = readUtf8(cp, reader.readU2());
383
+ const pairCount = reader.readU2();
384
+ const pairs = [];
385
+ for (let index = 0; index < pairCount; index += 1) {
386
+ const name = readUtf8(cp, reader.readU2());
387
+ pairs.push(`${name} = ${readElementValue(reader, cp)}`);
388
+ }
389
+ return `@${descriptorToClassName(typeDescriptor)}${pairs.length > 0 ? `(${pairs.join(", ")})` : ""}`;
390
+ }
308
391
  function readAttributes(reader, cp, count) {
309
392
  const names = [];
393
+ let annotationDefault;
394
+ let annotations;
310
395
  for (let index = 0; index < count; index += 1) {
311
396
  const nameIndex = reader.readU2();
312
397
  const length = reader.readU4();
313
398
  const attributeName = readUtf8(cp, nameIndex);
314
399
  names.push(attributeName);
315
- reader.skip(length);
400
+ const body = reader.readBytes(length);
401
+ // Malformed attribute bodies must never fail the whole class parse; the
402
+ // rendered value is best-effort metadata, not a structural field.
403
+ if (attributeName === "AnnotationDefault") {
404
+ try {
405
+ annotationDefault = readElementValue(new ByteReader(body), cp);
406
+ }
407
+ catch {
408
+ annotationDefault = undefined;
409
+ }
410
+ }
411
+ else if (attributeName === "RuntimeVisibleAnnotations") {
412
+ try {
413
+ const bodyReader = new ByteReader(body);
414
+ const annotationCount = bodyReader.readU2();
415
+ const rendered = [];
416
+ for (let a = 0; a < annotationCount; a += 1) {
417
+ rendered.push(readAnnotationText(bodyReader, cp));
418
+ }
419
+ if (rendered.length > 0) {
420
+ annotations = rendered;
421
+ }
422
+ }
423
+ catch {
424
+ annotations = undefined;
425
+ }
426
+ }
316
427
  }
317
- return names;
428
+ return { names, annotationDefault, annotations };
318
429
  }
319
430
  function parseClassFile(buffer) {
320
431
  const reader = new ByteReader(buffer);
@@ -336,17 +447,28 @@ function parseClassFile(buffer) {
336
447
  cp[index] = { tag: 1, value: reader.readBytes(length).toString("utf8") };
337
448
  break;
338
449
  }
339
- case 3:
340
- case 4:
341
- reader.skip(4);
342
- cp[index] = { tag };
450
+ case 3: {
451
+ const bytes = reader.readBytes(4);
452
+ cp[index] = { tag, numericValue: bytes.readInt32BE(0) };
453
+ break;
454
+ }
455
+ case 4: {
456
+ const bytes = reader.readBytes(4);
457
+ cp[index] = { tag, numericValue: bytes.readFloatBE(0) };
343
458
  break;
344
- case 5:
345
- case 6:
346
- reader.skip(8);
347
- cp[index] = { tag };
459
+ }
460
+ case 5: {
461
+ const bytes = reader.readBytes(8);
462
+ cp[index] = { tag, numericValue: bytes.readBigInt64BE(0).toString() };
348
463
  index += 1;
349
464
  break;
465
+ }
466
+ case 6: {
467
+ const bytes = reader.readBytes(8);
468
+ cp[index] = { tag, numericValue: bytes.readDoubleBE(0) };
469
+ index += 1;
470
+ break;
471
+ }
350
472
  case 7:
351
473
  case 8:
352
474
  case 16:
@@ -389,13 +511,17 @@ function parseClassFile(buffer) {
389
511
  const nameIndex = reader.readU2();
390
512
  const descriptorIndex = reader.readU2();
391
513
  const attributesCount = reader.readU2();
392
- const attributeNames = readAttributes(reader, cp, attributesCount);
514
+ const attributeResult = readAttributes(reader, cp, attributesCount);
393
515
  members.push({
394
516
  name: readUtf8(cp, nameIndex),
395
517
  descriptor: readUtf8(cp, descriptorIndex),
396
518
  accessFlags,
397
519
  isSynthetic: (accessFlags & ACC_SYNTHETIC) !== 0 ||
398
- attributeNames.some((attributeName) => attributeName === "Synthetic")
520
+ attributeResult.names.some((attributeName) => attributeName === "Synthetic"),
521
+ ...(attributeResult.annotationDefault !== undefined
522
+ ? { annotationDefault: attributeResult.annotationDefault }
523
+ : {}),
524
+ ...(attributeResult.annotations ? { annotations: attributeResult.annotations } : {})
399
525
  });
400
526
  }
401
527
  return members;
@@ -478,6 +604,9 @@ export class MinecraftExplorerService {
478
604
  const parsedClassCache = new Map([[parsed.internalName, parsed]]);
479
605
  const warnings = [];
480
606
  const warnMissingInheritedClass = (internalName, relation) => {
607
+ if (KNOWN_ABSENT_PLATFORM_PREFIXES.some((prefix) => internalName.startsWith(prefix))) {
608
+ return;
609
+ }
481
610
  warnings.push(`Could not resolve ${relation} class "${internalName.replace(/\//g, ".")}" while expanding inherited members.`);
482
611
  };
483
612
  const readParsedClassByInternalName = async (internalName, relation) => {
@@ -572,7 +701,9 @@ export class MinecraftExplorerService {
572
701
  javaSignature: `${modifiers ? `${modifiers} ` : ""}${fieldType} ${member.name}`.trim(),
573
702
  jvmDescriptor: member.descriptor,
574
703
  accessFlags: member.accessFlags,
575
- isSynthetic: member.isSynthetic
704
+ isSynthetic: member.isSynthetic,
705
+ ...(member.annotationDefault !== undefined ? { annotationDefault: member.annotationDefault } : {}),
706
+ ...(member.annotations ? { annotations: member.annotations } : {})
576
707
  };
577
708
  }
578
709
  const parsedMethod = parseMethodDescriptor(member.descriptor);
@@ -585,7 +716,9 @@ export class MinecraftExplorerService {
585
716
  javaSignature: `${modifiers ? `${modifiers} ` : ""}${ownerSimpleClassName}(${args})`.trim(),
586
717
  jvmDescriptor: member.descriptor,
587
718
  accessFlags: member.accessFlags,
588
- isSynthetic: member.isSynthetic
719
+ isSynthetic: member.isSynthetic,
720
+ ...(member.annotationDefault !== undefined ? { annotationDefault: member.annotationDefault } : {}),
721
+ ...(member.annotations ? { annotations: member.annotations } : {})
589
722
  };
590
723
  }
591
724
  return {
@@ -594,7 +727,9 @@ export class MinecraftExplorerService {
594
727
  javaSignature: `${modifiers ? `${modifiers} ` : ""}${parsedMethod.returnType} ${member.name}(${args})`.trim(),
595
728
  jvmDescriptor: member.descriptor,
596
729
  accessFlags: member.accessFlags,
597
- isSynthetic: member.isSynthetic
730
+ isSynthetic: member.isSynthetic,
731
+ ...(member.annotationDefault !== undefined ? { annotationDefault: member.annotationDefault } : {}),
732
+ ...(member.annotations ? { annotations: member.annotations } : {})
598
733
  };
599
734
  };
600
735
  const shouldIncludeMember = (member) => {
@@ -652,10 +787,14 @@ export class MinecraftExplorerService {
652
787
  };
653
788
  }
654
789
  contextForJar(jarPath) {
790
+ const minecraftVersion = extractVersionFromPath(jarPath);
655
791
  return {
656
- minecraftVersion: extractVersionFromPath(jarPath) ?? "unknown",
792
+ minecraftVersion: minecraftVersion ?? "unknown",
657
793
  mappingType: "unknown",
658
- mappingNamespace: "obfuscated",
794
+ // Unobfuscated releases ship mojang names in their bytecode; claiming
795
+ // "obfuscated" for them misled namespace reconciliation downstream.
796
+ // With no derivable version the conservative "obfuscated" stands.
797
+ mappingNamespace: minecraftVersion && isUnobfuscatedVersion(minecraftVersion) ? "mojang" : "obfuscated",
659
798
  jarHash: artifactSignatureFromFile(jarPath).sourceArtifactId,
660
799
  generatedAt: new Date().toISOString()
661
800
  };
@@ -19,8 +19,15 @@ export interface ModAnalysisResult {
19
19
  dependencies?: ModDependency[];
20
20
  classCount: number;
21
21
  classes?: string[];
22
+ /**
23
+ * In-archive paths of bundled Jar-in-Jar nested jars (META-INF/jars scan
24
+ * plus present-and-safe fabric.mod.json "jars" declarations). Absent when
25
+ * the mod bundles none.
26
+ */
27
+ nestedJars?: string[];
22
28
  }
23
29
  export interface AnalyzeModOptions {
24
30
  includeClasses?: boolean;
25
31
  }
32
+ export declare function collectNestedJars(entries: string[], declared: string[] | undefined): string[];
26
33
  export declare function analyzeModJar(jarPath: string, options?: AnalyzeModOptions): Promise<ModAnalysisResult>;
@@ -1,7 +1,7 @@
1
1
  import { parse as parseToml } from "smol-toml";
2
2
  import { z } from "zod";
3
3
  import { AppError, ERROR_CODES } from "./errors.js";
4
- import { normalizeJarPath } from "./path-resolver.js";
4
+ import { isSecureJarEntryPath, normalizeJarPath } from "./path-resolver.js";
5
5
  import { listJarEntries, readJarEntryAsUtf8 } from "./source-jar-reader.js";
6
6
  function toErrorMessage(value) {
7
7
  if (value instanceof Error) {
@@ -32,7 +32,8 @@ const fabricModJsonSchema = z
32
32
  depends: z.record(z.union([z.string(), z.array(z.string())])).optional(),
33
33
  recommends: z.record(z.union([z.string(), z.array(z.string())])).optional(),
34
34
  conflicts: z.record(z.union([z.string(), z.array(z.string())])).optional(),
35
- suggests: z.record(z.union([z.string(), z.array(z.string())])).optional()
35
+ suggests: z.record(z.union([z.string(), z.array(z.string())])).optional(),
36
+ jars: z.array(z.object({ file: z.string() }).passthrough()).optional()
36
37
  })
37
38
  .passthrough();
38
39
  const quiltModJsonSchema = z
@@ -120,6 +121,7 @@ function parseFabricMod(content) {
120
121
  ...collectFabricDeps(mod.conflicts, "conflicts"),
121
122
  ...collectFabricDeps(mod.suggests, "optional")
122
123
  ];
124
+ const declaredNestedJars = mod.jars?.map((entry) => entry.file);
123
125
  return {
124
126
  modId: mod.id,
125
127
  modName: mod.name,
@@ -128,9 +130,25 @@ function parseFabricMod(content) {
128
130
  entrypoints,
129
131
  mixinConfigs,
130
132
  accessWidener: mod.accessWidener,
131
- dependencies: dependencies.length > 0 ? dependencies : undefined
133
+ dependencies: dependencies.length > 0 ? dependencies : undefined,
134
+ ...(declaredNestedJars && declaredNestedJars.length > 0 ? { declaredNestedJars } : {})
132
135
  };
133
136
  }
137
+ // Nested-jar inventory: every real `.jar` entry directly under META-INF/jars/
138
+ // plus declared fabric.mod.json "jars" files that actually exist in the
139
+ // archive under a safe (non-escaping) path. Declared-but-absent or escaping
140
+ // paths are dropped — the inventory never names content the archive cannot
141
+ // safely serve.
142
+ export function collectNestedJars(entries, declared) {
143
+ const present = new Set(entries);
144
+ const collected = new Set(entries.filter((entry) => /^META-INF\/jars\/[^/]+\.jar$/.test(entry)));
145
+ for (const file of declared ?? []) {
146
+ if (isSecureJarEntryPath(file) && present.has(file) && file.endsWith(".jar")) {
147
+ collected.add(file);
148
+ }
149
+ }
150
+ return [...collected].sort((left, right) => left.localeCompare(right));
151
+ }
134
152
  // ---------------------------------------------------------------------------
135
153
  // Parser: Quilt
136
154
  // ---------------------------------------------------------------------------
@@ -350,20 +368,23 @@ export async function analyzeModJar(jarPath, options) {
350
368
  // graceful fallback
351
369
  }
352
370
  }
371
+ const { declaredNestedJars, ...metadataRest } = metadata;
372
+ const nestedJars = collectNestedJars(entries, declaredNestedJars);
353
373
  return {
354
374
  loader,
355
375
  jarKind,
356
- ...metadata,
357
- ...(packagedAccessTransformers.length > 0 || metadata.accessTransformers
376
+ ...metadataRest,
377
+ ...(packagedAccessTransformers.length > 0 || metadataRest.accessTransformers
358
378
  ? {
359
379
  accessTransformers: [...new Set([
360
- ...(metadata.accessTransformers ?? []),
380
+ ...(metadataRest.accessTransformers ?? []),
361
381
  ...packagedAccessTransformers
362
382
  ])]
363
383
  }
364
384
  : {}),
365
385
  classCount,
366
- ...(classes !== undefined ? { classes } : {})
386
+ ...(classes !== undefined ? { classes } : {}),
387
+ ...(nestedJars.length > 0 ? { nestedJars } : {})
367
388
  };
368
389
  }
369
390
  //# sourceMappingURL=mod-analyzer.js.map
@@ -126,7 +126,10 @@ function validateNode(value, pointer) {
126
126
  }
127
127
  return { ok: true };
128
128
  }
129
- if (typeof node.value !== "number") {
129
+ if (typeof node.value !== "number" || !Number.isFinite(node.value)) {
130
+ // A raw non-finite number (NaN/Infinity) must use the sentinel-string form —
131
+ // it cannot survive JSON serialization (JSON.stringify turns it into null),
132
+ // so accepting it here would let a silently-corrupted document through.
130
133
  return fail(`${pointer}/value`, "number-or-non-finite-sentinel", node.value);
131
134
  }
132
135
  return { ok: true };
@@ -7,6 +7,8 @@ export type VersionSourceDiscovery = {
7
7
  candidateArtifacts: string[];
8
8
  selectedSourceJarPath?: string;
9
9
  selectedHasMinecraftNamespace?: boolean;
10
+ /** The other half of a Loom split-source pair (common/clientOnly), when present. */
11
+ companionSourceJarPaths?: string[];
10
12
  };
11
13
  type RuntimeJarCandidate = {
12
14
  jarPath: string;