@adhisang/minecraft-modding-mcp 3.1.1 → 3.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 (41) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +20 -8
  3. package/dist/access-transformer-parser.d.ts +17 -0
  4. package/dist/access-transformer-parser.js +97 -0
  5. package/dist/concurrency.d.ts +1 -0
  6. package/dist/concurrency.js +24 -0
  7. package/dist/decompiler/vineflower.js +22 -21
  8. package/dist/entry-tools/analyze-mod-service.d.ts +4 -4
  9. package/dist/entry-tools/analyze-symbol-service.d.ts +20 -20
  10. package/dist/entry-tools/inspect-minecraft-service.d.ts +148 -148
  11. package/dist/entry-tools/validate-project-service.d.ts +153 -16
  12. package/dist/entry-tools/validate-project-service.js +360 -23
  13. package/dist/gradle-paths.d.ts +4 -0
  14. package/dist/gradle-paths.js +57 -0
  15. package/dist/index.js +65 -13
  16. package/dist/mapping-pipeline-service.d.ts +3 -1
  17. package/dist/mapping-pipeline-service.js +16 -1
  18. package/dist/mapping-service.d.ts +4 -0
  19. package/dist/mapping-service.js +155 -60
  20. package/dist/minecraft-explorer-service.d.ts +13 -0
  21. package/dist/minecraft-explorer-service.js +8 -4
  22. package/dist/mixin-validator.d.ts +33 -2
  23. package/dist/mixin-validator.js +197 -11
  24. package/dist/mod-analyzer.d.ts +1 -0
  25. package/dist/mod-analyzer.js +17 -1
  26. package/dist/mod-decompile-service.js +4 -4
  27. package/dist/mod-remap-service.js +1 -54
  28. package/dist/mod-search-service.d.ts +1 -0
  29. package/dist/mod-search-service.js +84 -51
  30. package/dist/response-utils.d.ts +35 -0
  31. package/dist/response-utils.js +113 -0
  32. package/dist/source-jar-reader.d.ts +16 -0
  33. package/dist/source-jar-reader.js +103 -1
  34. package/dist/source-resolver.js +9 -10
  35. package/dist/source-service.d.ts +22 -2
  36. package/dist/source-service.js +914 -105
  37. package/dist/tool-contract-manifest.js +8 -6
  38. package/dist/types.d.ts +17 -0
  39. package/dist/workspace-mapping-service.d.ts +13 -0
  40. package/dist/workspace-mapping-service.js +146 -14
  41. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import { ZodError, z } from "zod";
4
4
  import { CompatStdioServerTransport } from "./compat-stdio-transport.js";
5
5
  import { objectResult } from "./mcp-helpers.js";
6
6
  import { prepareToolInput } from "./tool-input.js";
7
+ import { isCompactEnabled, COMPACT_MAPPING_TOOL_NAMES, compactResponse, compactArtifactResponse, compactMappingResponse } from "./response-utils.js";
7
8
  import { loadConfig } from "./config.js";
8
9
  import { createError, ERROR_CODES, isAppError } from "./errors.js";
9
10
  import { log } from "./logger.js";
