@adhisang/minecraft-modding-mcp 4.2.1 → 5.0.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 (114) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +5 -4
  3. package/dist/access-transformer-parser.d.ts +2 -1
  4. package/dist/access-transformer-parser.js +10 -1
  5. package/dist/build-suggested-call.js +31 -10
  6. package/dist/cache-registry.d.ts +8 -0
  7. package/dist/cache-registry.js +15 -1
  8. package/dist/config.d.ts +1 -0
  9. package/dist/config.js +4 -0
  10. package/dist/entry-tools/analyze-mod-service.d.ts +24 -24
  11. package/dist/entry-tools/analyze-symbol-service.d.ts +57 -22
  12. package/dist/entry-tools/analyze-symbol-service.js +124 -28
  13. package/dist/entry-tools/batch-class-members-service.d.ts +3 -1
  14. package/dist/entry-tools/batch-class-members-service.js +6 -6
  15. package/dist/entry-tools/batch-class-source-service.d.ts +3 -1
  16. package/dist/entry-tools/batch-class-source-service.js +5 -6
  17. package/dist/entry-tools/batch-mappings-service.d.ts +3 -1
  18. package/dist/entry-tools/batch-mappings-service.js +4 -5
  19. package/dist/entry-tools/batch-symbol-exists-service.d.ts +3 -1
  20. package/dist/entry-tools/batch-symbol-exists-service.js +4 -5
  21. package/dist/entry-tools/compare-minecraft-service.d.ts +10 -10
  22. package/dist/entry-tools/entry-tool-schema.d.ts +1 -1
  23. package/dist/entry-tools/inspect-minecraft/handlers/artifact.js +1 -3
  24. package/dist/entry-tools/inspect-minecraft/handlers/class-members.js +2 -2
  25. package/dist/entry-tools/inspect-minecraft/handlers/class-overview.js +0 -5
  26. package/dist/entry-tools/inspect-minecraft/handlers/class-source.js +0 -1
  27. package/dist/entry-tools/inspect-minecraft/handlers/file.js +0 -1
  28. package/dist/entry-tools/inspect-minecraft/handlers/list-files.js +0 -1
  29. package/dist/entry-tools/inspect-minecraft/handlers/search.d.ts +8 -0
  30. package/dist/entry-tools/inspect-minecraft/handlers/search.js +13 -2
  31. package/dist/entry-tools/inspect-minecraft/handlers/versions.d.ts +1 -1
  32. package/dist/entry-tools/inspect-minecraft/handlers/versions.js +1 -1
  33. package/dist/entry-tools/inspect-minecraft/internal.d.ts +90 -89
  34. package/dist/entry-tools/inspect-minecraft/internal.js +30 -14
  35. package/dist/entry-tools/inspect-minecraft-service.d.ts +218 -218
  36. package/dist/entry-tools/inspect-minecraft-service.js +1 -1
  37. package/dist/entry-tools/manage-cache-service.d.ts +4 -4
  38. package/dist/entry-tools/response-contract.d.ts +1 -1
  39. package/dist/entry-tools/response-contract.js +1 -0
  40. package/dist/entry-tools/validate-project/cases/mixin.js +2 -1
  41. package/dist/entry-tools/validate-project/cases/project-summary.js +5 -3
  42. package/dist/entry-tools/validate-project-service.d.ts +42 -36
  43. package/dist/entry-tools/validate-project-service.js +3 -1
  44. package/dist/entry-tools/verify-mixin-target-service.d.ts +30 -0
  45. package/dist/entry-tools/verify-mixin-target-service.js +113 -22
  46. package/dist/error-mapping.d.ts +51 -0
  47. package/dist/error-mapping.js +157 -2
  48. package/dist/index.js +138 -71
  49. package/dist/mapping/internal-types.d.ts +7 -0
  50. package/dist/mapping/lookup.d.ts +1 -1
  51. package/dist/mapping/lookup.js +7 -1
  52. package/dist/mapping/types.d.ts +6 -0
  53. package/dist/mapping-service.d.ts +26 -2
  54. package/dist/mapping-service.js +178 -36
  55. package/dist/mcp-helpers.d.ts +1 -0
  56. package/dist/mcp-helpers.js +25 -3
  57. package/dist/minecraft-explorer-service.js +24 -11
  58. package/dist/mixin/access-validators.js +33 -1
  59. package/dist/mixin/annotation-validators.js +53 -7
  60. package/dist/mixin/parsed-validator.js +2 -2
  61. package/dist/mixin-parser.js +48 -4
  62. package/dist/mod-decompile-service.js +2 -1
  63. package/dist/mod-remap-service.d.ts +20 -1
  64. package/dist/mod-remap-service.js +95 -40
  65. package/dist/nbt/java-nbt-codec.js +6 -0
  66. package/dist/observability.d.ts +6 -0
  67. package/dist/observability.js +10 -0
  68. package/dist/page-cursor.d.ts +17 -0
  69. package/dist/page-cursor.js +0 -0
  70. package/dist/registry-service.js +25 -1
  71. package/dist/resources.js +45 -9
  72. package/dist/response-utils.d.ts +29 -4
  73. package/dist/response-utils.js +95 -5
  74. package/dist/source/access-validate.js +12 -2
  75. package/dist/source/artifact-resolver.js +1 -1
  76. package/dist/source/class-source/members-builder.d.ts +44 -1
  77. package/dist/source/class-source/members-builder.js +51 -4
  78. package/dist/source/class-source/snippet-builder.d.ts +13 -0
  79. package/dist/source/class-source/snippet-builder.js +48 -4
  80. package/dist/source/class-source-helpers.js +67 -11
  81. package/dist/source/class-source.js +97 -20
  82. package/dist/source/indexer.js +5 -2
  83. package/dist/source/search.d.ts +1 -1
  84. package/dist/source/search.js +94 -22
  85. package/dist/source/validate-mixin.d.ts +2 -1
  86. package/dist/source/validate-mixin.js +45 -4
  87. package/dist/source/workspace-target.js +15 -9
  88. package/dist/source-jar-reader.d.ts +54 -1
  89. package/dist/source-jar-reader.js +121 -10
  90. package/dist/source-service.d.ts +28 -7
  91. package/dist/storage/files-repo.d.ts +12 -0
  92. package/dist/storage/files-repo.js +24 -0
  93. package/dist/storage/symbols-repo.d.ts +6 -0
  94. package/dist/storage/symbols-repo.js +6 -0
  95. package/dist/symbols/symbol-extractor.js +35 -6
  96. package/dist/text-truncate.d.ts +7 -0
  97. package/dist/text-truncate.js +21 -0
  98. package/dist/tiny-remapper-resolver.js +10 -0
  99. package/dist/tool-contract-manifest.js +2 -2
  100. package/dist/tool-execution-gate.js +12 -1
  101. package/dist/tool-guidance.d.ts +4 -1
  102. package/dist/tool-guidance.js +37 -12
  103. package/dist/tool-schemas.d.ts +559 -494
  104. package/dist/tool-schemas.js +99 -63
  105. package/dist/types.d.ts +1 -0
  106. package/dist/version-diff-service.js +5 -0
  107. package/dist/version-service.d.ts +3 -0
  108. package/dist/version-service.js +18 -2
  109. package/dist/warning-details.d.ts +18 -0
  110. package/dist/warning-details.js +63 -0
  111. package/docs/README-ja.md +2 -2
  112. package/docs/examples.md +26 -2
  113. package/docs/tool-reference.md +22 -17
  114. package/package.json +1 -1
