@adhisang/minecraft-modding-mcp 4.2.1 → 6.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 (144) hide show
  1. package/CHANGELOG.md +69 -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/access-widener-parser.d.ts +1 -0
  6. package/dist/access-widener-parser.js +6 -3
  7. package/dist/build-suggested-call.js +31 -10
  8. package/dist/cache-registry.d.ts +8 -0
  9. package/dist/cache-registry.js +50 -32
  10. package/dist/config.d.ts +1 -0
  11. package/dist/config.js +4 -0
  12. package/dist/decompiler/vineflower.js +8 -2
  13. package/dist/entry-tools/analyze-mod-service.d.ts +24 -24
  14. package/dist/entry-tools/analyze-mod-service.js +2 -1
  15. package/dist/entry-tools/analyze-symbol-service.d.ts +57 -22
  16. package/dist/entry-tools/analyze-symbol-service.js +139 -34
  17. package/dist/entry-tools/batch-class-members-service.d.ts +3 -1
  18. package/dist/entry-tools/batch-class-members-service.js +6 -6
  19. package/dist/entry-tools/batch-class-source-service.d.ts +3 -1
  20. package/dist/entry-tools/batch-class-source-service.js +5 -6
  21. package/dist/entry-tools/batch-mappings-service.d.ts +3 -1
  22. package/dist/entry-tools/batch-mappings-service.js +4 -5
  23. package/dist/entry-tools/batch-symbol-exists-service.d.ts +3 -1
  24. package/dist/entry-tools/batch-symbol-exists-service.js +4 -5
  25. package/dist/entry-tools/compare-minecraft-service.d.ts +10 -10
  26. package/dist/entry-tools/compare-minecraft-service.js +1 -1
  27. package/dist/entry-tools/entry-tool-schema.d.ts +1 -1
  28. package/dist/entry-tools/inspect-minecraft/handlers/artifact.js +1 -3
  29. package/dist/entry-tools/inspect-minecraft/handlers/class-members.js +4 -3
  30. package/dist/entry-tools/inspect-minecraft/handlers/class-overview.js +0 -5
  31. package/dist/entry-tools/inspect-minecraft/handlers/class-source.js +0 -1
  32. package/dist/entry-tools/inspect-minecraft/handlers/file.js +26 -2
  33. package/dist/entry-tools/inspect-minecraft/handlers/list-files.js +0 -1
  34. package/dist/entry-tools/inspect-minecraft/handlers/search.d.ts +8 -0
  35. package/dist/entry-tools/inspect-minecraft/handlers/search.js +13 -2
  36. package/dist/entry-tools/inspect-minecraft/handlers/versions.d.ts +1 -1
  37. package/dist/entry-tools/inspect-minecraft/handlers/versions.js +3 -2
  38. package/dist/entry-tools/inspect-minecraft/internal.d.ts +90 -89
  39. package/dist/entry-tools/inspect-minecraft/internal.js +32 -14
  40. package/dist/entry-tools/inspect-minecraft-service.d.ts +218 -218
  41. package/dist/entry-tools/inspect-minecraft-service.js +1 -1
  42. package/dist/entry-tools/manage-cache-service.d.ts +4 -4
  43. package/dist/entry-tools/response-contract.d.ts +3 -1
  44. package/dist/entry-tools/response-contract.js +3 -2
  45. package/dist/entry-tools/validate-project/cases/mixin.js +2 -1
  46. package/dist/entry-tools/validate-project/cases/project-summary.js +5 -3
  47. package/dist/entry-tools/validate-project-service.d.ts +42 -36
  48. package/dist/entry-tools/validate-project-service.js +3 -1
  49. package/dist/entry-tools/verify-mixin-target-service.d.ts +30 -0
  50. package/dist/entry-tools/verify-mixin-target-service.js +120 -29
  51. package/dist/error-mapping.d.ts +51 -0
  52. package/dist/error-mapping.js +157 -2
  53. package/dist/index.js +147 -76
  54. package/dist/java-process.js +1 -1
  55. package/dist/json-rpc-framing.d.ts +4 -0
  56. package/dist/json-rpc-framing.js +23 -1
  57. package/dist/mapping/internal-types.d.ts +24 -0
  58. package/dist/mapping/loaders/tiny-maven.js +46 -20
  59. package/dist/mapping/lookup.d.ts +13 -2
  60. package/dist/mapping/lookup.js +60 -2
  61. package/dist/mapping/types.d.ts +6 -0
  62. package/dist/mapping-service.d.ts +26 -2
  63. package/dist/mapping-service.js +212 -55
  64. package/dist/mcp-helpers.d.ts +1 -0
  65. package/dist/mcp-helpers.js +25 -3
  66. package/dist/minecraft-explorer-service.js +24 -11
  67. package/dist/mixin/access-validators.js +33 -1
  68. package/dist/mixin/annotation-validators.js +73 -9
  69. package/dist/mixin/parsed-validator.js +3 -2
  70. package/dist/mixin-parser.js +119 -7
  71. package/dist/mod-decompile-service.d.ts +2 -0
  72. package/dist/mod-decompile-service.js +21 -3
  73. package/dist/mod-remap-service.d.ts +20 -1
  74. package/dist/mod-remap-service.js +101 -45
  75. package/dist/mojang-tiny-mapping-service.js +5 -2
  76. package/dist/nbt/java-nbt-codec.js +46 -16
  77. package/dist/nbt/json-patch.js +51 -12
  78. package/dist/nbt/typed-json.d.ts +1 -0
  79. package/dist/nbt/typed-json.js +12 -2
  80. package/dist/nbt/types.d.ts +2 -2
  81. package/dist/observability.d.ts +6 -0
  82. package/dist/observability.js +10 -0
  83. package/dist/page-cursor.d.ts +17 -0
  84. package/dist/page-cursor.js +0 -0
  85. package/dist/path-converter.js +10 -3
  86. package/dist/registry-service.d.ts +2 -0
  87. package/dist/registry-service.js +41 -2
  88. package/dist/repo-downloader.js +4 -3
  89. package/dist/resources.js +45 -9
  90. package/dist/response-utils.d.ts +29 -4
  91. package/dist/response-utils.js +95 -5
  92. package/dist/source/access-validate.js +12 -2
  93. package/dist/source/artifact-resolver.js +2 -2
  94. package/dist/source/class-source/members-builder.d.ts +44 -1
  95. package/dist/source/class-source/members-builder.js +51 -4
  96. package/dist/source/class-source/snippet-builder.d.ts +13 -0
  97. package/dist/source/class-source/snippet-builder.js +54 -4
  98. package/dist/source/class-source-helpers.d.ts +4 -4
  99. package/dist/source/class-source-helpers.js +77 -20
  100. package/dist/source/class-source.d.ts +19 -0
  101. package/dist/source/class-source.js +144 -40
  102. package/dist/source/file-access.js +3 -2
  103. package/dist/source/indexer.js +29 -9
  104. package/dist/source/lifecycle/mapping-helpers.js +28 -3
  105. package/dist/source/lifecycle/runtime-check.js +20 -6
  106. package/dist/source/search.d.ts +1 -1
  107. package/dist/source/search.js +117 -30
  108. package/dist/source/validate-mixin/pipeline/resolve.js +23 -3
  109. package/dist/source/validate-mixin.d.ts +2 -1
  110. package/dist/source/validate-mixin.js +45 -4
  111. package/dist/source/workspace-target.js +17 -10
  112. package/dist/source-jar-reader.d.ts +54 -1
  113. package/dist/source-jar-reader.js +121 -10
  114. package/dist/source-resolver.js +2 -2
  115. package/dist/source-service.d.ts +28 -7
  116. package/dist/source-service.js +9 -1
  117. package/dist/stdio-supervisor.d.ts +2 -2
  118. package/dist/stdio-supervisor.js +29 -9
  119. package/dist/storage/db.js +24 -1
  120. package/dist/storage/files-repo.d.ts +15 -0
  121. package/dist/storage/files-repo.js +67 -37
  122. package/dist/storage/symbols-repo.d.ts +15 -0
  123. package/dist/storage/symbols-repo.js +53 -19
  124. package/dist/symbols/symbol-extractor.js +35 -6
  125. package/dist/text-truncate.d.ts +7 -0
  126. package/dist/text-truncate.js +21 -0
  127. package/dist/tiny-remapper-resolver.js +10 -0
  128. package/dist/tool-contract-manifest.js +2 -2
  129. package/dist/tool-execution-gate.js +12 -1
  130. package/dist/tool-guidance.d.ts +6 -2
  131. package/dist/tool-guidance.js +63 -13
  132. package/dist/tool-schemas.d.ts +561 -496
  133. package/dist/tool-schemas.js +175 -139
  134. package/dist/types.d.ts +1 -0
  135. package/dist/version-diff-service.js +5 -0
  136. package/dist/version-service.d.ts +3 -0
  137. package/dist/version-service.js +18 -2
  138. package/dist/warning-details.d.ts +30 -0
  139. package/dist/warning-details.js +80 -0
  140. package/dist/workspace-mapping-service.js +9 -0
  141. package/docs/README-ja.md +2 -2
  142. package/docs/examples.md +26 -2
  143. package/docs/tool-reference.md +22 -17
  144. package/package.json +1 -1