@@ -18,7 +19,7 @@ import { InspectMinecraftService, inspectMinecraftSchema, inspectMinecraftShape
18
19
  import { AnalyzeSymbolService, analyzeSymbolSchema, analyzeSymbolShape } from "./entry-tools/analyze-symbol-service.js";
19
20
  import { CompareMinecraftService, compareMinecraftSchema, compareMinecraftShape } from "./entry-tools/compare-minecraft-service.js";
20
21
  import { AnalyzeModService, analyzeModSchema, analyzeModShape } from "./entry-tools/analyze-mod-service.js";
21
- import { ValidateProjectService, validateProjectSchema, validateProjectShape, discoverWorkspaceAccessWideners, discoverWorkspaceMixins } from "./entry-tools/validate-project-service.js";
22
+ import { ValidateProjectService, validateProjectSchema, validateProjectShape, discoverWorkspaceAccessTransformers, discoverWorkspaceAccessWideners, discoverWorkspaceMixins } from "./entry-tools/validate-project-service.js";
22
23
  import { ManageCacheService, manageCacheSchema, manageCacheShape } from "./entry-tools/manage-cache-service.js";
23
24
  import { createCacheRegistry } from "./cache-registry.js";
24
25
  import { buildEntryToolMeta } from "./entry-tools/response-contract.js";
@@ -89,7 +90,7 @@ const sourceLookupTargetSchema = z.discriminatedUnion("type", [
89
90
  ]);
90
91
  const RESOLVE_ARTIFACT_TARGET_DESCRIPTION = "Object with kind and value. Example: {\"kind\":\"version\",\"value\":\"1.21.10\"}. Must be an object, not a string.";
91
92
  const SOURCE_LOOKUP_TARGET_DESCRIPTION = "Object: {\"type\":\"resolve\",\"kind\":\"version\",\"value\":\"1.21.10\"} or {\"type\":\"artifact\",\"artifactId\":\"...\"}. Must be an object, not a string.";
92
- const SOURCE_SCOPE_DESCRIPTION = 'vanilla = Mojang client jar only; merged = Loom cache discovery (default); loader = currently behaves the same as "merged".';
93
+ const SOURCE_SCOPE_DESCRIPTION = "vanilla = Mojang client jar only; merged = source-oriented merged runtime discovery; loader = loader/runtime artifact discovery when the workspace exposes transformed runtime jars.";
93
94
  const SUGGESTED_CALL_DEFAULTS = {
94
95
  allowDecompile: true,
95
96
  preferProjectVersion: false,
@@ -129,7 +130,9 @@ const resolveArtifactShape = {
129
130
  projectPath: optionalNonEmptyString.describe("Optional workspace root path for Loom cache-assisted source resolution"),
130
131
  scope: artifactScopeSchema.optional().describe(SOURCE_SCOPE_DESCRIPTION),
131
132
  preferProjectVersion: z.boolean().optional().describe("When true, detect MC version from gradle.properties and override target.value"),
132
- strictVersion: z.boolean().optional().describe("When true, reject version-approximated results instead of returning them. Default false.")
133
+ strictVersion: z.boolean().optional().describe("When true, reject version-approximated results instead of returning them. Default false."),
134
+ compact: z.boolean().default(false).describe("When true, return minimal fields (artifactId, origin, isDecompiled, version, requestedMapping, mappingApplied, qualityFlags). "
135
+ + "Omit provenance, artifactContents, sampleEntries, adjacentSourceCandidates, binaryJarPath, coordinate, repoUrl, resolvedSourceJarPath.")
133
136
  };
134
137
  const resolveArtifactSchema = z.object(resolveArtifactShape);
135
138
  const getClassSourceShape = {
@@ -250,7 +253,9 @@ const findMappingShape = {
250
253
  })
251
254
  .partial()
252
255
  .optional(),
253
- maxCandidates: optionalPositiveInt.default(200).describe("Limit returned candidates (max 200)")
256
+ maxCandidates: optionalPositiveInt.default(200).describe("Limit returned candidates (max 200)"),
257
+ compact: z.boolean().default(false).describe("When true, omit top-level empty arrays, null/undefined values, and empty objects from the response. "
258
+ + "Also omit redundant candidates array for single full-confidence exact-match resolutions.")
254
259
  };
255
260
  const findMappingSchema = z.object(findMappingShape).superRefine((value, ctx) => {
256
261
  if (value.kind === "class") {
@@ -317,7 +322,9 @@ const resolveMethodMappingExactShape = {
317
322
  sourceMapping: sourceMappingSchema.describe("obfuscated | mojang | intermediary | yarn"),
318
323
  targetMapping: sourceMappingSchema.describe("obfuscated | mojang | intermediary | yarn"),
319
324
  sourcePriority: mappingSourcePrioritySchema.optional().describe("loom-first | maven-first"),
320
- maxCandidates: optionalPositiveInt.default(200).describe("Limit returned candidates (max 200)")
325
+ maxCandidates: optionalPositiveInt.default(200).describe("Limit returned candidates (max 200)"),
326
+ compact: z.boolean().default(false).describe("When true, omit top-level empty arrays, null/undefined values, and empty objects from the response. "
327
+ + "Also omit redundant candidates array for single full-confidence exact-match resolutions.")
321
328
  };
322
329
  const resolveMethodMappingExactSchema = z
323
330
  .object(resolveMethodMappingExactShape)
@@ -368,7 +375,9 @@ const resolveWorkspaceSymbolShape = {
368
375
  descriptor: optionalNonEmptyString,
369
376
  sourceMapping: sourceMappingSchema.describe("obfuscated | mojang | intermediary | yarn"),
370
377
  sourcePriority: mappingSourcePrioritySchema.optional().describe("loom-first | maven-first"),
371
- maxCandidates: optionalPositiveInt.default(200).describe("Limit returned candidates for field/method lookups (max 200)")
378
+ maxCandidates: optionalPositiveInt.default(200).describe("Limit returned candidates for field/method lookups (max 200)"),
379
+ compact: z.boolean().default(false).describe("When true, omit top-level empty arrays, null/undefined values, and empty objects from the response. "
380
+ + "Also omit redundant candidates array for single full-confidence exact-match resolutions.")
372
381
  };
373
382
  const resolveWorkspaceSymbolSchema = z
374
383
  .object(resolveWorkspaceSymbolShape)
@@ -440,7 +449,9 @@ const checkSymbolExistsShape = {
440
449
  nameMode: classNameModeSchema.default("fqcn").describe("fqcn | auto"),
441
450
  signatureMode: z.enum(["exact", "name-only"]).default("exact")
442
451
  .describe("exact: require descriptor for methods; name-only: match by owner+name only"),
443
- maxCandidates: optionalPositiveInt.default(200).describe("Limit returned candidates (max 200)")
452
+ maxCandidates: optionalPositiveInt.default(200).describe("Limit returned candidates (max 200)"),
453
+ compact: z.boolean().default(false).describe("When true, omit top-level empty arrays, null/undefined values, and empty objects from the response. "
454
+ + "Also omit redundant candidates array for single full-confidence exact-match resolutions.")
444
455
  };
445
456
  const checkSymbolExistsSchema = z.object(checkSymbolExistsShape).superRefine((value, ctx) => {
446
457
  if (value.kind === "class") {
@@ -581,9 +592,24 @@ const validateAccessWidenerShape = {
581
592
  content: nonEmptyString.describe("Access Widener file content"),
582
593
  version: nonEmptyString.describe("Minecraft version"),
583
594
  mapping: sourceMappingSchema.optional().describe("obfuscated | mojang | intermediary | yarn"),
584
- sourcePriority: mappingSourcePrioritySchema.optional().describe("loom-first | maven-first")
595
+ sourcePriority: mappingSourcePrioritySchema.optional().describe("loom-first | maven-first"),
596
+ projectPath: optionalNonEmptyString.describe("Optional workspace root path for Loom cache-assisted runtime validation"),
597
+ scope: artifactScopeSchema.optional().describe(SOURCE_SCOPE_DESCRIPTION),
598
+ preferProjectVersion: z.boolean().default(false)
599
+ .describe("When true, detect MC version from gradle.properties and override version")
585
600
  };
586
601
  const validateAccessWidenerSchema = z.object(validateAccessWidenerShape);
602
+ const validateAccessTransformerShape = {
603
+ content: nonEmptyString.describe("Access Transformer file content"),
604
+ version: nonEmptyString.describe("Minecraft version"),
605
+ atNamespace: z.enum(["srg", "mojang", "obfuscated"]).optional().describe("srg | mojang | obfuscated"),
606
+ sourcePriority: mappingSourcePrioritySchema.optional().describe("loom-first | maven-first"),
607
+ projectPath: optionalNonEmptyString.describe("Optional workspace root path for Forge/NeoForge runtime validation"),
608
+ scope: artifactScopeSchema.optional().describe(SOURCE_SCOPE_DESCRIPTION),
609
+ preferProjectVersion: z.boolean().default(false)
610
+ .describe("When true, detect MC version from gradle.properties and override version")
611
+ };
612
+ const validateAccessTransformerSchema = z.object(validateAccessTransformerShape);
587
613
  const analyzeModJarShape = {
588
614
  jarPath: nonEmptyString.describe("Local path to the mod JAR file"),
589
615
  includeClasses: z.boolean().default(false).describe("Include full class listing")
@@ -715,8 +741,11 @@ const analyzeModService = new AnalyzeModService({
715
741
  const validateProjectService = new ValidateProjectService({
716
742
  validateMixin: (input) => sourceService.validateMixin(input),
717
743
  validateAccessWidener: (input) => sourceService.validateAccessWidener(input),
744
+ validateAccessTransformer: (input) => sourceService.validateAccessTransformer(input),
718
745
  discoverMixins: discoverWorkspaceMixins,
719
- discoverAccessWideners: discoverWorkspaceAccessWideners
746
+ discoverAccessWideners: discoverWorkspaceAccessWideners,
747
+ discoverAccessTransformers: discoverWorkspaceAccessTransformers,
748
+ detectProjectMinecraftVersion: (projectPath) => workspaceMappingService.detectProjectMinecraftVersion(projectPath)
720
749
  });
721
750
  const manageCacheService = new ManageCacheService({
722
751
  registry: createCacheRegistry({
@@ -1379,7 +1408,7 @@ function buildInvalidInputGuidance(tool, normalizedInput) {
1379
1408
  if (tool === "validate-project") {
1380
1409
  return {
1381
1410
  hints: [
1382
- "validate-project.subject must be an object with subject.kind=workspace|mixin|access-widener.",
1411
+ "validate-project.subject must be an object with subject.kind=workspace|mixin|access-widener|access-transformer.",
1383
1412
  "task=\"project-summary\" uses {\"subject\":{\"kind\":\"workspace\",\"projectPath\":\"/workspace\"}}.",
1384
1413
  "Legacy include names like projectSummary/detectedConfig/validationSummary are not accepted; use include:[\"workspace\"] only when you need discovery details."
1385
1414
  ],
@@ -1502,6 +1531,17 @@ async function runTool(tool, rawInput, schema, action) {
1502
1531
  ? heavyToolExecutionGate.run(tool, () => action(parsedInput))
1503
1532
  : action(parsedInput));
1504
1533
  const { result, warnings, meta: resultMeta } = splitWarnings(payload);
1534
+ const isCompact = isCompactEnabled(tool, parsedInput);
1535
+ let projectedResult = result;
1536
+ if (isCompact) {
1537
+ if (tool === "resolve-artifact") {
1538
+ projectedResult = compactArtifactResponse(projectedResult);
1539
+ }
1540
+ if (COMPACT_MAPPING_TOOL_NAMES.has(tool)) {
1541
+ projectedResult = compactMappingResponse(projectedResult);
1542
+ }
1543
+ projectedResult = compactResponse(projectedResult);
1544
+ }
1505
1545
  const entryMeta = ENTRY_TOOL_NAMES.has(tool)
1506
1546
  ? buildEntryToolMeta({
1507
1547
  detail: normalizedInput &&
@@ -1519,7 +1559,7 @@ async function runTool(tool, rawInput, schema, action) {
1519
1559
  })
1520
1560
  : undefined;
1521
1561
  return objectResult({
1522
- result,
1562
+ result: projectedResult,
1523
1563
  meta: {
1524
1564
  ...(entryMeta ?? {}),
1525
1565
  ...resultMeta,
@@ -1584,7 +1624,7 @@ server.tool("inspect-minecraft", "High-level v3 entry tool for version discovery
1584
1624
  server.tool("analyze-symbol", "High-level v3 entry tool for symbol existence, mapping, lifecycle, workspace analysis, and API overview.", analyzeSymbolShape, { readOnlyHint: true }, async (args) => runTool("analyze-symbol", args, analyzeSymbolSchema, async (input) => analyzeSymbolService.execute(input)));
1585
1625
  server.tool("compare-minecraft", "High-level v3 entry 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)));
1586
1626
  server.tool("analyze-mod", "High-level v3 entry 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)));
1587
- server.tool("validate-project", "High-level v3 entry tool for project summary, direct mixin validation, and access widener validation.", validateProjectShape, { readOnlyHint: true }, async (args) => runTool("validate-project", args, validateProjectSchema, async (input) => validateProjectService.execute(input)));
1627
+ server.tool("validate-project", "High-level v3 entry tool for project summary, direct mixin validation, and access widener/access transformer validation.", validateProjectShape, { readOnlyHint: true }, async (args) => runTool("validate-project", args, validateProjectSchema, async (input) => validateProjectService.execute(input)));
1588
1628
  server.tool("manage-cache", "High-level v3 entry tool for cache summaries, listing, verification, previewed mutation, and explicit apply operations.", manageCacheShape, { readOnlyHint: false }, async (args) => runTool("manage-cache", args, manageCacheSchema, async (input) => manageCacheService.execute(input)));
1589
1629
  server.tool("resolve-artifact", "Resolve source artifact from a target object ({ kind, value }) and return artifact metadata. For target.kind=jar, only <basename>-sources.jar is auto-adopted; other adjacent *-sources.jar files are informational.", resolveArtifactShape, { readOnlyHint: true }, async (args) => runTool("resolve-artifact", args, resolveArtifactSchema, async (input) => sourceService.resolveArtifact({
1590
1630
  target: input.target,
@@ -1785,7 +1825,19 @@ server.tool("validate-access-widener", "Validate Access Widener file entries aga
1785
1825
  content: input.content,
1786
1826
  version: input.version,
1787
1827
  mapping: input.mapping,
1788
- sourcePriority: input.sourcePriority
1828
+ sourcePriority: input.sourcePriority,
1829
+ projectPath: input.projectPath,
1830
+ scope: input.scope,
1831
+ preferProjectVersion: input.preferProjectVersion
1832
+ })));
1833
+ server.tool("validate-access-transformer", "Validate Access Transformer file entries against Minecraft bytecode signatures for a given version.", validateAccessTransformerShape, { readOnlyHint: true }, async (args) => runTool("validate-access-transformer", args, validateAccessTransformerSchema, async (input) => sourceService.validateAccessTransformer({
1834
+ content: input.content,
1835
+ version: input.version,
1836
+ atNamespace: input.atNamespace,
1837
+ sourcePriority: input.sourcePriority,
1838
+ projectPath: input.projectPath,
1839
+ scope: input.scope,
1840
+ preferProjectVersion: input.preferProjectVersion
1789
1841
  })));
1790
1842
  server.tool("analyze-mod-jar", "Analyze a Minecraft mod JAR to extract loader type, metadata, entrypoints, mixins, and dependencies.", analyzeModJarShape, { readOnlyHint: true }, async (args) => runTool("analyze-mod-jar", args, analyzeModJarSchema, async (input) => {
1791
1843
  const result = await analyzeModJar(input.jarPath, {
@@ -3,6 +3,7 @@ export interface MappingPipelineInput {
3
3
  requestedMapping: SourceMapping;
4
4
  target: SourceTargetInput;
5
5
  resolved: ResolvedSourceArtifact;
6
+ runtimeNamesUnobfuscated?: boolean;
6
7
  }
7
8
  export interface MappingPipelineResult {
8
9
  mappingApplied: SourceMapping;
@@ -13,6 +14,7 @@ export interface MappingPipelineResult {
13
14
  * Mapping pipeline for v0.3.
14
15
  * Current implementation enforces explicit guarantees:
15
16
  * - obfuscated: always pass-through
16
- * - mojang: requires source-backed artifact; decompile-only artifacts are rejected
17
+ * - mojang: requires source-backed artifacts on legacy obfuscated versions,
18
+ * but unobfuscated runtime jars can pass through directly
17
19
  */
18
20
  export declare function applyMappingPipeline(input: MappingPipelineInput): MappingPipelineResult;
@@ -3,7 +3,8 @@ import { createError, ERROR_CODES } from "./errors.js";
3
3
  * Mapping pipeline for v0.3.
4
4
  * Current implementation enforces explicit guarantees:
5
5
  * - obfuscated: always pass-through
6
- * - mojang: requires source-backed artifact; decompile-only artifacts are rejected
6
+ * - mojang: requires source-backed artifacts on legacy obfuscated versions,
7
+ * but unobfuscated runtime jars can pass through directly
7
8
  */
8
9
  export function applyMappingPipeline(input) {
9
10
  const transformChain = [];
@@ -22,6 +23,20 @@ export function applyMappingPipeline(input) {
22
23
  transformChain
23
24
  };
24
25
  }
26
+ if (input.requestedMapping === "mojang" && input.runtimeNamesUnobfuscated) {
27
+ transformChain.push("mapping:mojang-runtime-unobfuscated");
28
+ if (input.resolved.isDecompiled) {
29
+ qualityFlags.push("decompiled");
30
+ }
31
+ else {
32
+ qualityFlags.push("source-backed");
33
+ }
34
+ return {
35
+ mappingApplied: "mojang",
36
+ qualityFlags,
37
+ transformChain
38
+ };
39
+ }
25
40
  if (input.requestedMapping !== "mojang" &&
26
41
  input.requestedMapping !== "intermediary" &&
27
42
  input.requestedMapping !== "yarn") {
@@ -62,6 +62,7 @@ export type FindMappingInput = {
62
62
  sourceMapping: SourceMapping;
63
63
  targetMapping: SourceMapping;
64
64
  sourcePriority?: MappingSourcePriority;
65
+ projectPath?: string;
65
66
  disambiguation?: {
66
67
  ownerHint?: string;
67
68
  descriptorHint?: string;
@@ -74,6 +75,7 @@ export type EnsureMappingAvailableInput = {
74
75
  sourceMapping: SourceMapping;
75
76
  targetMapping: SourceMapping;
76
77
  sourcePriority?: MappingSourcePriority;
78
+ projectPath?: string;
77
79
  };
78
80
  export type EnsureMappingAvailableOutput = {
79
81
  transformChain: string[];
@@ -88,6 +90,7 @@ export type ResolveMethodMappingExactInput = {
88
90
  sourceMapping: SourceMapping;
89
91
  targetMapping: SourceMapping;
90
92
  sourcePriority?: MappingSourcePriority;
93
+ projectPath?: string;
91
94
  maxCandidates?: number;
92
95
  };
93
96
  export type ResolveMethodMappingExactOutput = SymbolResolutionOutput;
@@ -153,6 +156,7 @@ export declare class MappingService {
153
156
  checkSymbolExists(input: SymbolExistenceInput): Promise<SymbolExistenceOutput>;
154
157
  private mapRecordBetweenMappings;
155
158
  private mapCandidatesAlongPath;
159
+ private projectMethodDescriptorToTarget;
156
160
  private provenanceForPath;
157
161
  /**
158
162
  * Probe the mapping graph health for a given version.
@@ -3,8 +3,9 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import { dirname, join } from "node:path";
4
4
  import fastGlob from "fast-glob";
5
5
  import { createError, ERROR_CODES } from "./errors.js";
6
+ import { buildVersionSourceSearchRoots, normalizeOptionalProjectPath } from "./gradle-paths.js";
6
7
  import { defaultDownloadPath, downloadToCache } from "./repo-downloader.js";
7
- import { listJarEntries, readJarEntryAsUtf8 } from "./source-jar-reader.js";
8
+ import { collectMatchedJarEntriesAsUtf8 } from "./source-jar-reader.js";
8
9
  import { VersionService, isUnobfuscatedVersion } from "./version-service.js";
9
10
  const SUPPORTED_MAPPINGS = new Set([
10
11
  "obfuscated",
@@ -19,6 +20,7 @@ const MATCH_RANK = {
19
20
  };
20
21
  const DESCRIPTOR_FALLBACK_CONFIDENCE = 0.85;
21
22
  const MAX_CANDIDATES = 200;
23
+ const GLOB_SPECIAL_CHARS = /[\\!*+?()[\]{}@|]/g;
22
24
  function createDirectionIndex() {
23
25
  return {
24
26
  exact: new Map(),
@@ -811,13 +813,32 @@ function applyDisambiguationHints(candidates, disambiguation) {
811
813
  }
812
814
  const descriptorHint = normalizeDescriptorHint(disambiguation.descriptorHint);
813
815
  if (descriptorHint) {
814
- const descriptorMatched = filtered.filter((candidate) => candidate.descriptor === descriptorHint);
816
+ const descriptorMatched = filtered.filter((candidate) => candidate.descriptor != null && candidate.descriptor === descriptorHint);
815
817
  if (descriptorMatched.length > 0) {
816
818
  filtered = descriptorMatched;
817
819
  }
818
820
  }
819
821
  return filtered;
820
822
  }
823
+ function projectLookupCandidateDescriptor(candidate, sourceDescriptor, targetDescriptor) {
824
+ // Tiny mappings preserve method descriptors verbatim, so single-hop tiny paths often
825
+ // return the source descriptor even though the final symbol is already in the target
826
+ // namespace. Multi-hop paths that already produced a target-side descriptor are left
827
+ // unchanged by design.
828
+ if (candidate.kind !== "method" ||
829
+ !candidate.descriptor ||
830
+ !targetDescriptor ||
831
+ candidate.descriptor !== sourceDescriptor) {
832
+ return candidate;
833
+ }
834
+ return {
835
+ ...candidate,
836
+ descriptor: targetDescriptor
837
+ };
838
+ }
839
+ function effectiveLoomSearchProjectPath(projectPath) {
840
+ return normalizeOptionalProjectPath(projectPath) ?? normalizeOptionalProjectPath(process.cwd());
841
+ }
821
842
  function collectTargetRecords(graph, targetMapping) {
822
843
  return [...(graph.recordsByTarget.get(targetMapping) ?? [])];
823
844
  }
@@ -953,7 +974,7 @@ export class MappingService {
953
974
  warnings: []
954
975
  };
955
976
  }
956
- const graph = await this.loadGraph(version, priority, requiresOnlyObfuscatedMojangGraph(sourceMapping, targetMapping) ? "obfuscated-mojang-only" : "full");
977
+ const graph = await this.loadGraph(version, priority, requiresOnlyObfuscatedMojangGraph(sourceMapping, targetMapping) ? "obfuscated-mojang-only" : "full", input.projectPath);
957
978
  const path = namespacePath(graph, sourceMapping, targetMapping);
958
979
  if (!path) {
959
980
  return {
@@ -968,7 +989,15 @@ export class MappingService {
968
989
  ]
969
990
  };
970
991
  }
971
- const rawCandidates = this.mapCandidatesAlongPath(graph, path, queryRecord);
992
+ const descriptorProjection = queryRecord.kind === "method" && queryRecord.descriptor
993
+ ? this.projectMethodDescriptorToTarget(graph, path, queryRecord.descriptor)
994
+ : undefined;
995
+ const projectedDescriptor = descriptorProjection?.complete ? descriptorProjection.descriptor : undefined;
996
+ const rawCandidates = this
997
+ .mapCandidatesAlongPath(graph, path, queryRecord)
998
+ .map((candidate) => queryRecord.kind === "method" && queryRecord.descriptor
999
+ ? projectLookupCandidateDescriptor(candidate, queryRecord.descriptor, projectedDescriptor)
1000
+ : candidate);
972
1001
  const warnings = [];
973
1002
  const disambiguatedCandidates = applyDisambiguationHints(rawCandidates, input.disambiguation);
974
1003
  if (rawCandidates.length > disambiguatedCandidates.length) {
@@ -1037,7 +1066,7 @@ export class MappingService {
1037
1066
  warnings: []
1038
1067
  };
1039
1068
  }
1040
- const graph = await this.loadGraph(version, priority, requiresOnlyObfuscatedMojangGraph(sourceMapping, targetMapping) ? "obfuscated-mojang-only" : "full");
1069
+ const graph = await this.loadGraph(version, priority, requiresOnlyObfuscatedMojangGraph(sourceMapping, targetMapping) ? "obfuscated-mojang-only" : "full", input.projectPath);
1041
1070
  const path = namespacePath(graph, sourceMapping, targetMapping);
1042
1071
  if (!path) {
1043
1072
  throw createError({
@@ -1121,7 +1150,7 @@ export class MappingService {
1121
1150
  warnings: []
1122
1151
  };
1123
1152
  }
1124
- const graph = await this.loadGraph(version, priority, requiresOnlyObfuscatedMojangGraph(sourceMapping, targetMapping) ? "obfuscated-mojang-only" : "full");
1153
+ const graph = await this.loadGraph(version, priority, requiresOnlyObfuscatedMojangGraph(sourceMapping, targetMapping) ? "obfuscated-mojang-only" : "full", input.projectPath);
1125
1154
  const path = namespacePath(graph, sourceMapping, targetMapping);
1126
1155
  if (!path) {
1127
1156
  return {
@@ -1137,12 +1166,16 @@ export class MappingService {
1137
1166
  };
1138
1167
  }
1139
1168
  const warnings = [];
1169
+ const descriptorProjection = this.projectMethodDescriptorToTarget(graph, path, descriptor);
1170
+ const projectedDescriptor = descriptorProjection.complete ? descriptorProjection.descriptor : undefined;
1140
1171
  const rawCandidates = this
1141
1172
  .mapCandidatesAlongPath(graph, path, queryRecord)
1142
- .filter((candidate) => candidate.kind === "method");
1173
+ .filter((candidate) => candidate.kind === "method")
1174
+ .map((candidate) => projectLookupCandidateDescriptor(candidate, descriptor, projectedDescriptor));
1143
1175
  const candidates = rawCandidates.map(toResolutionCandidate);
1144
1176
  const limitedCandidates = limitResolutionCandidates(candidates, input.maxCandidates);
1145
- const strictCandidates = rawCandidates.filter((candidate) => candidate.descriptor === descriptor);
1177
+ const strictDescriptor = projectedDescriptor ?? descriptor;
1178
+ const strictCandidates = rawCandidates.filter((candidate) => candidate.descriptor === strictDescriptor);
1146
1179
  if (strictCandidates.length === 1) {
1147
1180
  const resolved = toResolutionCandidate(strictCandidates[0]);
1148
1181
  return {
@@ -1172,8 +1205,10 @@ export class MappingService {
1172
1205
  provenance: this.provenanceForPath(graph, path)
1173
1206
  };
1174
1207
  }
1175
- if (pathUsesSource(graph.pairs, path, "mojang-client-mappings")) {
1176
- warnings.push("Method descriptor could not be preserved through mojang-client-mappings and exact resolution is unavailable.");
1208
+ if (descriptorProjection.hadClassReferences && !descriptorProjection.complete) {
1209
+ warnings.push(pathUsesSource(graph.pairs, path, "mojang-client-mappings")
1210
+ ? "Method descriptor could not be preserved through mojang-client-mappings and exact resolution is unavailable."
1211
+ : "Method descriptor could not be fully remapped across the mapping path and exact resolution is unavailable.");
1177
1212
  return {
1178
1213
  querySymbol,
1179
1214
  mappingContext,
@@ -1624,6 +1659,33 @@ export class MappingService {
1624
1659
  descriptor: item.record.descriptor
1625
1660
  }));
1626
1661
  }
1662
+ projectMethodDescriptorToTarget(graph, path, descriptor) {
1663
+ let hadClassReferences = false;
1664
+ let complete = true;
1665
+ const classProjectionCache = new Map();
1666
+ const projectedDescriptor = descriptor.replace(/L([^;]+);/g, (fullMatch, internalName) => {
1667
+ hadClassReferences = true;
1668
+ const cached = classProjectionCache.get(internalName);
1669
+ if (cached) {
1670
+ return `L${cached};`;
1671
+ }
1672
+ const projectedClassCandidates = this
1673
+ .mapCandidatesAlongPath(graph, path, createClassSymbolRecord(internalName.replace(/\//g, ".")))
1674
+ .filter((candidate) => candidate.kind === "class");
1675
+ if (projectedClassCandidates.length !== 1) {
1676
+ complete = false;
1677
+ return fullMatch;
1678
+ }
1679
+ const projectedInternalName = projectedClassCandidates[0].symbol.replace(/\./g, "/");
1680
+ classProjectionCache.set(internalName, projectedInternalName);
1681
+ return `L${projectedInternalName};`;
1682
+ });
1683
+ return {
1684
+ descriptor: projectedDescriptor,
1685
+ hadClassReferences,
1686
+ complete
1687
+ };
1688
+ }
1627
1689
  provenanceForPath(graph, path) {
1628
1690
  if (path.length <= 1) {
1629
1691
  return undefined;
@@ -1646,6 +1708,18 @@ export class MappingService {
1646
1708
  async checkMappingHealth(input) {
1647
1709
  const priority = mappingPriorityFromInput(this.config.mappingSourcePriority, input.sourcePriority);
1648
1710
  const degradations = [];
1711
+ if (isUnobfuscatedVersion(input.version)) {
1712
+ const requestFulfillable = input.requestedMapping === "obfuscated" || input.requestedMapping === "mojang";
1713
+ if (!requestFulfillable) {
1714
+ degradations.push(`Version ${input.version} is unobfuscated; ${input.requestedMapping} mappings are not applicable.`);
1715
+ }
1716
+ return {
1717
+ mojangMappingsAvailable: true,
1718
+ tinyMappingsAvailable: false,
1719
+ memberRemapAvailable: requestFulfillable,
1720
+ degradations
1721
+ };
1722
+ }
1649
1723
  let graph;
1650
1724
  try {
1651
1725
  graph = await this.loadGraph(input.version, priority, "full");
@@ -1692,8 +1766,9 @@ export class MappingService {
1692
1766
  degradations
1693
1767
  };
1694
1768
  }
1695
- async loadGraph(version, priority, mode) {
1696
- const cacheKey = `${version}|${priority}|${mode}`;
1769
+ async loadGraph(version, priority, mode, projectPath) {
1770
+ const effectiveProjectPath = effectiveLoomSearchProjectPath(projectPath);
1771
+ const cacheKey = `${version}|${priority}|${mode}|${effectiveProjectPath ?? ""}`;
1697
1772
  const cached = this.graphCache.get(cacheKey);
1698
1773
  if (cached) {
1699
1774
  this.graphCache.delete(cacheKey);
@@ -1704,7 +1779,7 @@ export class MappingService {
1704
1779
  if (existingLock) {
1705
1780
  return existingLock;
1706
1781
  }
1707
- const buildPromise = this.buildGraph(version, priority, mode);
1782
+ const buildPromise = this.buildGraph(version, priority, mode, effectiveProjectPath);
1708
1783
  this.buildLocks.set(cacheKey, buildPromise);
1709
1784
  try {
1710
1785
  const built = await buildPromise;
@@ -1716,7 +1791,7 @@ export class MappingService {
1716
1791
  this.buildLocks.delete(cacheKey);
1717
1792
  }
1718
1793
  }
1719
- async buildGraph(version, priority, mode) {
1794
+ async buildGraph(version, priority, mode, projectPath) {
1720
1795
  if (isUnobfuscatedVersion(version)) {
1721
1796
  return {
1722
1797
  version,
@@ -1749,7 +1824,7 @@ export class MappingService {
1749
1824
  const deferredTinyWarnings = [];
1750
1825
  for (const source of mappingSourceOrder(priority)) {
1751
1826
  const tinyLoad = source === "loom-cache"
1752
- ? await this.loadTinyPairsFromLoom(version)
1827
+ ? await this.loadTinyPairsFromLoom(version, projectPath)
1753
1828
  : await this.loadTinyPairsFromMaven(version);
1754
1829
  if (tinyLoad.pairs.size === 0) {
1755
1830
  deferredTinyWarnings.push(...tinyLoad.warnings);
@@ -1847,46 +1922,67 @@ export class MappingService {
1847
1922
  };
1848
1923
  }
1849
1924
  }
1850
- async loadTinyPairsFromLoom(version) {
1851
- const patterns = [".gradle/loom-cache/**/*.tiny", ".gradle/loom-cache/**/*.tinyv2"];
1852
- const candidates = fastGlob.sync(patterns, {
1853
- cwd: process.cwd(),
1854
- absolute: true,
1855
- onlyFiles: true
1856
- });
1857
- const byVersion = candidates
1858
- .filter((p) => p.replaceAll("\\", "/").includes(`/${version}/`))
1859
- .sort((left, right) => left.localeCompare(right));
1860
- if (byVersion.length === 0) {
1861
- return {
1862
- pairs: new Map(),
1863
- warnings: [`No Loom tiny mapping files matched version "${version}".`],
1864
- mappingArtifact: "loom-cache:none"
1865
- };
1866
- }
1925
+ async loadTinyPairsFromLoom(version, projectPath) {
1926
+ const searchRoots = buildVersionSourceSearchRoots(effectiveLoomSearchProjectPath(projectPath));
1867
1927
  const merged = new Map();
1868
- for (const path of byVersion) {
1928
+ const discoveredPaths = new Set();
1929
+ for (const root of searchRoots) {
1930
+ let discovered = [];
1931
+ const versionRoot = join(root, version);
1869
1932
  try {
1870
- const content = await readFile(path, "utf8");
1871
- const parsed = parseTinyMappings(content);
1872
- for (const [key, index] of parsed.entries()) {
1873
- const existing = merged.get(key);
1874
- if (!existing) {
1875
- merged.set(key, index);
1876
- }
1877
- else {
1878
- mergeDirectionIndexes(existing, index);
1879
- }
1880
- }
1933
+ discovered = existsSync(versionRoot)
1934
+ ? await fastGlob.glob(["**/*.tiny", "**/*.tinyv2"], {
1935
+ cwd: versionRoot,
1936
+ absolute: true,
1937
+ onlyFiles: true
1938
+ })
1939
+ : await fastGlob.glob([`${version.replace(GLOB_SPECIAL_CHARS, "\\$&")}/**/*.tiny`, `${version.replace(GLOB_SPECIAL_CHARS, "\\$&")}/**/*.tinyv2`], {
1940
+ cwd: root,
1941
+ absolute: true,
1942
+ onlyFiles: true
1943
+ });
1881
1944
  }
1882
1945
  catch {
1883
- // best effort: skip unreadable or invalid files
1946
+ continue;
1884
1947
  }
1948
+ const byVersion = discovered
1949
+ .filter((path) => path.replaceAll("\\", "/").includes(`/${version}/`))
1950
+ .sort((left, right) => left.localeCompare(right));
1951
+ if (byVersion.length === 0) {
1952
+ continue;
1953
+ }
1954
+ for (const path of byVersion) {
1955
+ discoveredPaths.add(path);
1956
+ try {
1957
+ const content = await readFile(path, "utf8");
1958
+ const parsed = parseTinyMappings(content);
1959
+ for (const [key, index] of parsed.entries()) {
1960
+ const existing = merged.get(key);
1961
+ if (!existing) {
1962
+ merged.set(key, index);
1963
+ }
1964
+ else {
1965
+ mergeDirectionIndexes(existing, index);
1966
+ }
1967
+ }
1968
+ }
1969
+ catch {
1970
+ // best effort: skip unreadable or invalid files
1971
+ }
1972
+ }
1973
+ }
1974
+ const orderedPaths = [...discoveredPaths].sort((left, right) => left.localeCompare(right));
1975
+ if (orderedPaths.length > 0) {
1976
+ return {
1977
+ pairs: merged,
1978
+ warnings: [],
1979
+ mappingArtifact: orderedPaths[0]
1980
+ };
1885
1981
  }
1886
1982
  return {
1887
- pairs: merged,
1888
- warnings: [],
1889
- mappingArtifact: byVersion[0]
1983
+ pairs: new Map(),
1984
+ warnings: [`No Loom tiny mapping files matched version "${version}".`],
1985
+ mappingArtifact: "loom-cache:none"
1890
1986
  };
1891
1987
  }
1892
1988
  async loadTinyPairsFromMaven(version) {
@@ -1942,15 +2038,11 @@ export class MappingService {
1942
2038
  };
1943
2039
  }
1944
2040
  async parseTinyFromJar(jarPath) {
1945
- const entries = await listJarEntries(jarPath);
1946
- const tinyEntries = entries
1947
- .filter((entry) => entry.toLowerCase().endsWith(".tiny") || entry.toLowerCase().endsWith(".tinyv2"))
1948
- .sort((left, right) => left.localeCompare(right));
2041
+ const tinyEntries = (await collectMatchedJarEntriesAsUtf8(jarPath, (entry) => entry.toLowerCase().endsWith(".tiny") || entry.toLowerCase().endsWith(".tinyv2"), { continueOnError: true })).sort((left, right) => left.filePath.localeCompare(right.filePath));
1949
2042
  const merged = new Map();
1950
2043
  for (const entry of tinyEntries) {
1951
2044
  try {
1952
- const text = await readJarEntryAsUtf8(jarPath, entry);
1953
- const parsed = parseTinyMappings(text);
2045
+ const parsed = parseTinyMappings(entry.content);
1954
2046
  for (const [key, index] of parsed.entries()) {
1955
2047
  const existing = merged.get(key);
1956
2048
  if (!existing) {
@@ -2008,8 +2100,12 @@ export class MappingService {
2008
2100
  return;
2009
2101
  }
2010
2102
  const priority = mappingPriorityFromInput(this.config.mappingSourcePriority, sourcePriority);
2011
- this.graphCache.delete(`${normalizedVersion}|${priority}|full`);
2012
- this.graphCache.delete(`${normalizedVersion}|${priority}|obfuscated-mojang-only`);
2103
+ const prefix = `${normalizedVersion}|${priority}|`;
2104
+ for (const key of this.graphCache.keys()) {
2105
+ if (key.startsWith(prefix)) {
2106
+ this.graphCache.delete(key);
2107
+ }
2108
+ }
2013
2109
  }
2014
2110
  }
2015
2111
  // ---------------------------------------------------------------------------
@@ -2039,14 +2135,13 @@ async function fetchYarnCoordinatesStandalone(version, fetchFn = globalThis.fetc
2039
2135
  }
2040
2136
  }
2041
2137
  async function extractTinyFromJar(jarPath, outputPath) {
2042
- const entries = await listJarEntries(jarPath);
2043
- const tinyEntry = entries.find((entry) => entry === "mappings/mappings.tiny" || entry.toLowerCase().endsWith(".tiny"));
2138
+ const matchedEntries = await collectMatchedJarEntriesAsUtf8(jarPath, (entry) => entry === "mappings/mappings.tiny" || entry.toLowerCase().endsWith(".tiny"), { maxEntries: 1 });
2139
+ const tinyEntry = matchedEntries[0];
2044
2140
  if (!tinyEntry) {
2045
2141
  return false;
2046
2142
  }
2047
- const content = await readJarEntryAsUtf8(jarPath, tinyEntry);
2048
2143
  await mkdir(dirname(outputPath), { recursive: true });
2049
- await writeFile(outputPath, content, "utf8");
2144
+ await writeFile(outputPath, tinyEntry.content, "utf8");
2050
2145
  return true;
2051
2146
  }
2052
2147
  /**