@@ -43,13 +43,59 @@ export function validateInjection(inj, targetMembers, targetNames, issues, resol
43
43
  });
44
44
  }
45
45
  else {
46
- resolvedMembers.push({
47
- annotation: `@${inj.annotation}`,
48
- name: methodName,
49
- line: inj.line,
50
- resolvedTo: `${targetName}#${methodName}`,
51
- status: "resolved"
52
- });
46
+ // Name matched. If the reference carries a JVM descriptor, require an
47
+ // overload to match it; otherwise a wrong-signature target (e.g. tick(D)V
48
+ // when only tick(I)V exists) is silently reported as resolved.
49
+ const refDescriptor = extractMethodDescriptor(inj.method);
50
+ const sameNameMembers = [...members.constructors, ...members.methods].filter((member) => member.name === methodName);
51
+ if (refDescriptor &&
52
+ sameNameMembers.length > 0 &&
53
+ !sameNameMembers.some((member) => member.jvmDescriptor === refDescriptor)) {
54
+ const available = [...new Set(sameNameMembers.map((member) => member.jvmDescriptor))];
55
+ const isRemapFailed = remapFailedMembers?.get(targetName)?.has(methodName);
56
+ const isSigFailed = signatureFailedTargets?.has(targetName);
57
+ // Only a healthy mapping lets us trust that the user-written descriptor is
58
+ // byte-equal to the runtime descriptor; otherwise downgrade to a warning.
59
+ const mappingDegraded = Boolean(isRemapFailed) || Boolean(isSigFailed) || healthReport?.overallHealthy === false;
60
+ const resolutionPath = isRemapFailed
61
+ ? "member-remap-failed"
62
+ : isSigFailed
63
+ ? "source-signature-unavailable"
64
+ : undefined;
65
+ const issueConfidence = mappingDegraded
66
+ ? "uncertain"
67
+ : confidence ?? "definite";
68
+ issues.push({
69
+ severity: mappingDegraded ? "warning" : "error",
70
+ kind: "descriptor-mismatch",
71
+ annotation: `@${inj.annotation}`,
72
+ target: `${targetName}#${inj.method}`,
73
+ message: `Method "${methodName}" exists in "${targetName}" but no overload matches descriptor ${refDescriptor} (available: ${available.join(", ")}).${mappingDegraded ? " (mapping degraded; may be a remap artifact)" : ""}`,
74
+ suggestions: available.length > 0 ? available : undefined,
75
+ line: inj.line,
76
+ confidence: issueConfidence,
77
+ confidenceReason: mappingDegraded
78
+ ? `Member remap/signature degraded; descriptor mismatch may be a tooling artifact, not a true signature error.`
79
+ : confidenceReason,
80
+ resolutionPath,
81
+ falsePositiveRisk: computeFalsePositiveRisk(healthReport, resolutionPath, issueConfidence)
82
+ });
83
+ resolvedMembers.push({
84
+ annotation: `@${inj.annotation}`,
85
+ name: methodName,
86
+ line: inj.line,
87
+ status: "not-found"
88
+ });
89
+ }
90
+ else {
91
+ resolvedMembers.push({
92
+ annotation: `@${inj.annotation}`,
93
+ name: methodName,
94
+ line: inj.line,
95
+ resolvedTo: `${targetName}#${methodName}`,
96
+ status: "resolved"
97
+ });
98
+ }
53
99
  }
