@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
@@ -3,6 +3,15 @@ import { createError, ERROR_CODES } from "./errors.js";
3
3
  import { log } from "./logger.js";
4
4
  import { isSecureJarEntryPath } from "./path-resolver.js";
5
5
  const UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
6
+ // Test-only instrumentation: counts real jar opens at the single openZipFile
7
+ // chokepoint so perf tests can assert N-opens-per-call collapses to 1.
8
+ let zipOpenCount = 0;
9
+ export function __getZipOpenCount() {
10
+ return zipOpenCount;
11
+ }
12
+ export function __resetZipOpenCount() {
13
+ zipOpenCount = 0;
14
+ }
6
15
  function toErrorMessage(value) {
7
16
  if (value instanceof Error) {
8
17
  return value.message;
@@ -23,7 +32,7 @@ export function hasJavaSourceExtension(entryPath) {
23
32
  }
24
33
  return true;
25
34
  }
26
- function openZipFile(jarPath) {
35
+ export function openZipFile(jarPath) {
27
36
  return new Promise((resolve, reject) => {
28
37
  yauzl.open(jarPath, {
29
38
  lazyEntries: true,
@@ -33,6 +42,7 @@ function openZipFile(jarPath) {
33
42
  reject(new Error(`Failed to read jar "${jarPath}": ${toErrorMessage(error)}`));
34
43
  return;
35
44
  }
45
+ zipOpenCount += 1;
36
46
  resolve(zipFile);
37
47
  });
38
48
  });
@@ -193,6 +203,66 @@ export async function readJarEntryAsBuffer(jarPath, entryPath) {
193
203
  }
194
204
  });
195
205
  }
206
+ /**
207
+ * Opens a jar ONCE, drains its central directory into a name->entry index, and
208
+ * serves repeated entry reads via O(1) lookup + openReadStream. Use this when a
209
+ * single logical operation reads many entries from the same jar (e.g. walking a
210
+ * class hierarchy) instead of calling {@link readJarEntryAsBuffer} per entry,
211
+ * which re-opens and re-scans the jar each time. Callers MUST close() the reader
212
+ * (in a finally) to release the file descriptor. getEntryBuffer throws the same
213
+ * INVALID_INPUT (unsafe path) / SOURCE_NOT_FOUND (missing entry) errors as
214
+ * readJarEntryAsBuffer; duplicate entry names resolve to the first occurrence.
215
+ */
216
+ export async function createJarEntryReader(jarPath) {
217
+ const zipFile = await openZipFile(jarPath);
218
+ let closed = false;
219
+ const index = new Map();
220
+ try {
221
+ while (true) {
222
+ const entry = await readNextEntry(zipFile);
223
+ if (!entry) {
224
+ break;
225
+ }
226
+ if (!isSecureJarEntryPath(entry.fileName)) {
227
+ continue;
228
+ }
229
+ if (!index.has(entry.fileName)) {
230
+ index.set(entry.fileName, entry);
231
+ }
232
+ }
233
+ }
234
+ catch (error) {
235
+ zipFile.close();
236
+ throw error;
237
+ }
238
+ return {
239
+ async getEntryBuffer(entryPath) {
240
+ const normalizedTargetPath = entryPath.replaceAll("\\", "/");
241
+ if (!isSecureJarEntryPath(normalizedTargetPath)) {
242
+ throw createError({
243
+ code: ERROR_CODES.INVALID_INPUT,
244
+ message: `Entry path "${normalizedTargetPath}" is not allowed.`,
245
+ details: { jarPath, entryPath: normalizedTargetPath }
246
+ });
247
+ }
248
+ const entry = index.get(normalizedTargetPath);
249
+ if (!entry) {
250
+ throw createError({
251
+ code: ERROR_CODES.SOURCE_NOT_FOUND,
252
+ message: `Entry "${normalizedTargetPath}" was not found in "${jarPath}".`,
253
+ details: { jarPath, entryPath: normalizedTargetPath }
254
+ });
255
+ }
256
+ return readEntryStream(zipFile, entry, jarPath);
257
+ },
258
+ close() {
259
+ if (!closed) {
260
+ closed = true;
261
+ zipFile.close();
262
+ }
263
+ }
264
+ };
265
+ }
196
266
  export async function collectMatchedJarEntriesAsUtf8(jarPath, predicate, options = {}) {
197
267
  return withZipFile(jarPath, async (zipFile) => {
198
268
  const entries = [];
@@ -226,6 +296,48 @@ export async function collectMatchedJarEntriesAsUtf8(jarPath, predicate, options
226
296
  }
227
297
  });
228
298
  }