@@ -30,13 +30,14 @@ export function extractDecompiledMembers(className, filePath, content) {
30
30
  const symbols = extractSymbolsFromSource(filePath, content);
31
31
  const simpleName = className.split(/[.$]/).at(-1) ?? className;
32
32
  const lines = content.split(/\r?\n/);
33
- const body = computeBraceRange(lines, symbols, simpleName);
33
+ const stripped = stripBraceNoise(lines);
34
+ const body = computeBraceRange(lines, symbols, simpleName, stripped);
34
35
  if (!body) {
35
36
  return { constructors: [], fields: [], methods: [] };
36
37
  }
37
- const depths = computeLineBraceDepths(lines);
38
+ const depths = computeLineBraceDepths(lines, stripped);
38
39
  const baseDepth = depths[body.declarationLine - 1] ?? 0;
39
- const nestedRanges = computeNestedTypeRanges(lines, symbols, body);
40
+ const nestedRanges = computeNestedTypeRanges(lines, symbols, body, stripped);
40
41
  const constructors = [];
41
42
  const fields = [];
42
43
  const methods = [];
@@ -67,17 +68,73 @@ export function extractDecompiledMembers(className, filePath, content) {
67
68
  }
68
69
  return { constructors, fields, methods };
69
70
  }
70
- export function computeLineBraceDepths(lines) {
71
+ /**
72
+ * Remove everything that must not count toward brace depth — line comments,
73
+ * (possibly multi-line) block comments, string literals, and char literals —
74
+ * in a single stateful pass so that block comments spanning lines and char
75
+ * literals like '{' / '}' never skew brace accounting. Line indices are
76
+ * preserved (one output entry per input line). Text blocks (""" … """) are not
77
+ * special-cased; they are vanishingly rare in decompiled output.
78
+ */
79
+ function stripBraceNoise(lines) {
80
+ const out = new Array(lines.length);
81
+ let inBlockComment = false;
82
+ for (let i = 0; i < lines.length; i += 1) {
83
+ const line = lines[i] ?? "";
84
+ let result = "";
85
+ let j = 0;
86
+ while (j < line.length) {
87
+ const ch = line[j];
88
+ const next = line[j + 1];
89
+ if (inBlockComment) {
90
+ if (ch === "*" && next === "/") {
91
+ inBlockComment = false;
92
+ j += 2;
93
+ }
94
+ else {
95
+ j += 1;
96
+ }
97
+ continue;
98
+ }
99
+ if (ch === "/" && next === "/") {
100
+ break; // line comment: ignore the rest of the line
101
+ }
102
+ if (ch === "/" && next === "*") {
103
+ inBlockComment = true;
104
+ j += 2;
105
+ continue;
106
+ }
107
+ if (ch === '"' || ch === "'") {
108
+ const quote = ch;
109
+ j += 1;
110
+ while (j < line.length) {
111
+ if (line[j] === "\\") {
112
+ j += 2;
113
+ continue;
114
+ }
115
+ if (line[j] === quote) {
116
+ j += 1;
117
+ break;
118
+ }
119
+ j += 1;
120
+ }
121
+ continue;
122
+ }
123
+ result += ch;
124
+ j += 1;
125
+ }
126
+ out[i] = result;
127
+ }
128
+ return out;
129
+ }
130
+ export function computeLineBraceDepths(lines, strippedLines) {
131
+ const stripped = strippedLines ?? stripBraceNoise(lines);
71
132
  const depths = new Array(lines.length).fill(0);
72
133
  let depth = 0;
73
- for (let i = 0; i < lines.length; i += 1) {
134
+ for (let i = 0; i < stripped.length; i += 1) {
74
135
  // Entry depth for this line = depth observed before any brace on it.
75
136
  depths[i] = depth;
76
- const stripped = (lines[i] ?? "")
77
- .replace(/\/\/.*/g, "")
78
- .replace(/"(?:\\.|[^"\\])*"/g, "\"\"")
79
- .replace(/'(?:\\.|[^'\\])*'/g, "''");
80
- for (const char of stripped) {
137
+ for (const char of stripped[i] ?? "") {
81
138
  if (char === "{") {
82
139
  depth += 1;
83
140
  }
@@ -88,23 +145,23 @@ export function computeLineBraceDepths(lines) {
88
145
  }
89
146
  return depths;
90
147
  }
91
- export function computeBraceRange(lines, symbols, simpleName) {
148
+ export function computeBraceRange(lines, symbols, simpleName, strippedLines) {
92
149
  const classSymbol = symbols.find((symbol) => (symbol.symbolKind === "class" || symbol.symbolKind === "interface"
93
150
  || symbol.symbolKind === "enum" || symbol.symbolKind === "record")
94
151
  && symbol.symbolName === simpleName);
95
152
  if (!classSymbol) {
96
153
  return undefined;
97
154
  }
98
- return scanBraceRange(lines, classSymbol.line);
155
+ return scanBraceRange(lines, classSymbol.line, strippedLines);
99
156
  }
100
- export function scanBraceRange(lines, declarationLine) {
157
+ export function scanBraceRange(lines, declarationLine, strippedLines) {
158
+ // Strip from the start of the file so multi-line block-comment state is correct
159
+ // by the time we reach declarationLine; indices stay aligned with `lines`.
160
+ const stripped = strippedLines ?? stripBraceNoise(lines);
101
161
  let depth = 0;
102
162
  let started = false;
103
- for (let i = declarationLine - 1; i < lines.length; i += 1) {
104
- const stripped = (lines[i] ?? "")
105
- .replace(/\/\/.*/g, "")
106
- .replace(/"(?:\\.|[^"\\])*"/g, "\"\"");
107
- for (const char of stripped) {
163
+ for (let i = declarationLine - 1; i < stripped.length; i += 1) {
164
+ for (const char of stripped[i] ?? "") {
108
165
  if (char === "{") {
109
166
  depth += 1;
110
167
  started = true;
@@ -119,7 +176,7 @@ export function scanBraceRange(lines, declarationLine) {
119
176
  }
120
177
  return { declarationLine, endLine: lines.length };
121
178
  }
122
- export function computeNestedTypeRanges(lines, symbols, outerBody) {
179
+ export function computeNestedTypeRanges(lines, symbols, outerBody, strippedLines) {
123
180
  const ranges = [];
124
181
  for (const candidate of symbols) {
125
182
  if (candidate.symbolKind !== "class" && candidate.symbolKind !== "interface"
@@ -132,7 +189,7 @@ export function computeNestedTypeRanges(lines, symbols, outerBody) {
132
189
  if (ranges.some((range) => candidate.line >= range.declarationLine && candidate.line <= range.endLine)) {
133
190
  continue;
134
191
  }
135
- const nestedRange = scanBraceRange(lines, candidate.line);
192
+ const nestedRange = scanBraceRange(lines, candidate.line, strippedLines);
136
193
  ranges.push(nestedRange);
137
194
  }
138
195
  return ranges;
@@ -2,6 +2,24 @@ import { type AppError } from "../errors.js";
2
2
  import type { SourceService } from "../source-service.js";
3
3
  import type { DecompiledFallback, FindClassInput, FindClassOutput, GetClassMembersInput, GetClassMembersOutput, GetClassSourceInput, GetClassSourceOutput } from "../source-service.js";
4
4
  import type { ArtifactProvenance, ArtifactScope, MappingSourcePriority, ResolvedSourceArtifact, SourceMapping } from "../types.js";
5
+ /**
6
+ * Unobfuscated Minecraft versions (26.1+) ship their runtime and decompiled source
7
+ * in deobfuscated (mojang) names directly — there is no real obfuscated namespace to
8
+ * map to. An artifact whose stored namespace label is "obfuscated" is therefore a
9
+ * mislabel for a mojang request: the bytes already carry mojang names.
10
+ *
11
+ * Without this reconciliation, requestedMapping="mojang" !== mappingApplied="obfuscated"
12
+ * triggers a cascade of doomed work: resolveClassNameForLookup attempts a mojang->obfuscated
13
+ * class remap, remapSignatureMembers tries to remap every member obfuscated->mojang and drops
14
+ * them all (counts.total===0), which forces a spurious decompiledFallback, disables
15
+ * memberPattern, and floods the response with one "Could not remap ..." warning per member.
16
+ *
17
+ * Collapsing mappingApplied to the requested mojang namespace makes the whole pipeline an
18
+ * identity no-op: members are returned from bytecode with correct mojang names, memberPattern
19
+ * applies, and the per-member warning flood disappears. This is the single highest-impact
20
+ * token-efficiency fix for unobfuscated versions.
21
+ */
22
+ export declare function reconcileUnobfuscatedNamespace(version: string | undefined, requestedMapping: SourceMapping, mappingApplied: SourceMapping): SourceMapping;
5
23
  export declare function resolveClassFilePath(svc: SourceService, artifactId: string, className: string): string | undefined;
6
24
  export declare function resolveClassNameForLookup(svc: SourceService, input: {
7
25
  className: string;
@@ -37,6 +55,7 @@ export declare function buildClassSourceNotFoundError(_svc: SourceService, input
37
55
  export declare function buildDecompiledFallback(svc: SourceService, artifactId: string, lookupClassName: string, memberPattern: string | undefined, maxMembers: number): {
38
56
  fallback: DecompiledFallback;
39
57
  counts: NonNullable<GetClassMembersOutput["decompiledMemberCounts"]>;
58
+ truncated: boolean;
40
59
  } | undefined;
41
60
  export declare function findClass(svc: SourceService, input: FindClassInput): FindClassOutput;
42
61
  export declare function getClassSource(svc: SourceService, input: GetClassSourceInput): Promise<GetClassSourceOutput>;
@@ -5,9 +5,37 @@ import { ERROR_CODES, createError, isAppError } from "../errors.js";
5
5
  import * as artifactResolver from "./artifact-resolver.js";
6
6
  import * as classSourceHelpers from "./class-source-helpers.js";
7
7
  import { buildClassSourceSnippet } from "./class-source/snippet-builder.js";
8
- import { remapAndCountMembers, sliceMembersWithLimit } from "./class-source/members-builder.js";
8
+ import { remapAndCountMembers, sliceMembersWithLimit, projectMembersForWire } from "./class-source/members-builder.js";
9
+ import { buildPageContextKey, encodeOffsetCursor, resolveCursorOffset } from "../page-cursor.js";
9
10
  import { dedupeQualityFlags, normalizeMapping, normalizeOptionalString, normalizePathStyle } from "./shared-utils.js";
11
+ import { isUnobfuscatedVersion } from "../version-service.js";
10
12
  const MEMBERS_STATUS_LEGACY = process.env.MEMBERS_STATUS_LEGACY === "1";
13
+ /**
14
+ * Unobfuscated Minecraft versions (26.1+) ship their runtime and decompiled source
15
+ * in deobfuscated (mojang) names directly — there is no real obfuscated namespace to
16
+ * map to. An artifact whose stored namespace label is "obfuscated" is therefore a
17
+ * mislabel for a mojang request: the bytes already carry mojang names.
18
+ *
19
+ * Without this reconciliation, requestedMapping="mojang" !== mappingApplied="obfuscated"
20
+ * triggers a cascade of doomed work: resolveClassNameForLookup attempts a mojang->obfuscated
21
+ * class remap, remapSignatureMembers tries to remap every member obfuscated->mojang and drops
22
+ * them all (counts.total===0), which forces a spurious decompiledFallback, disables
23
+ * memberPattern, and floods the response with one "Could not remap ..." warning per member.
24
+ *
25
+ * Collapsing mappingApplied to the requested mojang namespace makes the whole pipeline an
26
+ * identity no-op: members are returned from bytecode with correct mojang names, memberPattern
27
+ * applies, and the per-member warning flood disappears. This is the single highest-impact
28
+ * token-efficiency fix for unobfuscated versions.
29
+ */
30
+ export function reconcileUnobfuscatedNamespace(version, requestedMapping, mappingApplied) {
31
+ if (version &&
32
+ requestedMapping === "mojang" &&
33
+ mappingApplied === "obfuscated" &&
34
+ isUnobfuscatedVersion(version)) {
35
+ return "mojang";
36
+ }
37
+ return mappingApplied;
38
+ }
11
39
  function normalizeStrictPositiveInt(value, field) {
12
40
  if (value == null) {
13
41
  return undefined;
@@ -34,15 +62,6 @@ function normalizeMemberAccess(access) {
34
62
  details: { access }
35
63
  });
36
64
  }
37
- function buildResolveArtifactParams(target, extra = {}) {
38
- return {
39
- target: {
40
- kind: target.kind,
41
- value: target.value
42
- },
43
- ...extra
44
- };
45
- }
46
65
  function looksLikeDeobfuscatedClassName(value) {
47
66
  const trimmed = value.trim();
48
67
  if (!trimmed) {
@@ -226,7 +245,10 @@ export function buildDecompiledFallback(svc, artifactId, lookupClassName, member
226
245
  let constructors = filterByPattern(extracted.constructors);
227
246
  let fields = filterByPattern(extracted.fields);
228
247
  let methods = filterByPattern(extracted.methods);
229
- const totalBefore = constructors.length + fields.length + methods.length;
248
+ const constructorsBefore = constructors.length;
249
+ const fieldsBefore = fields.length;
250
+ const methodsBefore = methods.length;
251
+ const totalBefore = constructorsBefore + fieldsBefore + methodsBefore;
230
252
  if (totalBefore === 0) {
231
253
  return undefined;
232
254
  }
@@ -250,13 +272,17 @@ export function buildDecompiledFallback(svc, artifactId, lookupClassName, member
250
272
  origin: "source-extracted"
251
273
  },
252
274
  counts: {
253
- constructors: constructors.length,
254
- fields: fields.length,
255
- methods: methods.length,
256
- total: constructors.length + fields.length + methods.length
257
- }
275
+ constructors: constructorsBefore,
276
+ fields: fieldsBefore,
277
+ methods: methodsBefore,
278
+ total: totalBefore
279
+ },
280
+ truncated: totalBefore > maxMembers
258
281
  };
259
282
  }
283
+ // The class-like symbol kinds findClass returns. MUST stay in sync with the JS-side
284
+ // isTypeSymbol checks below; pushed down to SQL so non-type rows are never fetched.
285
+ const TYPE_SYMBOL_KINDS = ["class", "interface", "enum", "record"];
260
286
  export function findClass(svc, input) {
261
287
  const className = input.className.trim();
262
288
  if (!className) {
@@ -278,12 +304,20 @@ export function findClass(svc, input) {
278
304
  const warnings = [];
279
305
  const isQualified = className.includes(".");
280
306
  if (isQualified) {
281
- const classPath = className.replace(/\./g, "/");
307
+ // The innermost simple name (handles both dot- and $-separated inner types).
308
+ const simpleName = className.split(/[.$]/).at(-1) ?? className;
309
+ const directPath = `${classNameToClassPath(className)}.java`;
310
+ // The extractor stores ONE (outer) qualifiedName per file, so a nested type's
311
+ // FQCN never equals any stored qualifiedName and its synthetic file path
312
+ // (pkg/Outer/Inner.java) does not exist. resolveClassFilePath maps the FQCN —
313
+ // inner classes included — to the real outer file, which we match on.
314
+ const resolvedFilePath = resolveClassFilePath(svc, artifactId, className);
282
315
  const result = svc.symbolsRepo.findScopedSymbols({
283
316
  artifactId,
284
- query: className.split(".").at(-1) ?? className,
317
+ query: simpleName,
285
318
  match: "exact",
286
- limit: 5000
319
+ symbolKinds: TYPE_SYMBOL_KINDS,
320
+ limit: limit * 5
287
321
  });
288
322
  const matches = result.items
289
323
  .filter((row) => {
@@ -292,14 +326,22 @@ export function findClass(svc, input) {
292
326
  if (!isTypeSymbol)
293
327
  return false;
294
328
  const rowQualified = row.qualifiedName ?? row.filePath.replace(/\.java$/, "").replaceAll("/", ".");
295
- return rowQualified === className || row.filePath === `${classPath}.java`;
329
+ return (rowQualified === className ||
330
+ row.filePath === directPath ||
331
+ (resolvedFilePath != null && row.filePath === resolvedFilePath));
332
+ })
333
+ .map((row) => {
334
+ const rowQualified = row.qualifiedName ?? row.filePath.replace(/\.java$/, "").replaceAll("/", ".");
335
+ // For an inner-class match the stored qualifiedName is the outer type; the
336
+ // caller asked for the full nested FQCN, so report that.
337
+ const isInnerMatch = rowQualified !== className && row.filePath !== directPath;
338
+ return {
339
+ qualifiedName: isInnerMatch ? className : rowQualified,
340
+ filePath: row.filePath,
341
+ line: row.line,
342
+ symbolKind: row.symbolKind
343
+ };
296
344
  })
297
- .map((row) => ({
298
- qualifiedName: row.qualifiedName ?? row.filePath.replace(/\.java$/, "").replaceAll("/", "."),
299
- filePath: row.filePath,
300
- line: row.line,
301
- symbolKind: row.symbolKind
302
- }))
303
345
  .slice(0, limit);
304
346
  const partialVanillaLookup = hasPartialNetMinecraftCoverage(artifact.qualityFlags) && looksLikeDeobfuscatedClassName(className);
305
347
  const filteredMatches = partialVanillaLookup && matches.every((match) => !match.qualifiedName.startsWith("net.minecraft.") && !match.qualifiedName.startsWith("com.mojang."))
@@ -317,6 +359,7 @@ export function findClass(svc, input) {
317
359
  artifactId,
318
360
  query: className,
319
361
  match: "exact",
362
+ symbolKinds: TYPE_SYMBOL_KINDS,
320
363
  limit: limit * 5
321
364
  });
322
365
  const matches = [];
@@ -423,7 +466,7 @@ export async function getClassSource(svc, input) {
423
466
  const artifact = svc.getArtifact(artifactId);
424
467
  artifactId = artifact.artifactId;
425
468
  origin = artifact.origin;
426
- requestedMapping = artifact.requestedMapping ?? requestedMapping;
469
+ requestedMapping = input.mapping != null ? requestedMapping : (artifact.requestedMapping ?? requestedMapping);
427
470
  mappingApplied = artifact.mappingApplied ?? requestedMapping;
428
471
  provenance = artifact.provenance;
429
472
  qualityFlags = artifact.qualityFlags;
@@ -440,6 +483,7 @@ export async function getClassSource(svc, input) {
440
483
  preferProjectVersion: input.preferProjectVersion,
441
484
  warnings
442
485
  });
486
+ mappingApplied = reconcileUnobfuscatedNamespace(version, requestedMapping, mappingApplied);
443
487
  let activeArtifactId = artifactId;
444
488
  let activeOrigin = origin;
445
489
  let activeProvenance = provenance;
@@ -587,6 +631,27 @@ export async function getClassSource(svc, input) {
587
631
  requestedMapping,
588
632
  mappingApplied: activeMappingApplied
589
633
  });
634
+ const nextStartLine = snippet.nextStartLine;
635
+ // Continuation guidance: when output was truncated and was not redirected to
636
+ // a file, hand the caller the next line to read plus a replayable call that
637
+ // re-reads from the already-resolved artifact (no re-resolution needed).
638
+ // nextStartLine is only set for snippet/full mode, so `mode` here is never
639
+ // "metadata". The caller's original endLine window is preserved so the
640
+ // continuation never reads past the requested range.
641
+ const continuation = nextStartLine != null && !resolvedOutputFile
642
+ ? buildSuggestedCall({
643
+ tool: "get-class-source",
644
+ params: {
645
+ className,
646
+ target: { kind: "artifact", artifactId: activeArtifactId },
647
+ mode,
648
+ startLine: nextStartLine,
649
+ ...(input.endLine != null ? { endLine: input.endLine } : {}),
650
+ ...(maxLines != null ? { maxLines } : {}),
651
+ ...(input.maxChars != null ? { maxChars: input.maxChars } : {})
652
+ }
653
+ })
654
+ : undefined;
590
655
  return {
591
656
  className,
592
657
  mode,
@@ -598,6 +663,8 @@ export async function getClassSource(svc, input) {
598
663
  },
599
664
  truncated,
600
665
  ...(charsTruncated ? { charsTruncated } : {}),
666
+ ...(snippet.outOfRange ? { outOfRange: true } : {}),
667
+ ...(nextStartLine != null ? { nextStartLine } : {}),
601
668
  origin: activeOrigin,
602
669
  artifactId: activeArtifactId,
603
670
  requestedMapping,
@@ -611,6 +678,7 @@ export async function getClassSource(svc, input) {
611
678
  isDecompiled: activeOrigin === "decompiled",
612
679
  qualityFlags: activeQualityFlags
613
680
  }),
681
+ ...(continuation?.suggestedCall ? { suggestedCall: continuation.suggestedCall } : {}),
614
682
  ...(resolvedOutputFile ? { outputFile: resolvedOutputFile } : {}),
615
683
  warnings
616
684
  };
@@ -629,7 +697,7 @@ export async function getClassMembers(svc, input) {
629
697
  const includeInherited = input.includeInherited ?? false;
630
698
  const memberPattern = normalizeOptionalString(input.memberPattern);
631
699
  const parsedMaxMembers = normalizeStrictPositiveInt(input.maxMembers, "maxMembers");
632
- const maxMembers = parsedMaxMembers == null ? 500 : Math.min(parsedMaxMembers, 5000);
700
+ const maxMembers = parsedMaxMembers == null ? 150 : Math.min(parsedMaxMembers, 5000);
633
701
  const normalizedArtifactId = normalizeOptionalString(input.artifactId);
634
702
  if (normalizedArtifactId && input.target) {
635
703
  throw createError({
@@ -701,6 +769,7 @@ export async function getClassMembers(svc, input) {
701
769
  preferProjectVersion: input.preferProjectVersion,
702
770
  warnings
703
771
  });
772
+ mappingApplied = reconcileUnobfuscatedNamespace(version, requestedMapping, mappingApplied);
704
773
  if (requestedMapping !== "obfuscated" && !version) {
705
774
  throw createError({
706
775
  code: ERROR_CODES.MAPPING_NOT_APPLIED,
@@ -708,10 +777,7 @@ export async function getClassMembers(svc, input) {
708
777
  details: {
709
778
  mapping: requestedMapping,
710
779
  nextAction: "Resolve with target: { kind: \"version\", value: ... } or specify a versioned coordinate.",
711
- ...buildSuggestedCall({
712
- tool: "resolve-artifact",
713
- params: buildResolveArtifactParams({ kind: "version", value: "latest" })
714
- })
780
+ ...buildSuggestedCall({ tool: "list-versions", params: {} })
715
781
  }
716
782
  });
717
783
  }
@@ -759,7 +825,23 @@ export async function getClassMembers(svc, input) {
759
825
  }
760
826
  catch (error) {
761
827
  if (isAppError(error) && error.code === ERROR_CODES.CLASS_NOT_FOUND) {
762
- throw error;
828
+ // Re-raise with the shared recovery shape (find-class/api-matrix
829
+ // suggestedCall, namespace + scope hints) instead of the sparse bytecode
830
+ // error, so members and source agree on CLASS_NOT_FOUND guidance.
831
+ throw buildClassSourceNotFoundError(svc, {
832
+ artifactId,
833
+ className,
834
+ lookupClassName,
835
+ mappingApplied,
836
+ requestedMapping,
837
+ qualityFlags,
838
+ attemptedBinaryFallback: true,
839
+ targetKind: input.target?.kind,
840
+ targetValue: input.target && "value" in input.target ? input.target.value : undefined,
841
+ scope: input.scope,
842
+ projectPath: input.projectPath,
843
+ version
844
+ });
763
845
  }
764
846
  binaryExtractionFailed = true;
765
847
  binaryExtractionFailureReason = error instanceof Error ? error.message : String(error);
@@ -787,11 +869,29 @@ export async function getClassMembers(svc, input) {
787
869
  warnings
788
870
  });
789
871
  const counts = remapped.counts;
790
- const sliced = sliceMembersWithLimit(remapped, counts.total, maxMembers, warnings);
872
+ // Offset cursor over the flat [constructors, fields, methods] member sequence.
873
+ // The context key ties a cursor to this exact query so a stale cursor restarts.
874
+ const memberCursorContext = buildPageContextKey([
875
+ artifactId,
876
+ lookupClassName,
877
+ requestedMapping,
878
+ mappingApplied,
879
+ access,
880
+ includeSynthetic,
881
+ includeInherited,
882
+ memberPattern
883
+ ]);
884
+ const { offset: memberOffset, cursorIgnored: memberCursorIgnored } = resolveCursorOffset(input.cursor, memberCursorContext);
885
+ const sliced = sliceMembersWithLimit(remapped, counts.total, maxMembers, warnings, memberOffset);
791
886
  const constructors = sliced.constructors;
792
887
  const fields = sliced.fields;
793
888
  const methods = sliced.methods;
889
+ // Slim the wire member shape: hoist a shared ownerFqn, drop accessFlags, omit
890
+ // isSynthetic:false, and drop FIELD jvmDescriptor unless includeDescriptors.
891
+ // Internal SignatureMember arrays above stay intact.
892
+ const projectedMembers = projectMembersForWire({ constructors, fields, methods }, includeInherited, input.includeDescriptors ?? false);
794
893
  const truncated = sliced.truncated;
894
+ const nextCursor = sliced.nextOffset != null ? encodeOffsetCursor(sliced.nextOffset, memberCursorContext) : undefined;
795
895
  const normalizedProvenance = provenance ??
796
896
  buildFallbackProvenance(svc, {
797
897
  artifactId,
@@ -819,6 +919,12 @@ export async function getClassMembers(svc, input) {
819
919
  warnings.push("Bytecode member enumeration returned zero; populated decompiledFallback from decompiled source. "
820
920
  + "Descriptors and access modifiers are unavailable — use get-class-source for full details."
821
921
  + namespaceNote);
922
+ if (sourceFallback.truncated) {
923
+ const returnedTotal = decompiledFallback.constructors.length
924
+ + decompiledFallback.fields.length
925
+ + decompiledFallback.methods.length;
926
+ warnings.push(`Member list was truncated to ${returnedTotal} entries (from ${sourceFallback.counts.total}).`);
927
+ }
822
928
  if (namespaceMismatch && memberPattern) {
823
929
  warnings.push(`memberPattern="${memberPattern}" was not applied to decompiledFallback because the artifact namespace (${mappingApplied}) differs from the requested namespace (${requestedMapping}); filter the response client-side after mapping.`);
824
930
  }
@@ -843,7 +949,7 @@ export async function getClassMembers(svc, input) {
843
949
  suggestedCall = buildSuggestedCall({
844
950
  tool: "get-class-source",
845
951
  params: {
846
- target: { type: "artifact", artifactId },
952
+ target: { kind: "artifact", artifactId },
847
953
  className,
848
954
  mode: "snippet",
849
955
  mapping: requestedMapping
@@ -861,13 +967,11 @@ export async function getClassMembers(svc, input) {
861
967
  }
862
968
  return {
863
969
  className,
864
- members: {
865
- constructors,
866
- fields,
867
- methods
868
- },
970
+ members: projectedMembers,
869
971
  counts,
870
972
  truncated,
973
+ ...(nextCursor ? { nextCursor } : {}),
974
+ ...(memberCursorIgnored ? { cursorIgnored: true } : {}),
871
975
  context: signatureContext,
872
976
  origin,
873
977
  artifactId,
@@ -71,12 +71,13 @@ export async function listArtifactFiles(svc, input) {
71
71
  const artifact = svc.getArtifact(input.artifactId);
72
72
  const limit = clampLimit(input.limit, 200, 2000);
73
73
  const warnings = [];
74
+ const prefix = input.prefix === undefined ? undefined : normalizePathStyle(input.prefix);
74
75
  const page = svc.filesRepo.listFiles(artifact.artifactId, {
75
76
  limit,
76
77
  cursor: input.cursor,
77
- prefix: input.prefix
78
+ prefix
78
79
  });
79
- const normalizedPrefix = normalizeOptionalString(input.prefix);
80
+ const normalizedPrefix = normalizeOptionalString(prefix);
80
81
  if (normalizedPrefix &&
81
82
  page.items.length === 0 &&
82
83
  (normalizedPrefix.startsWith("assets/") || normalizedPrefix.startsWith("data/"))) {
@@ -43,8 +43,8 @@ export async function indexArtifact(svc, input) {
43
43
  }
44
44
  const artifact = svc.getArtifact(artifactId);
45
45
  const force = input.force ?? false;
46
- const hasFiles = svc.filesRepo.listFiles(artifact.artifactId, { limit: 1 }).items.length > 0;
47
46
  const meta = svc.indexMetaRepo.get(artifact.artifactId);
47
+ const hasFiles = meta ? meta.filesCount > 0 : false;
48
48
  const expectedSignature = artifact.artifactSignature ?? fallbackArtifactSignature(artifact.artifactId);
49
49
  const reason = resolveIndexRebuildReason({
50
50
  force,
@@ -210,17 +210,20 @@ export async function buildRebuiltArtifactData(svc, resolved) {
210
210
  }
211
211
  catch (caughtError) {
212
212
  if (isAppError(caughtError) && caughtError.code === ERROR_CODES.DECOMPILER_FAILED) {
213
+ // Decompilation failed BEFORE the artifact was ever upserted, so there is no
214
+ // queryable artifact. Deliberately omit artifactId from the error: exposing the
215
+ // would-be id led callers to pass it to find-class/get-class-*, which then failed
216
+ // with "Artifact not found. Resolve context first." (B5 state inconsistency).
217
+ const priorDetails = { ...(caughtError.details ?? {}) };
218
+ delete priorDetails.artifactId;
213
219
  throw createError({
214
220
  code: ERROR_CODES.DECOMPILER_FAILED,
215
221
  message: caughtError.message,
216
222
  details: {
217
- ...(caughtError.details ?? {}),
218
- artifactId: resolved.artifactId,
223
+ ...priorDetails,
219
224
  binaryJarPath: resolved.binaryJarPath,
220
- producedJavaCount: typeof caughtError.details?.producedJavaCount === "number"
221
- ? caughtError.details.producedJavaCount
222
- : 0,
223
- nextAction: "Verify Java runtime and Vineflower availability, then retry. If available, prefer source-backed artifacts.",
225
+ producedJavaCount: typeof priorDetails.producedJavaCount === "number" ? priorDetails.producedJavaCount : 0,
226
+ nextAction: "Decompilation failed, so no artifact was created. Verify Java runtime and Vineflower availability, then retry; prefer source-backed artifacts when available.",
224
227
  recommendedCommand: "echo $MCP_VINEFLOWER_JAR_PATH"
225
228
  }
226
229
  });
@@ -291,8 +294,11 @@ export function getArtifact(svc, artifactId) {
291
294
  }
292
295
  export async function ingestIfNeeded(svc, resolved) {
293
296
  const existing = svc.artifactsRepo.getArtifact(resolved.artifactId);
294
- const hasFiles = svc.filesRepo.listFiles(resolved.artifactId, { limit: 1 }).items.length > 0;
295
297
  const meta = svc.indexMetaRepo.get(resolved.artifactId);
298
+ // Derive hasFiles from meta instead of a separate listFiles probe: when meta is
299
+ // absent the reason is "missing_meta" regardless of hasFiles, and when present
300
+ // meta.filesCount is the authoritative count written alongside the file rows.
301
+ const hasFiles = meta ? meta.filesCount > 0 : false;
296
302
  const reason = resolveIndexRebuildReason({
297
303
  force: false,
298
304
  expectedSignature: resolved.artifactSignature,
@@ -333,6 +339,13 @@ export async function ingestIfNeeded(svc, resolved) {
333
339
  const inflight = svc.state.inflightArtifactIngests.get(resolved.artifactId);
334
340
  if (inflight) {
335
341
  await inflight;
342
+ const transformChain = resolved.provenance?.transformChain ?? [];
343
+ if (transformChain.includes("binary-remap:obf->mojang") && resolved.binaryJarPath) {
344
+ const reconciledBinaryJarPath = await maybeRemapBinaryForMojang(svc, resolved);
345
+ if (reconciledBinaryJarPath !== resolved.binaryJarPath) {
346
+ resolved.binaryJarPath = reconciledBinaryJarPath;
347
+ }
348
+ }
336
349
  return;
337
350
  }
338
351
  const ingestPromise = rebuildMissingArtifactIndex(svc, resolved, reason);
@@ -426,9 +439,16 @@ export async function maybeRemapBinaryForMojang(svc, resolved, deps = defaultBin
426
439
  if (inflight) {
427
440
  return inflight;
428
441
  }
442
+ // When a prior index-artifact persisted the remapped jar back into the
443
+ // artifacts row, binaryJarPath already equals remappedJarPath. Remapping that
444
+ // missing/corrupt path onto itself would fail permanently, so re-resolve the
445
+ // original obfuscated client jar to recover from out-of-band cache loss.
446
+ const inputJar = binaryJarPath === remappedJarPath
447
+ ? (await svc.versionService.resolveVersionJar(resolved.version)).jarPath
448
+ : binaryJarPath;
429
449
  const remapPromise = runBinaryRemapWithDeps(svc, {
430
450
  version: resolved.version,
431
- inputJar: binaryJarPath,
451
+ inputJar,
432
452
  remappedDir,
433
453
  remappedJarPath
434
454
  }, deps);