54
100
  }
55
101
  }
@@ -209,7 +209,7 @@ export function validateParsedMixin(parsed, targetMembers, warnings, provenance,
209
209
  if (version) {
210
210
  assignSuggested(issue, "get-class-source", {
211
211
  className: issue.target,
212
- target: { type: "resolve", kind: "version", value: version },
212
+ target: { kind: "version", value: version },
213
213
  ...(mapping ? { mapping } : {}),
214
214
  mode: "metadata",
215
215
  ...classSourceContext
@@ -236,7 +236,7 @@ export function validateParsedMixin(parsed, targetMembers, warnings, provenance,
236
236
  if (version) {
237
237
  assignSuggested(issue, "get-class-source", {
238
238
  className,
239
- target: { type: "resolve", kind: "version", value: version },
239
+ target: { kind: "version", value: version },
240
240
  ...(mapping ? { mapping } : {}),
241
241
  mode: "metadata",
242
242
  ...classSourceContext
@@ -61,9 +61,38 @@ function collectMultilineAnnotation(lines, startIndex) {
61
61
  * Lines with inline annotation + declaration (e.g. `@Nullable private int x;`)
62
62
  * are treated as declaration lines (not skipped).
63
63
  */
64
+ function isUpperCaseLetter(ch) {
65
+ return ch !== ch.toLowerCase() && ch === ch.toUpperCase();
66
+ }
64
67
  function skipAnnotations(lines, startIndex) {
65
68
  let idx = startIndex;
66
- while (idx < lines.length && /^\s*@/.test(lines[idx])) {
69
+ while (idx < lines.length) {
70
+ const current = lines[idx];
71
+ // Skip blank lines and comment-only lines that sit between an annotation and the
72
+ // member declaration; otherwise the declaration line is missed and the member dropped.
73
+ if (/^\s*$/.test(current) || /^\s*\/\//.test(current)) {
74
+ idx++;
75
+ continue;
76
+ }
77
+ if (/^\s*\/\*/.test(current)) {
78
+ if (current.includes("*/")) {
79
+ const afterComment = current.slice(current.indexOf("*/") + 2).trim();
80
+ if (afterComment === "") {
81
+ idx++;
82
+ continue;
83
+ }
84
+ break; // declaration text follows an inline block comment
85
+ }
86
+ idx++;
87
+ while (idx < lines.length && !lines[idx].includes("*/")) {
88
+ idx++;
89
+ }
90
+ idx++; // move past the closing "*/" line
91
+ continue;
92
+ }
93
+ if (!/^\s*@/.test(current)) {
94
+ break;
95
+ }
67
96
  if (lines[idx].includes("(")) {
68
97
  // Check if parentheses are unbalanced (multi-line annotation) — always skip
69
98
  let depth = 0;
@@ -106,18 +135,33 @@ const INLINE_ANNOTATION_RE = /\s*@[\w$.]+(?:\([^)]*\))?\s*/g;
106
135
  function stripInlineAnnotations(line) {
107
136
  return line.replace(INLINE_ANNOTATION_RE, " ").trim();
108
137
  }
138
+ /**
139
+ * JavaBeans `Introspector.decapitalize`, which SpongePowered Mixin's AccessorInfo
140
+ * uses for inflection: leave the name unchanged when its first two characters are
141
+ * both upper case (getURL -> URL, getNBT -> NBT), otherwise lower-case the first
142
+ * character (getId -> id, getHealth -> health).
143
+ */
144
+ function decapitalize(name) {
145
+ if (name.length === 0) {
146
+ return name;
147
+ }
148
+ if (name.length > 1 && isUpperCaseLetter(name.charAt(1)) && isUpperCaseLetter(name.charAt(0))) {
149
+ return name;
150
+ }
151
+ return name.charAt(0).toLowerCase() + name.slice(1);
152
+ }
109
153
  function inferAccessorTarget(methodName) {
110
154
  const getterMatch = GETTER_PREFIX_RE.exec(methodName);
111
155
  if (getterMatch) {
112
- return getterMatch[1].charAt(0).toLowerCase() + getterMatch[1].slice(1);
156
+ return decapitalize(getterMatch[1]);
113
157
  }
114
158
  const setterMatch = SETTER_PREFIX_RE.exec(methodName);
115
159
  if (setterMatch) {
116
- return setterMatch[1].charAt(0).toLowerCase() + setterMatch[1].slice(1);
160
+ return decapitalize(setterMatch[1]);
117
161
  }
118
162
  const invokerMatch = INVOKER_PREFIX_RE.exec(methodName);
119
163
  if (invokerMatch) {
120
- return invokerMatch[1].charAt(0).toLowerCase() + invokerMatch[1].slice(1);
164
+ return decapitalize(invokerMatch[1]);
121
165
  }
122
166
  return methodName;
123
167
  }
@@ -7,6 +7,7 @@ import { decompileBinaryJar } from "./decompiler/vineflower.js";
7
7
  import { resolveVineflowerJar } from "./vineflower-resolver.js";
8
8
  import { analyzeModJar } from "./mod-analyzer.js";
9
9
  import { validateAndNormalizeJarPath } from "./path-resolver.js";
10
+ import { sliceToMaxCharsSafe } from "./text-truncate.js";
10
11
  const DECOMPILE_TIMEOUT_MS = 300_000;
11
12
  function modDecompileCacheKey(jarPath) {
12
13
  return createHash("sha256").update(jarPath).digest("hex");
@@ -122,7 +123,7 @@ export class ModDecompileService {
122
123
  }
123
124
  // Apply maxChars truncation
124
125
  if (input.maxChars != null && input.maxChars > 0 && content.length > input.maxChars) {
125
- content = content.slice(0, input.maxChars);
126
+ content = sliceToMaxCharsSafe(content, input.maxChars);
126
127
  charsTruncated = true;
127
128
  truncated = true;
128
129
  }
@@ -1,9 +1,18 @@
1
+ import { resolveTinyMappingFile } from "./mapping-service.js";
2
+ import { resolveMojangTinyFile } from "./mojang-tiny-mapping-service.js";
3
+ import { remapJar } from "./tiny-remapper-service.js";
4
+ import { resolveTinyRemapperJar } from "./tiny-remapper-resolver.js";
1
5
  import type { Config } from "./types.js";
2
6
  export interface ModRemapInput {
3
7
  inputJar: string;
4
8
  outputJar?: string;
5
9
  mcVersion?: string;
6
10
  targetMapping: "yarn" | "mojang";
11
+ /**
12
+ * Skip the remap cache and force re-resolution of the newest yarn build
13
+ * (bypasses both the output-jar cache hit and the yarn metadata TTL).
14
+ */
15
+ forceRemap?: boolean;
7
16
  }
8
17
  export interface ModRemapResult {
9
18
  outputJar: string;
@@ -14,4 +23,14 @@ export interface ModRemapResult {
14
23
  durationMs: number;
15
24
  warnings: string[];
16
25
  }
17
- export declare function remapModJar(input: ModRemapInput, config: Config): Promise<ModRemapResult>;
26
+ /**
27
+ * Injectable external dependencies, used by tests to drive the remap pipeline
28
+ * without spawning Java or downloading mapping jars. Production callers omit it.
29
+ */
30
+ export interface RemapModJarDeps {
31
+ remapJar?: typeof remapJar;
32
+ resolveTinyMappingFile?: typeof resolveTinyMappingFile;
33
+ resolveMojangTinyFile?: typeof resolveMojangTinyFile;
34
+ resolveTinyRemapperJar?: typeof resolveTinyRemapperJar;
35
+ }
36
+ export declare function remapModJar(input: ModRemapInput, config: Config, deps?: RemapModJarDeps): Promise<ModRemapResult>;
@@ -11,6 +11,13 @@ import { normalizePathForHost } from "./path-converter.js";
11
11
  import { detectFabricLikeInputNamespace } from "./source-jar-reader.js";
12
12
  import { remapJar } from "./tiny-remapper-service.js";
13
13
  import { resolveTinyRemapperJar } from "./tiny-remapper-resolver.js";
14
+ // Bumped whenever the remap pipeline changes in a way that makes previously
15
+ // cached output jars wrong. The v1 pipeline mis-mapped intermediary jars to
16
+ // mojang using an obfuscated<->mojang tiny (no intermediary namespace), so any
17
+ // cached mojang output is invalid and must not be reused. v3 adds the resolved
18
+ // mapping identity (yarn build coordinate) to the cache key so a newer yarn
19
+ // build no longer serves the stale jar; the bump invalidates v2 outputs once.
20
+ const REMAP_PIPELINE_VERSION = "v3";
14
21
  function normalizeTargetNamespace(target) {
15
22
  return target === "yarn" ? "yarn" : "mojang";
16
23
  }
@@ -58,14 +65,18 @@ function copyJarToDestination(sourceJar, destinationJar) {
58
65
  mkdirSync(dirname(destinationJar), { recursive: true });
59
66
  copyFileSync(sourceJar, destinationJar);
60
67
  }
61
- function buildCacheKey(inputJar, fromNamespace, targetNamespace, mcVersion) {
68
+ function buildCacheKey(inputJar, fromNamespace, targetNamespace, mcVersion, mappingIdentity) {
62
69
  const stat = statSync(inputJar, { throwIfNoEntry: false });
63
70
  const signature = stat ? `${stat.mtimeMs}:${stat.size}` : "unknown";
64
71
  return createHash("sha256")
65
- .update(`${inputJar}|${signature}|${fromNamespace}|${targetNamespace}|${mcVersion}`)
72
+ .update(`${REMAP_PIPELINE_VERSION}|${inputJar}|${signature}|${fromNamespace}|${targetNamespace}|${mcVersion}|${mappingIdentity}`)
66
73
  .digest("hex");
67
74
  }
68
- export async function remapModJar(input, config) {
75
+ export async function remapModJar(input, config, deps = {}) {
76
+ const remapJarFn = deps.remapJar ?? remapJar;
77
+ const resolveTinyMappingFileFn = deps.resolveTinyMappingFile ?? resolveTinyMappingFile;
78
+ const resolveMojangTinyFileFn = deps.resolveMojangTinyFile ?? resolveMojangTinyFile;
79
+ const resolveTinyRemapperJarFn = deps.resolveTinyRemapperJar ?? resolveTinyRemapperJar;
69
80
  const startedAt = Date.now();
70
81
  const warnings = [];
71
82
  // 1. Normalize input JAR path
@@ -111,12 +122,33 @@ export async function remapModJar(input, config) {
111
122
  });
112
123
  }
113
124
  const outputJar = resolveOutputJarPath(input, normalizedInput, analysis.modId, analysis.modVersion);
114
- // 4. Check cache after mapping context is known
115
- const cacheKey = buildCacheKey(normalizedInput, fromNamespace, resolvedTargetNamespace, mcVersion);
125
+ // 4. Resolve the mapping identity BEFORE the cache key so the key reflects
126
+ // which mappings produced the jar. Yarn builds are mutable (Fabric publishes
127
+ // new builds per MC version), so the resolved yarn coordinate is the identity;
128
+ // mojang is immutable per release so its version-keyed path string suffices.
129
+ // The yarn resolution is TTL-fenced (see resolveTinyMappingFile), so on the
130
+ // common warm path this is zero-network and stays off the cache-hit fast path.
131
+ let mappingIdentity;
132
+ let resolvedYarnTiny;
133
+ if (fromNamespace === resolvedTargetNamespace) {
134
+ // No remap will run (input already in target namespace); the output is a
135
+ // copy of the input, so no mapping identity is involved.
136
+ mappingIdentity = `copy:${resolvedTargetNamespace}`;
137
+ }
138
+ else if (resolvedTargetNamespace === "yarn") {
139
+ resolvedYarnTiny = await resolveTinyMappingFileFn(mcVersion, "yarn", config.cacheDir, undefined, {
140
+ forceRefresh: input.forceRemap
141
+ });
142
+ mappingIdentity = resolvedYarnTiny.coordinate ?? resolvedYarnTiny.path;
143
+ }
144
+ else {
145
+ mappingIdentity = `${mcVersion}-mojang-merged.tiny`;
146
+ }
147
+ const cacheKey = buildCacheKey(normalizedInput, fromNamespace, resolvedTargetNamespace, mcVersion, mappingIdentity);
116
148
  const cacheDir = join(config.cacheDir, "remapped-mods");
117
149
  mkdirSync(cacheDir, { recursive: true });
118
150
  const cachedOutput = join(cacheDir, `${cacheKey}.jar`);
119
- if (existsSync(cachedOutput)) {
151
+ if (!input.forceRemap && existsSync(cachedOutput)) {
120
152
  const cacheHitOutputJar = input.outputJar
121
153
  ? outputJar
122
154
  : cachedOutput;
@@ -161,51 +193,74 @@ export async function remapModJar(input, config) {
161
193
  });
162
194
  }
163
195
  // 5. Resolve tiny-remapper
164
- const tinyRemapperJar = await resolveTinyRemapperJar(config.cacheDir, config.tinyRemapperJarPath);
165
- // 6. Resolve mapping file and remap
166
- let mappingsFile;
167
- let toNamespace;
196
+ const tinyRemapperJar = await resolveTinyRemapperJarFn(config.cacheDir, config.tinyRemapperJarPath);
197
+ const passes = [];
168
198
  if (resolvedTargetNamespace === "yarn") {
169
- mappingsFile = await resolveTinyMappingFile(mcVersion, "yarn", config.cacheDir);
170
- toNamespace = "named";
199
+ // The Fabric yarn v2 tiny declares both intermediary and named, so a single
200
+ // intermediary -> named pass is sufficient.
201
+ // Reuse the coordinate resolved for the cache key (already TTL-fenced).
202
+ const yarnTiny = resolvedYarnTiny ?? await resolveTinyMappingFileFn(mcVersion, "yarn", config.cacheDir, undefined, {
203
+ forceRefresh: input.forceRemap
204
+ });
205
+ passes.push({ mappingsFile: yarnTiny.path, fromNamespace, toNamespace: "named" });
171
206
  }
172
207
  else {
173
- const mojangTiny = await resolveMojangTinyFile(mcVersion, config);
174
- mappingsFile = mojangTiny.path;
175
- toNamespace = "mojang";
208
+ // Mojang target. The merged mojang tiny only bridges obfuscated <-> mojang,
209
+ // but a Fabric/Quilt jar is intermediary-named, so a direct intermediary ->
210
+ // mojang remap finds no matching keys and silently no-ops. Remap in two passes:
211
+ // pass 1: intermediary -> official (= obfuscated) via the Fabric intermediary v2 tiny
212
+ // pass 2: obfuscated -> mojang via the merged mojang tiny
213
+ const mojangTiny = await resolveMojangTinyFileFn(mcVersion, config);
176
214
  warnings.push(...mojangTiny.warnings);
215
+ if (fromNamespace === "intermediary") {
216
+ const intermediaryTiny = await resolveTinyMappingFileFn(mcVersion, "intermediary", config.cacheDir);
217
+ passes.push({ mappingsFile: intermediaryTiny.path, fromNamespace: "intermediary", toNamespace: "official" });
218
+ passes.push({ mappingsFile: mojangTiny.path, fromNamespace: "obfuscated", toNamespace: "mojang" });
219
+ }
220
+ else {
221
+ // Defensive: an already-obfuscated input maps directly. (mojang -> mojang is
222
+ // handled earlier by the copy short-circuit, so this is not normally reached.)
223
+ passes.push({ mappingsFile: mojangTiny.path, fromNamespace: "obfuscated", toNamespace: "mojang" });
224
+ }
177
225
  }
178
226
  mkdirSync(dirname(outputJar), { recursive: true });
179
- // 8. Use temporary directory for intermediate work
227
+ // 7. Use a temporary directory for intermediate work
180
228
  const tempDir = join(tmpdir(), `mcp-remap-${cacheKey.slice(0, 12)}`);
181
229
  mkdirSync(tempDir, { recursive: true });
182
230
  try {
183
- const tempOutput = join(tempDir, "remapped.jar");
184
- try {
185
- await remapJar(tinyRemapperJar, {
186
- inputJar: normalizedInput,
187
- outputJar: tempOutput,
188
- mappingsFile,
189
- fromNamespace,
190
- toNamespace,
191
- timeoutMs: config.remapTimeoutMs,
192
- maxMemoryMb: config.remapMaxMemoryMb
193
- });
194
- }
195
- catch (caughtError) {
196
- if (isAppError(caughtError)) {
197
- throw createError({
198
- code: caughtError.code,
199
- message: caughtError.message,
200
- details: {
201
- ...(caughtError.details ?? {}),
202
- fromMapping: fromNamespace,
203
- targetMapping: input.targetMapping,
204
- resolvedTargetNamespace
205
- }
231
+ let passInput = normalizedInput;
232
+ let tempOutput = "";
233
+ for (let passIndex = 0; passIndex < passes.length; passIndex += 1) {
234
+ const pass = passes[passIndex];
235
+ tempOutput = join(tempDir, `pass-${passIndex}.jar`);
236
+ try {
237
+ await remapJarFn(tinyRemapperJar, {
238
+ inputJar: passInput,
239
+ outputJar: tempOutput,
240
+ mappingsFile: pass.mappingsFile,
241
+ fromNamespace: pass.fromNamespace,
242
+ toNamespace: pass.toNamespace,
243
+ timeoutMs: config.remapTimeoutMs,
244
+ maxMemoryMb: config.remapMaxMemoryMb
206
245
  });
207
246
  }
208
- throw caughtError;
247
+ catch (caughtError) {
248
+ if (isAppError(caughtError)) {
249
+ throw createError({
250
+ code: caughtError.code,
251
+ message: caughtError.message,
252
+ details: {
253
+ ...(caughtError.details ?? {}),
254
+ fromMapping: fromNamespace,
255
+ targetMapping: input.targetMapping,
256
+ resolvedTargetNamespace,
257
+ pass: `${pass.fromNamespace}->${pass.toNamespace}`
258
+ }
259
+ });
260
+ }
261
+ throw caughtError;
262
+ }
263
+ passInput = tempOutput;
209
264
  }
210
265
  // Copy to final destination and cache
211
266
  copyFileSync(tempOutput, outputJar);
@@ -98,6 +98,9 @@ function decodeMutf8(buffer, start, end) {
98
98
  throw parseError("Truncated MUTF-8 2-byte sequence.", { offset: i - 1 });
99
99
  }
100
100
  const b1 = buffer[i++];
101
+ if ((b1 & 0xc0) !== 0x80) {
102
+ throw parseError("Invalid MUTF-8 continuation byte.", { byte: b1, offset: i - 1 });
103
+ }
101
104
  codeUnits.push(((b0 & 0x1f) << 6) | (b1 & 0x3f));
102
105
  }
103
106
  else if ((b0 & 0xf0) === 0xe0) {
@@ -106,6 +109,9 @@ function decodeMutf8(buffer, start, end) {
106
109
  }
107
110
  const b1 = buffer[i++];
108
111
  const b2 = buffer[i++];
112
+ if ((b1 & 0xc0) !== 0x80 || (b2 & 0xc0) !== 0x80) {
113
+ throw parseError("Invalid MUTF-8 continuation byte.", { offset: i - 2 });
114
+ }
109
115
  codeUnits.push(((b0 & 0x0f) << 12) | ((b1 & 0x3f) << 6) | (b2 & 0x3f));
110
116
  }
111
117
  else {
@@ -40,6 +40,8 @@ export interface RuntimeMetricSnapshot {
40
40
  search_rows_scanned: number;
41
41
  search_rows_returned: number;
42
42
  search_indexed_zero_shortcircuit_count: number;
43
+ search_like_prefilter_count: number;
44
+ search_scan_truncated_count: number;
43
45
  reindex_count: number;
44
46
  reindex_skip_count: number;
45
47
  cache_evictions: number;
@@ -74,6 +76,8 @@ export declare class RuntimeMetrics {
74
76
  private searchRowsScanned;
75
77
  private searchRowsReturned;
76
78
  private searchIndexedZeroShortcircuitCount;
79
+ private searchLikePrefilterCount;
80
+ private searchScanTruncatedCount;
77
81
  private reindexCount;
78
82
  private reindexSkipCount;
79
83
  private cacheEvictions;
@@ -102,6 +106,8 @@ export declare class RuntimeMetrics {
102
106
  recordSearchRowsScanned(count: number): void;
103
107
  recordSearchRowsReturned(count: number): void;
104
108
  recordSearchIndexedZeroShortcircuit(): void;
109
+ recordSearchLikePrefilter(): void;
110
+ recordSearchScanTruncated(): void;
105
111
  recordReindex(): void;
106
112
  recordReindexSkip(): void;
107
113
  recordCacheEviction(count?: number): void;
@@ -26,6 +26,8 @@ export class RuntimeMetrics {
26
26
  searchRowsScanned = 0;
27
27
  searchRowsReturned = 0;
28
28
  searchIndexedZeroShortcircuitCount = 0;
29
+ searchLikePrefilterCount = 0;
30
+ searchScanTruncatedCount = 0;
29
31
  reindexCount = 0;
30
32
  reindexSkipCount = 0;
31
33
  cacheEvictions = 0;
@@ -128,6 +130,12 @@ export class RuntimeMetrics {
128
130
  recordSearchIndexedZeroShortcircuit() {
129
131
  this.searchIndexedZeroShortcircuitCount += 1;
130
132
  }
133
+ recordSearchLikePrefilter() {
134
+ this.searchLikePrefilterCount += 1;
135
+ }
136
+ recordSearchScanTruncated() {
137
+ this.searchScanTruncatedCount += 1;
138
+ }
131
139
  recordReindex() {
132
140
  this.reindexCount += 1;
133
141
  }
@@ -192,6 +200,8 @@ export class RuntimeMetrics {
192
200
  search_rows_scanned: this.searchRowsScanned,
193
201
  search_rows_returned: this.searchRowsReturned,
194
202
  search_indexed_zero_shortcircuit_count: this.searchIndexedZeroShortcircuitCount,
203
+ search_like_prefilter_count: this.searchLikePrefilterCount,
204
+ search_scan_truncated_count: this.searchScanTruncatedCount,
195
205
  reindex_count: this.reindexCount,
196
206
  reindex_skip_count: this.reindexSkipCount,
197
207
  cache_evictions: this.cacheEvictions,
@@ -0,0 +1,17 @@
1
+ export type OffsetCursor = {
2
+ offset: number;
3
+ contextKey: string;
4
+ };
5
+ /** Build a stable context key from the query parameters a cursor belongs to. */
6
+ export declare function buildPageContextKey(parts: Array<string | number | boolean | undefined>): string;
7
+ export declare function encodeOffsetCursor(offset: number, contextKey: string): string;
8
+ export declare function decodeOffsetCursor(cursor: string | undefined): OffsetCursor | undefined;
9
+ /**
10
+ * Resolve the starting offset from an incoming cursor. Returns offset 0 when the
11
+ * cursor is absent, malformed, or belongs to a different query context, and
12
+ * reports whether a provided cursor was ignored.
13
+ */
14
+ export declare function resolveCursorOffset(cursor: string | undefined, contextKey: string): {
15
+ offset: number;
16
+ cursorIgnored: boolean;
17
+ };
Binary file
@@ -82,6 +82,25 @@ function parseRegistrySnapshot(raw, version, registryFile) {
82
82
  details: { version, registryFile }
83
83
  });
84
84
  }
85
+ // Every registry value must carry an `entries` object. Without this guard a
86
+ // malformed value slips through and later throws a raw TypeError on `.entries`
87
+ // (registry-service.ts:268/305, version-diff-service.ts), bypassing the
88
+ // corrupt-snapshot regeneration path that keys off REGISTRY_GENERATION_FAILED.
89
+ for (const [registryName, value] of Object.entries(parsed)) {
90
+ const entries = value?.entries;
91
+ if (typeof value !== "object" ||
92
+ value === null ||
93
+ Array.isArray(value) ||
94
+ typeof entries !== "object" ||
95
+ entries === null ||
96
+ Array.isArray(entries)) {
97
+ throw createError({
98
+ code: ERROR_CODES.REGISTRY_GENERATION_FAILED,
99
+ message: `registries.json for version "${version}" has a malformed "${registryName}" registry (missing entries).`,
100
+ details: { version, registryFile, registryName }
101
+ });
102
+ }
103
+ }
85
104
  return parsed;
86
105
  }
87
106
  function runDataGen(serverJarPath, outputDir, version) {
@@ -252,8 +271,13 @@ export class RegistryService {
252
271
  }
253
272
  async loadRegistries(version, warnings) {
254
273
  const cached = this.registryCache.get(version);
255
- if (cached)
274
+ if (cached) {
275
+ // LRU touch: move to most-recently-used so the bound evicts the true
276
+ // coldest entry (Map preserves insertion order).
277
+ this.registryCache.delete(version);
278
+ this.registryCache.set(version, cached);
256
279
  return cached;
280
+ }
257
281
  const registryDir = join(this.config.cacheDir, "registries", version);
258
282
  const cachedRegistries = this.loadExistingRegistries(registryDir, version, warnings);
259
283
  if (cachedRegistries) {
package/dist/resources.js CHANGED
@@ -30,7 +30,7 @@ export function registerResources(server, sourceService) {
30
30
  }
31
31
  catch (e) {
32
32
  if (isAppError(e))
33
- return errorResource(uri.href, { message: e.message, code: e.code });
33
+ return errorResource(uri.href, { message: e.message, code: e.code, details: e.details });
34
34
  throw e;
35
35
  }
36
36
  });
@@ -41,7 +41,7 @@ export function registerResources(server, sourceService) {
41
41
  }
42
42
  catch (e) {
43
43
  if (isAppError(e))
44
- return errorResource(uri.href, { message: e.message, code: e.code });
44
+ return errorResource(uri.href, { message: e.message, code: e.code, details: e.details });
45
45
  throw e;
46
46
  }
47
47
  });
@@ -50,13 +50,31 @@ export function registerResources(server, sourceService) {
50
50
  try {
51
51
  const result = await sourceService.getClassSource({
52
52
  artifactId: params.artifactId,
53
- className: decodeTemplateParam(params, "className")
53
+ className: decodeTemplateParam(params, "className"),
54
+ // A "text/x-java" source resource must return real Java, not the default
55
+ // metadata outline; match the sibling class-source-json resource.
56
+ mode: "full"
54
57
  });
55
58
  return textResource(uri.href, result.sourceText);
56
59
  }
57
60
  catch (e) {
58
61
  if (isAppError(e))
59
- return errorResource(uri.href, { message: e.message, code: e.code });
62
+ return errorResource(uri.href, { message: e.message, code: e.code, details: e.details });
63
+ throw e;
64
+ }
65
+ });
66
+ server.resource("class-source-json", new ResourceTemplate("mc://source-json/{artifactId}/{className}", { list: undefined }), { description: "JSON envelope of a class's full source plus metadata (artifactId, mappingApplied, totalLines, returnedRange, provenance, warnings) — the structured alternative to the raw-text class-source resource, easier to cite and continue.", mimeType: "application/json" }, async (uri, params) => {
67
+ try {
68
+ const result = await sourceService.getClassSource({
69
+ artifactId: params.artifactId,
70
+ className: decodeTemplateParam(params, "className"),
71
+ mode: "full"
72
+ });
73
+ return objectResource(uri.href, result);
74
+ }
75
+ catch (e) {
76
+ if (isAppError(e))
77
+ return errorResource(uri.href, { message: e.message, code: e.code, details: e.details });
60
78
  throw e;
61
79
  }
62
80
  });
@@ -70,15 +88,33 @@ export function registerResources(server, sourceService) {
70
88
  }
71
89
  catch (e) {
72
90
  if (isAppError(e))
73
- return errorResource(uri.href, { message: e.message, code: e.code });
91
+ return errorResource(uri.href, { message: e.message, code: e.code, details: e.details });
92
+ throw e;
93
+ }
94
+ });
95
+ server.resource("find-mapping", new ResourceTemplate("mc://mappings/{version}/{sourceMapping}/{targetMapping}/{kind}/{name}", { list: undefined }), { description: "Look up a CLASS mapping between two naming namespaces. This URI carries no owner, so field/method lookups (which need an owner) must use the find-member-mapping resource or the find-mapping tool.", mimeType: "application/json" }, async (uri, params) => {
96
+ try {
97
+ const result = await sourceService.findMapping({
98
+ version: params.version,
99
+ kind: params.kind,
100
+ name: decodeTemplateParam(params, "name"),
101
+ sourceMapping: params.sourceMapping,
102
+ targetMapping: params.targetMapping
103
+ });
104
+ return objectResource(uri.href, result);
105
+ }
106
+ catch (e) {
107
+ if (isAppError(e))
108
+ return errorResource(uri.href, { message: e.message, code: e.code, details: e.details });
74
109
  throw e;
75
110
  }
76
111
  });
77
- server.resource("find-mapping", new ResourceTemplate("mc://mappings/{version}/{sourceMapping}/{targetMapping}/{kind}/{name}", { list: undefined }), { description: "Look up a mapping for a class, field, or method between two naming namespaces.", mimeType: "application/json" }, async (uri, params) => {
112
+ server.resource("find-member-mapping", new ResourceTemplate("mc://mappings/{version}/{sourceMapping}/{targetMapping}/{kind}/{owner}/{name}", { list: undefined }), { description: "Look up a FIELD or METHOD mapping between two naming namespaces, including the owner class the member belongs to (required for member lookups). For exact method overload resolution, use the find-mapping tool with a descriptor.", mimeType: "application/json" }, async (uri, params) => {
78
113
  try {
79
114
  const result = await sourceService.findMapping({
80
115
  version: params.version,
81
116
  kind: params.kind,
117
+ owner: decodeTemplateParam(params, "owner"),
82
118
  name: decodeTemplateParam(params, "name"),
83
119
  sourceMapping: params.sourceMapping,
84
120
  targetMapping: params.targetMapping
@@ -87,7 +123,7 @@ export function registerResources(server, sourceService) {
87
123
  }
88
124
  catch (e) {
89
125
  if (isAppError(e))
90
- return errorResource(uri.href, { message: e.message, code: e.code });
126
+ return errorResource(uri.href, { message: e.message, code: e.code, details: e.details });
91
127
  throw e;
92
128
  }
93
129
  });
@@ -101,7 +137,7 @@ export function registerResources(server, sourceService) {
101
137
  }
102
138
  catch (e) {
103
139
  if (isAppError(e))
104
- return errorResource(uri.href, { message: e.message, code: e.code });
140
+ return errorResource(uri.href, { message: e.message, code: e.code, details: e.details });
105
141
  throw e;
106
142
  }
107
143
  });
@@ -112,7 +148,7 @@ export function registerResources(server, sourceService) {
112
148
  }
113
149
  catch (e) {
114
150
  if (isAppError(e))
115
- return errorResource(uri.href, { message: e.message, code: e.code });
151
+ return errorResource(uri.href, { message: e.message, code: e.code, details: e.details });
116
152
  throw e;
117
153
  }
118
154
  });