299
+ /**
300
+ * Raw-buffer counterpart of {@link collectMatchedJarEntriesAsUtf8}: opens the jar
301
+ * ONCE and returns the matched entries as raw Buffers (no UTF-8 decode), so binary
302
+ * inputs like `.class` bytes survive intact. Used to avoid re-opening the jar per
303
+ * sampled entry. Mirrors the sibling's maxBytes/maxEntries/continueOnError semantics
304
+ * and its finally-close, but takes an injectable open seam for open-count assertions.
305
+ */
306
+ export async function collectMatchedJarEntriesAsBuffers(jarPath, predicate, options = {}, deps = {}) {
307
+ const open = deps.openZipFile ?? openZipFile;
308
+ const zipFile = await open(jarPath);
309
+ try {
310
+ const entries = [];
311
+ const maxEntries = options.maxEntries == null ? undefined : Math.max(1, Math.trunc(options.maxEntries));
312
+ while (true) {
313
+ const entry = await readNextEntry(zipFile);
314
+ if (!entry) {
315
+ return entries;
316
+ }
317
+ if (!isSecureJarEntryPath(entry.fileName)) {
318
+ continue;
319
+ }
320
+ if (!predicate(entry.fileName)) {
321
+ continue;
322
+ }
323
+ try {
324
+ const contentBuffer = await readEntryStream(zipFile, entry, jarPath, options.maxBytes);
325
+ entries.push({ filePath: entry.fileName, content: contentBuffer });
326
+ if (maxEntries != null && entries.length >= maxEntries) {
327
+ return entries;
328
+ }
329
+ }
330
+ catch (error) {
331
+ if (!options.continueOnError) {
332
+ throw error;
333
+ }
334
+ }
335
+ }
336
+ }
337
+ finally {
338
+ zipFile.close();
339
+ }
340
+ }
229
341
  export async function* iterateJavaEntriesAsUtf8(jarPath, maxBytes) {
230
342
  const zipFile = await openZipFile(jarPath);
231
343
  try {
@@ -285,7 +397,7 @@ function countMatches(input, pattern) {
285
397
  }
286
398
  return count;
287
399
  }
288
- export async function detectFabricLikeInputNamespace(inputJar) {
400
+ export async function detectFabricLikeInputNamespace(inputJar, deps = {}) {
289
401
  const warnings = [];
290
402
  const classEntries = (await listJarEntries(inputJar))
291
403
  .filter((entry) => entry.endsWith(".class"))
@@ -297,16 +409,15 @@ export async function detectFabricLikeInputNamespace(inputJar) {
297
409
  warnings
298
410
  };
299
411
  }
412
+ // Read all sampled class entries in a SINGLE jar open instead of re-opening the
413
+ // jar once per entry. The sample set and latin1 decode are unchanged, so scores —
414
+ // and therefore the detected namespace — are identical.
415
+ const sampleSet = new Set(classEntries);
416
+ const matched = await collectMatchedJarEntriesAsBuffers(inputJar, (name) => sampleSet.has(name), { continueOnError: true }, deps);
300
417
  let mojangScore = 0;
301
418
  let intermediaryScore = 0;
302
- for (const entry of classEntries) {
303
- let text = "";
304
- try {
305
- text = (await readJarEntryAsBuffer(inputJar, entry)).toString("latin1");
306
- }
307
- catch {
308
- continue;
309
- }
419
+ for (const { content } of matched) {
420
+ const text = content.toString("latin1");
310
421
  mojangScore += countMatches(text, /net\/minecraft\/(?:advancements|client|commands|core|data|gametest|nbt|network|recipe|resources|server|sounds|stats|tags|util|world)\//g) * 3;
311
422
  intermediaryScore += countMatches(text, /net\/minecraft\/class_\d+/g) * 3;
312
423
  intermediaryScore += countMatches(text, /\b(?:method|field)_\d+\b/g);
@@ -11,6 +11,7 @@ import * as indexer from "./source/indexer.js";
11
11
  import * as workspaceTarget from "./source/workspace-target.js";
12
12
  import * as validateMixinModule from "./source/validate-mixin.js";
13
13
  import * as artifactResolver from "./source/artifact-resolver.js";
14
+ import type { WireMembersBlock } from "./source/class-source/members-builder.js";
14
15
  import { type StageEmitter } from "./stage-emitter.js";
15
16
  import { WorkspaceMappingService, type WorkspaceCompileMappingOutput } from "./workspace-mapping-service.js";
16
17
  import { type WorkspaceContextCache } from "./workspace-context-cache.js";
@@ -30,7 +31,8 @@ export type ResolveArtifactInput = {
30
31
  scope?: ArtifactScope;
31
32
  preferProjectVersion?: boolean;
32
33
  strictVersion?: boolean;
33
- compact?: boolean;
34
+ /** When true, compute the optional `sampleEntries` listing (detail=full / include=samples). */
35
+ includeSampleEntries?: boolean;
34
36
  };
35
37
  export type ResolveArtifactOutput = {
36
38
  artifactId: string;
@@ -112,6 +114,8 @@ export type SearchClassSourceInput = {
112
114
  export type SearchClassSourceOutput = {
113
115
  hits: SearchSourceHit[];
114
116
  nextCursor?: string;
117
+ /** True when a provided cursor was malformed or belonged to a different query and was ignored (results restart from page one). */
118
+ cursorIgnored?: boolean;
115
119
  mappingApplied: SourceMapping;
116
120
  returnedNamespace: SourceMapping;
117
121
  artifactContents: ArtifactContentsSummary;
@@ -205,6 +209,10 @@ export type GetClassSourceOutput = {
205
209
  };
206
210
  truncated: boolean;
207
211
  charsTruncated?: boolean;
212
+ /** True when startLine begins past EOF; the returned range is empty (not clamped). */
213
+ outOfRange?: boolean;
214
+ /** First not-yet-returned line when truncated; pass back as startLine to continue. */
215
+ nextStartLine?: number;
208
216
  origin: ResolvedSourceArtifact["origin"];
209
217
  artifactId: string;
210
218
  requestedMapping: SourceMapping;
@@ -213,6 +221,11 @@ export type GetClassSourceOutput = {
213
221
  provenance: ArtifactProvenance;
214
222
  qualityFlags: string[];
215
223
  artifactContents: ArtifactContentsSummary;
224
+ /** Replayable get-class-source call that reads from nextStartLine (truncated, non-file output only). */
225
+ suggestedCall?: {
226
+ tool: string;
227
+ params: Record<string, unknown>;
228
+ };
216
229
  outputFile?: string;
217
230
  warnings: string[];
218
231
  };
@@ -245,11 +258,14 @@ export type GetClassMembersInput = {
245
258
  includeInherited?: boolean;
246
259
  memberPattern?: string;
247
260
  maxMembers?: number;
261
+ cursor?: string;
248
262
  projectPath?: string;
249
263
  gradleUserHome?: string;
250
264
  scope?: ArtifactScope;
251
265
  preferProjectVersion?: boolean;
252
266
  strictVersion?: boolean;
267
+ /** When true, keep jvmDescriptor on FIELD members (always kept on methods/constructors). */
268
+ includeDescriptors?: boolean;
253
269
  };
254
270
  export type DecompiledMember = {
255
271
  name: string;
@@ -265,11 +281,7 @@ export type DecompiledFallback = {
265
281
  export type GetClassMembersStatus = "ok" | "members_unavailable" | "partial";
266
282
  export type GetClassMembersOutput = {
267
283
  className: string;
268
- members: {
269
- constructors: SignatureMember[];
270
- fields: SignatureMember[];
271
- methods: SignatureMember[];
272
- };
284
+ members: WireMembersBlock;
273
285
  counts: {
274
286
  constructors: number;
275
287
  fields: number;
@@ -277,6 +289,10 @@ export type GetClassMembersOutput = {
277
289
  total: number;
278
290
  };
279
291
  truncated: boolean;
292
+ /** Continuation cursor when more members remain; pass back as cursor to fetch the next page. */
293
+ nextCursor?: string;
294
+ /** True when a provided cursor was malformed or belonged to a different query and was ignored. */
295
+ cursorIgnored?: boolean;
280
296
  context: ExplorerResponseContext;
281
297
  origin: ResolvedSourceArtifact["origin"];
282
298
  artifactId: string;
@@ -420,7 +436,12 @@ export type ValidateMixinInput = {
420
436
  path: string;
421
437
  };
422
438
  sourceRoots?: string[];
423
- version: string;
439
+ /**
440
+ * Optional when input.mode='project', or preferProjectVersion=true with a
441
+ * projectPath set: the dispatcher detects the Minecraft version from
442
+ * gradle.properties before any version-dependent stage runs.
443
+ */
444
+ version?: string;
424
445
  mapping?: SourceMapping;
425
446
  sourcePriority?: MappingSourcePriority;
426
447
  scope?: ArtifactScope;
@@ -47,6 +47,7 @@ export declare class FilesRepo {
47
47
  private readonly listStmt;
48
48
  private readonly listRowsStmt;
49
49
  private readonly searchPathStmt;
50
+ private readonly searchContentLikePathsStmt;
50
51
  private readonly searchFtsStmt;
51
52
  private readonly getByPathsStmtCache;
52
53
  private readonly classLookupStmtCache;
@@ -58,6 +59,17 @@ export declare class FilesRepo {
58
59
  getFileContent(artifactId: string, filePath: string): FileRow | undefined;
59
60
  listFiles(artifactId: string, options: ListFilesOptions): PagedResult<string>;
60
61
  listFileRows(artifactId: string, options: ListFileRowsOptions): PagedResult<FileRow>;
62
+ /**
63
+ * Narrow the literal/contains text-scan candidate set without hydrating content:
64
+ * returns the file_paths whose content matches `content LIKE %needle%` (ASCII
65
+ * case-insensitive). The result is a SUPERSET of the JS contains/exact match set
66
+ * for an ASCII needle, so callers MUST still run the exact JS post-verify. Only
67
+ * file_path is selected — content stays in the DB until getFileContentsByPaths.
68
+ */
69
+ searchContentLikeCandidatePaths(artifactId: string, needle: string, limit: number): {
70
+ filePaths: string[];
71
+ scannedRows: number;
72
+ };
61
73
  getFileContentsByPaths(artifactId: string, filePaths: string[]): FileRow[];
62
74
  searchFileCandidates(artifactId: string, options: SearchFilesOptions): PagedResult<SearchFileCandidateResult> & {
63
75
  scannedRows: number;
@@ -121,6 +121,10 @@ function buildIndexedMatchQuery(query, match) {
121
121
  return token;
122
122
  }).join(" ");
123
123
  }
124
+ /** Escape LIKE wildcards so the needle is matched literally under ESCAPE '\'. */
125
+ function escapeLikeNeedle(value) {
126
+ return value.replace(/[\\%_]/g, "\\$&");
127
+ }
124
128
  export class FilesRepo {
125
129
  db;
126
130
  deleteStmt;
@@ -129,6 +133,7 @@ export class FilesRepo {
129
133
  listStmt;
130
134
  listRowsStmt;
131
135
  searchPathStmt;
136
+ searchContentLikePathsStmt;
132
137
  searchFtsStmt;
133
138
  getByPathsStmtCache = new Map();
134
139
  classLookupStmtCache = new Map();
@@ -167,6 +172,13 @@ export class FilesRepo {
167
172
  WHERE artifact_id = ? AND file_path LIKE ? ESCAPE '\\'
168
173
  ORDER BY file_path ASC
169
174
  LIMIT ?
175
+ `);
176
+ this.searchContentLikePathsStmt = this.db.prepare(`
177
+ SELECT file_path
178
+ FROM files
179
+ WHERE artifact_id = ? AND content LIKE ? ESCAPE '\\'
180
+ ORDER BY file_path ASC
181
+ LIMIT ?
170
182
  `);
171
183
  this.searchFtsStmt = this.db.prepare(`
172
184
  SELECT file_path, rank
@@ -239,6 +251,18 @@ export class FilesRepo {
239
251
  nextCursor: nextCursorFromRows(rows)
240
252
  };
241
253
  }
254
+ /**
255
+ * Narrow the literal/contains text-scan candidate set without hydrating content:
256
+ * returns the file_paths whose content matches `content LIKE %needle%` (ASCII
257
+ * case-insensitive). The result is a SUPERSET of the JS contains/exact match set
258
+ * for an ASCII needle, so callers MUST still run the exact JS post-verify. Only
259
+ * file_path is selected — content stays in the DB until getFileContentsByPaths.
260
+ */
261
+ searchContentLikeCandidatePaths(artifactId, needle, limit) {
262
+ const pattern = `%${escapeLikeNeedle(needle)}%`;
263
+ const rows = this.searchContentLikePathsStmt.all(artifactId, pattern, Math.max(1, limit));
264
+ return { filePaths: rows.map((row) => row.file_path), scannedRows: rows.length };
265
+ }
242
266
  getFileContentsByPaths(artifactId, filePaths) {
243
267
  if (filePaths.length === 0) {
244
268
  return [];
@@ -22,6 +22,12 @@ export interface FindScopedSymbolsOptions {
22
22
  query: string;
23
23
  match: ScopedSymbolMatch;
24
24
  symbolKind?: string;
25
+ /**
26
+ * Restrict results to these symbol kinds via a bound `symbol_kind IN (...)` clause.
27
+ * Combined with the singular `symbolKind` it ANDs (both apply). An empty array is
28
+ * ignored (degrades to no kind filter) to avoid emitting an invalid `IN ()`.
29
+ */
30
+ symbolKinds?: string[];
25
31
  packagePrefix?: string;
26
32
  filePathLike?: string;
27
33
  limit?: number;
@@ -260,6 +260,12 @@ export class SymbolsRepo {
260
260
  where.push("symbol_kind = ?");
261
261
  params.push(options.symbolKind);
262
262
  }
263
+ const symbolKinds = options.symbolKinds;
264
+ if (symbolKinds && symbolKinds.length > 0) {
265
+ const placeholders = symbolKinds.map(() => "?").join(", ");
266
+ where.push(`symbol_kind IN (${placeholders})`);
267
+ params.push(...symbolKinds);
268
+ }
263
269
  if (options.packagePrefix?.trim()) {
264
270
  const normalizedPrefix = options.packagePrefix.replace(/\.+/g, "/").replace(/\/+$/, "");
265
271
  where.push("file_path LIKE ? ESCAPE '\\'");
@@ -1,6 +1,26 @@
1
- const CLASS_DECLARATION = /^(?:\s*@[\w.]+\s+)*(?:\s*(?:public|private|protected|abstract|final|sealed|non-sealed|static)\s+)*\s*(class|interface|enum|record)\s+([A-Za-z_$][\w$]*)/;
2
- const METHOD_DECLARATION = /^(?:\s*@[\w.]+\s+)*[^{;]*?\b([A-Za-z_$][\w$]*)\s*\([^)]*\)\s*(?:\{|;)/;
3
- const FIELD_DECLARATION = /^(?:\s*@[\w.]+\s+)*[^\s][\w<>\[\],.?]+\s+([A-Za-z_$][\w$]*)\s*(?:=|;|,)/;
1
+ // A type token may carry spaced generics (Map<String, Integer>), arrays (int[][]),
2
+ // wildcards (List<? extends Foo>), and FQNs (java.util.Map). It must not contain "=".
3
+ const TYPE_TOKEN = "[\\w.$][\\w.$<>\\[\\],?\\s]*";
4
+ const MODIFIER = "(?:public|private|protected|abstract|final|static|native|synchronized|default|strictfp|transient|volatile)";
5
+ const MODIFIER_OR_ANNOTATION_RUN = `(?:(?:@[\\w.]+|${MODIFIER})\\s+)*`;
6
+ const TYPE_PARAMS = "(?:<[^>]+>\\s*)?";
7
+ const THROWS_CLAUSE = "(?:\\s+throws\\s+[\\w.$,\\s]+)?";
8
+ const CLASS_DECLARATION = new RegExp(`^${MODIFIER_OR_ANNOTATION_RUN}(class|interface|enum|record)\\s+([A-Za-z_$][\\w$]*)`);
9
+ // Method/constructor with a body: return type is optional (constructors have none); ends with "{".
10
+ const METHOD_BLOCK_DECLARATION = new RegExp(`^${MODIFIER_OR_ANNOTATION_RUN}${TYPE_PARAMS}(?:${TYPE_TOKEN}\\s+)?([A-Za-z_$][\\w$]*)\\s*\\([^)]*\\)${THROWS_CLAUSE}\\s*\\{`);
11
+ // Abstract/interface method declaration: a return type is REQUIRED; ends with ";".
12
+ // Requiring the return type is what separates "void onTick();" from a bare call "doThing();".
13
+ const METHOD_ABSTRACT_DECLARATION = new RegExp(`^${MODIFIER_OR_ANNOTATION_RUN}${TYPE_PARAMS}${TYPE_TOKEN}\\s+([A-Za-z_$][\\w$]*)\\s*\\([^)]*\\)${THROWS_CLAUSE}\\s*;`);
14
+ const FIELD_DECLARATION = new RegExp(`^${MODIFIER_OR_ANNOTATION_RUN}${TYPE_TOKEN}\\s+([A-Za-z_$][\\w$]*)\\s*(?:=|;|,)`);
15
+ // Lines that begin with one of these keywords are statements, not declarations. They are
16
+ // skipped wholesale so that "return helper();" / "for (...)" / "new Foo() {" never produce
17
+ // phantom members. "default"/"synchronized" are intentionally excluded (they double as
18
+ // member modifiers); the method regexes reject their statement forms structurally.
19
+ const STATEMENT_LINE_KEYWORDS = new Set([
20
+ "if", "for", "while", "switch", "catch", "return", "throw", "else", "do",
21
+ "case", "break", "continue", "assert", "super", "this", "try", "new", "yield"
22
+ ]);
23
+ // Defence-in-depth: even if a keyword is captured as a member name, drop it.
4
24
  const NOISE_TOKENS = new Set(["if", "for", "while", "switch", "catch", "return", "new", "throw"]);
5
25
  function normalizeLine(line) {
6
26
  return line.replace(/\s+/g, " ").trim();
@@ -8,6 +28,10 @@ function normalizeLine(line) {
8
28
  function isNoiseToken(token) {
9
29
  return NOISE_TOKENS.has(token);
10
30
  }
31
+ function isStatementLine(line) {
32
+ const firstWord = /^([A-Za-z_$][\w$]*)/.exec(line)?.[1];
33
+ return firstWord != null && STATEMENT_LINE_KEYWORDS.has(firstWord);
34
+ }
11
35
  function lineIndexToLine(lineNo) {
12
36
  return lineNo + 1;
13
37
  }
@@ -17,8 +41,13 @@ export function extractSymbolsFromSource(filePath, content) {
17
41
  const qualifiedName = filePath.replace(/\.java$/, "").replaceAll("/", ".");
18
42
  for (let index = 0; index < lines.length; index += 1) {
19
43
  const rawLine = lines[index] ?? "";
20
- const line = normalizeLine(rawLine);
21
- if (!line) {
44
+ const normalized = normalizeLine(rawLine);
45
+ if (!normalized) {
46
+ continue;
47
+ }
48
+ // Drop a single leading "}"/"{" so "} public void foo() {" still parses.
49
+ const line = normalized.replace(/^[}{]\s*/, "");
50
+ if (!line || isStatementLine(line)) {
22
51
  continue;
23
52
  }
24
53
  const classMatch = line.match(CLASS_DECLARATION);
@@ -35,7 +64,7 @@ export function extractSymbolsFromSource(filePath, content) {
35
64
  }
36
65
  continue;
37
66
  }
38
- const methodMatch = line.match(METHOD_DECLARATION);
67
+ const methodMatch = line.match(METHOD_BLOCK_DECLARATION) ?? line.match(METHOD_ABSTRACT_DECLARATION);
39
68
  if (methodMatch) {
40
69
  const symbolName = methodMatch[1];
41
70
  if (symbolName && !isNoiseToken(symbolName)) {
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Slice `text` to at most `maxChars` UTF-16 code units without splitting a
3
+ * surrogate pair. A naive `text.slice(0, maxChars)` can leave a lone high
4
+ * surrogate at the boundary (e.g. when an emoji straddles the cut), producing
5
+ * an invalid string; this drops that dangling half so callers never emit one.
6
+ */
7
+ export declare function sliceToMaxCharsSafe(text: string, maxChars: number): string;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Slice `text` to at most `maxChars` UTF-16 code units without splitting a
3
+ * surrogate pair. A naive `text.slice(0, maxChars)` can leave a lone high
4
+ * surrogate at the boundary (e.g. when an emoji straddles the cut), producing
5
+ * an invalid string; this drops that dangling half so callers never emit one.
6
+ */
7
+ export function sliceToMaxCharsSafe(text, maxChars) {
8
+ if (maxChars <= 0) {
9
+ return "";
10
+ }
11
+ if (text.length <= maxChars) {
12
+ return text;
13
+ }
14
+ let cut = text.slice(0, maxChars);
15
+ const lastUnit = cut.charCodeAt(cut.length - 1);
16
+ if (lastUnit >= 0xd800 && lastUnit <= 0xdbff) {
17
+ cut = cut.slice(0, -1);
18
+ }
19
+ return cut;
20
+ }
21
+ //# sourceMappingURL=text-truncate.js.map
@@ -15,6 +15,16 @@ function tinyRemapperDownloadUrl(version) {
15
15
  export async function resolveTinyRemapperJar(cacheDir, overridePath, fetchFn) {
16
16
  // 1. Environment / config override
17
17
  if (overridePath) {
18
+ if (!existsSync(overridePath)) {
19
+ throw createError({
20
+ code: ERROR_CODES.INVALID_INPUT,
21
+ message: `Configured tiny-remapper jar does not exist: ${overridePath}`,
22
+ details: {
23
+ path: overridePath,
24
+ nextAction: "Point tinyRemapperJarPath / MCP_TINY_REMAPPER_JAR at an existing fat jar, or unset it to download automatically."
25
+ }
26
+ });
27
+ }
18
28
  return overridePath;
19
29
  }
20
30
  const version = process.env.MCP_TINY_REMAPPER_VERSION ?? DEFAULT_VERSION;
@@ -67,14 +67,14 @@ const SECTION_ROWS = {
67
67
  "mapping-symbols": {
68
68
  en: [
69
69
  "| `find-mapping` | Look up mapping candidates for class, field, or method symbols |",
70
- "| `resolve-method-mapping-exact` | Resolve one method mapping with strict owner, name, and descriptor matching |",
70
+ "| `resolve-method-mapping-exact` | Strict shortcut for `find-mapping` (kind=method, signatureMode=exact); requires the full owner+name+descriptor triple |",
71
71
  "| `get-class-api-matrix` | Show one class API across `obfuscated`, `mojang`, `intermediary`, and `yarn` |",
72
72
  "| `resolve-workspace-symbol` | Resolve compile-visible symbol names from a Gradle workspace |",
73
73
  "| `check-symbol-exists` | Check whether a class, field, or method exists in a namespace |"
74
74
  ],
75
75
  ja: [
76
76
  "| `find-mapping` | クラス、フィールド、メソッドのシンボルに対するマッピング候補を調べる |",
77
- "| `resolve-method-mapping-exact` | ownernamedescriptor の厳密一致で 1 つのメソッドマッピングを解決する |",
77
+ "| `resolve-method-mapping-exact` | `find-mapping`(kind=method, signatureMode=exact)の厳密版ショートカット。owner+name+descriptor の三つ組が必須 |",
78
78
  "| `get-class-api-matrix` | 1 つのクラス API を `obfuscated`、`mojang`、`intermediary`、`yarn` で見比べる |",
79
79
  "| `resolve-workspace-symbol` | Gradle ワークスペースからコンパイル時に見えるシンボル名を解決する |",
80
80
  "| `check-symbol-exists` | 名前空間内でクラス、フィールド、メソッドが存在するかを確認する |"
@@ -3,6 +3,13 @@ const DEFAULT_OPTIONS = {
3
3
  maxConcurrent: 1,
4
4
  maxQueue: 2
5
5
  };
6
+ // Heavy tools that have a batch equivalent able to collapse many same-kind
7
+ // queries into a single gated call. Used to point overflow guidance at the
8
+ // right batch-* tool instead of just telling the caller to retry serially.
9
+ const BATCH_EQUIVALENTS = {
10
+ "find-mapping": "batch-mappings",
11
+ "resolve-method-mapping-exact": "batch-mappings"
12
+ };
6
13
  export class ToolExecutionGate {
7
14
  maxConcurrent;
8
15
  maxQueue;
@@ -17,6 +24,10 @@ export class ToolExecutionGate {
17
24
  return this.execute({ tool, task: task });
18
25
  }
19
26
  if (this.queue.length >= this.maxQueue) {
27
+ const batchTool = BATCH_EQUIVALENTS[tool];
28
+ const nextAction = batchTool
29
+ ? `Retry after the current heavy analysis request completes. To run many ${tool} queries efficiently, collapse them into a single ${batchTool} call instead of issuing them in parallel.`
30
+ : "Retry after the current heavy analysis request completes. Avoid sending multiple heavy mapping/version analysis tools in parallel.";
20
31
  return Promise.reject(createError({
21
32
  code: ERROR_CODES.LIMIT_EXCEEDED,
22
33
  message: `Heavy tool queue is full; "${tool}" was not started.`,
@@ -26,7 +37,7 @@ export class ToolExecutionGate {
26
37
  queuedCount: this.queue.length,
27
38
  maxConcurrent: this.maxConcurrent,
28
39
  maxQueue: this.maxQueue,
29
- nextAction: "Retry after the current heavy analysis request completes. Avoid sending multiple heavy mapping/version analysis tools in parallel."
40
+ nextAction
30
41
  }
31
42
  }));
32
43
  }
@@ -1,4 +1,5 @@
1
1
  import { ZodError } from "zod";
2
+ import type { WarningDetail } from "./warning-details.js";
2
3
  import { type ExampleCall, type ProblemDetails, type ProblemFieldError, type SuggestedCall } from "./error-mapping.js";
3
4
  import type { SourceTargetInput } from "./types.js";
4
5
  export type ToolMeta = {
@@ -6,6 +7,8 @@ export type ToolMeta = {
6
7
  tool: string;
7
8
  durationMs: number;
8
9
  warnings: string[];
10
+ /** Structured companion to `warnings`: one classified entry per warning string, referencing its text via `warnings[detail.index]`. */
11
+ warningDetails?: WarningDetail[];
9
12
  detailApplied?: "summary" | "standard" | "full";
10
13
  includeApplied?: string[];
11
14
  truncated?: Record<string, unknown>;
@@ -29,7 +32,7 @@ export declare const SUGGESTED_CALL_DEFAULTS: {
29
32
  readonly explain: false;
30
33
  readonly preferProjectMapping: false;
31
34
  readonly minSeverity: "all";
32
- readonly reportMode: "full";
35
+ readonly reportMode: "summary-first";
33
36
  readonly treatInfoAsWarning: true;
34
37
  readonly includeIssues: true;
35
38
  };