@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
@@ -67,18 +67,21 @@ function parseSearchCursor(cursor) {
67
67
  });
68
68
  }
69
69
  }
70
- function nextCursorFromRows(rows) {
71
- if (rows.length === 0) {
70
+ function nextCursorFromRows(rows, limit) {
71
+ if (rows.length === 0 || rows.length < limit) {
72
72
  return undefined;
73
73
  }
74
74
  const last = rows[rows.length - 1];
75
75
  return buildCursor(last.file_path);
76
76
  }
77
+ function compareFilePaths(left, right) {
78
+ return left < right ? -1 : left > right ? 1 : 0;
79
+ }
77
80
  function compareSearchOrdering(left, right) {
78
81
  if (right.score !== left.score) {
79
82
  return right.score - left.score;
80
83
  }
81
- return left.filePath.localeCompare(right.filePath);
84
+ return compareFilePaths(left.filePath, right.filePath);
82
85
  }
83
86
  function isAfterSearchCursor(hit, cursor) {
84
87
  if (hit.score < cursor.score) {
@@ -87,7 +90,7 @@ function isAfterSearchCursor(hit, cursor) {
87
90
  if (hit.score > cursor.score) {
88
91
  return false;
89
92
  }
90
- return hit.filePath.localeCompare(cursor.filePath) > 0;
93
+ return compareFilePaths(hit.filePath, cursor.filePath) > 0;
91
94
  }
92
95
  function buildPreview(content, query) {
93
96
  const normalizedQuery = query.toLowerCase();
@@ -121,6 +124,10 @@ function buildIndexedMatchQuery(query, match) {
121
124
  return token;
122
125
  }).join(" ");
123
126
  }
127
+ /** Escape LIKE wildcards so the needle is matched literally under ESCAPE '\'. */
128
+ function escapeLikeNeedle(value) {
129
+ return value.replace(/[\\%_]/g, "\\$&");
130
+ }
124
131
  export class FilesRepo {
125
132
  db;
126
133
  deleteStmt;
@@ -129,9 +136,14 @@ export class FilesRepo {
129
136
  listStmt;
130
137
  listRowsStmt;
131
138
  searchPathStmt;
139
+ searchContentLikePathsStmt;
132
140
  searchFtsStmt;
133
141
  getByPathsStmtCache = new Map();
134
142
  classLookupStmtCache = new Map();
143
+ // Fixed-SQL statements built outside the constructor; cached (LRU by SQL text)
144
+ // so hot paths do not re-prepare per call.
145
+ dynamicStmtCache = new Map();
146
+ static DYNAMIC_STMT_CACHE_MAX = 64;
135
147
  constructor(db) {
136
148
  this.db = db;
137
149
  this.deleteStmt = this.db.prepare(`
@@ -150,14 +162,14 @@ export class FilesRepo {
150
162
  this.listStmt = this.db.prepare(`
151
163
  SELECT artifact_id, file_path
152
164
  FROM files
153
- WHERE artifact_id = ? AND file_path > ? AND (? IS NULL OR file_path LIKE ? || '%')
165
+ WHERE artifact_id = ? AND file_path > ? AND (? IS NULL OR file_path LIKE ? ESCAPE '\\')
154
166
  ORDER BY file_path ASC
155
167
  LIMIT ?
156
168
  `);
157
169
  this.listRowsStmt = this.db.prepare(`
158
170
  SELECT artifact_id, file_path, content, content_bytes, content_hash
159
171
  FROM files
160
- WHERE artifact_id = ? AND file_path > ? AND (? IS NULL OR file_path LIKE ? || '%')
172
+ WHERE artifact_id = ? AND file_path > ? AND (? IS NULL OR file_path LIKE ? ESCAPE '\\')
161
173
  ORDER BY file_path ASC
162
174
  LIMIT ?
163
175
  `);
@@ -167,6 +179,13 @@ export class FilesRepo {
167
179
  WHERE artifact_id = ? AND file_path LIKE ? ESCAPE '\\'
168
180
  ORDER BY file_path ASC
169
181
  LIMIT ?
182
+ `);
183
+ this.searchContentLikePathsStmt = this.db.prepare(`
184
+ SELECT file_path
185
+ FROM files
186
+ WHERE artifact_id = ? AND content LIKE ? ESCAPE '\\'
187
+ ORDER BY file_path ASC
188
+ LIMIT ?
170
189
  `);
171
190
  this.searchFtsStmt = this.db.prepare(`
172
191
  SELECT file_path, rank
@@ -176,6 +195,23 @@ export class FilesRepo {
176
195
  LIMIT ?
177
196
  `);
178
197
  }
198
+ prepareCached(sql) {
199
+ const cached = this.dynamicStmtCache.get(sql);
200
+ if (cached) {
201
+ // Refresh recency so hot statements survive eviction.
202
+ this.dynamicStmtCache.delete(sql);
203
+ this.dynamicStmtCache.set(sql, cached);
204
+ return cached;
205
+ }
206
+ const stmt = this.db.prepare(sql);
207
+ this.dynamicStmtCache.set(sql, stmt);
208
+ if (this.dynamicStmtCache.size > FilesRepo.DYNAMIC_STMT_CACHE_MAX) {
209
+ const oldest = this.dynamicStmtCache.keys().next().value;
210
+ if (oldest !== undefined)
211
+ this.dynamicStmtCache.delete(oldest);
212
+ }
213
+ return stmt;
214
+ }
179
215
  clearFilesForArtifact(artifactId) {
180
216
  this.deleteStmt.run([artifactId]);
181
217
  }
@@ -219,15 +255,15 @@ export class FilesRepo {
219
255
  }
220
256
  listFiles(artifactId, options) {
221
257
  const cursor = parseCursor(options.cursor);
222
- const rows = this.listStmt.all(artifactId, cursor?.sortKey ?? "", options.prefix ?? null, options.prefix ?? "", Math.max(1, options.limit));
258
+ const rows = this.listStmt.all(artifactId, cursor?.sortKey ?? "", options.prefix ?? null, options.prefix != null ? `${escapeLikeNeedle(options.prefix)}%` : "", Math.max(1, options.limit));
223
259
  return {
224
260
  items: rows.map((row) => row.file_path),
225
- nextCursor: nextCursorFromRows(rows)
261
+ nextCursor: nextCursorFromRows(rows, Math.max(1, options.limit))
226
262
  };
227
263
  }
228
264
  listFileRows(artifactId, options) {
229
265
  const cursor = parseCursor(options.cursor);
230
- const rows = this.listRowsStmt.all(artifactId, cursor?.sortKey ?? "", options.prefix ?? null, options.prefix ?? "", Math.max(1, options.limit));
266
+ const rows = this.listRowsStmt.all(artifactId, cursor?.sortKey ?? "", options.prefix ?? null, options.prefix != null ? `${escapeLikeNeedle(options.prefix)}%` : "", Math.max(1, options.limit));
231
267
  return {
232
268
  items: rows.map((row) => ({
233
269
  artifactId: row.artifact_id,
@@ -236,9 +272,21 @@ export class FilesRepo {
236
272
  contentBytes: row.content_bytes,
237
273
  contentHash: row.content_hash
238
274
  })),
239
- nextCursor: nextCursorFromRows(rows)
275
+ nextCursor: nextCursorFromRows(rows, Math.max(1, options.limit))
240
276
  };
241
277
  }
278
+ /**
279
+ * Narrow the literal/contains text-scan candidate set without hydrating content:
280
+ * returns the file_paths whose content matches `content LIKE %needle%` (ASCII
281
+ * case-insensitive). The result is a SUPERSET of the JS contains/exact match set
282
+ * for an ASCII needle, so callers MUST still run the exact JS post-verify. Only
283
+ * file_path is selected — content stays in the DB until getFileContentsByPaths.
284
+ */
285
+ searchContentLikeCandidatePaths(artifactId, needle, limit) {
286
+ const pattern = `%${escapeLikeNeedle(needle)}%`;
287
+ const rows = this.searchContentLikePathsStmt.all(artifactId, pattern, Math.max(1, limit));
288
+ return { filePaths: rows.map((row) => row.file_path), scannedRows: rows.length };
289
+ }
242
290
  getFileContentsByPaths(artifactId, filePaths) {
243
291
  if (filePaths.length === 0) {
244
292
  return [];
@@ -264,7 +312,7 @@ export class FilesRepo {
264
312
  return { items: [], nextCursor: undefined, scannedRows: 0, dbRoundtrips: 0 };
265
313
  }
266
314
  const cursor = parseSearchCursor(options.cursor);
267
- const likeQuery = `%${normalized}%`;
315
+ const likeQuery = `%${escapeLikeNeedle(normalized)}%`;
268
316
  const ftsQuery = buildIndexedMatchQuery(normalized, options.match);
269
317
  const mode = options.mode ?? "mixed";
270
318
  // Cursor-adaptive fetch limit: when no cursor, use a generous limit;
@@ -274,28 +322,11 @@ export class FilesRepo {
274
322
  const fetchLimit = baseFetchLimit;
275
323
  // When cursor score is below all possible bands, skip both queries
276
324
  const cursorExhausted = cursor != null && cursor.score < 100;
277
- // Skip path query entirely when cursor is within the content-only tier
278
- const cursorPastPath = cursor != null && cursor.score < 120;
279
- const includePath = mode !== "text" && !cursorExhausted && !cursorPastPath;
325
+ const includePath = mode !== "text" && !cursorExhausted;
280
326
  const includeContent = mode !== "path" && !cursorExhausted;
281
- // Path query: push cursor into SQL when cursor.score == 120 (within path tier)
282
- let pathRows;
283
- if (includePath && cursor && cursor.score === 120) {
284
- // Cursor is within the path tier — only fetch paths after cursor.filePath
285
- pathRows = this.db.prepare(`
286
- SELECT file_path
287
- FROM files
288
- WHERE artifact_id = ? AND file_path LIKE ? ESCAPE '\\' AND file_path > ?
289
- ORDER BY file_path ASC
290
- LIMIT ?
291
- `).all(artifactId, likeQuery, cursor.filePath, fetchLimit);
292
- }
293
- else if (includePath) {
294
- pathRows = this.searchPathStmt.all(artifactId, likeQuery, fetchLimit);
295
- }
296
- else {
297
- pathRows = [];
298
- }
327
+ const pathRows = includePath
328
+ ? this.searchPathStmt.all(artifactId, likeQuery, fetchLimit)
329
+ : [];
299
330
  const merged = pathRows.map((row) => ({
300
331
  filePath: row.file_path,
301
332
  score: 120,
@@ -393,7 +424,7 @@ export class FilesRepo {
393
424
  return 0;
394
425
  }
395
426
  try {
396
- const row = this.db.prepare(`SELECT COUNT(*) AS cnt FROM files_fts WHERE artifact_id = ? AND files_fts MATCH ?`).get(artifactId, ftsQuery);
427
+ const row = this.prepareCached(`SELECT COUNT(*) AS cnt FROM files_fts WHERE artifact_id = ? AND files_fts MATCH ?`).get(artifactId, ftsQuery);
397
428
  return row?.cnt ?? 0;
398
429
  }
399
430
  catch {
@@ -409,8 +440,8 @@ export class FilesRepo {
409
440
  if (!normalized) {
410
441
  return 0;
411
442
  }
412
- const likeQuery = `%${normalized}%`;
413
- const row = this.db.prepare(`SELECT COUNT(*) AS cnt FROM files WHERE artifact_id = ? AND file_path LIKE ? ESCAPE '\\'`).get(artifactId, likeQuery);
443
+ const likeQuery = `%${escapeLikeNeedle(normalized)}%`;
444
+ const row = this.prepareCached(`SELECT COUNT(*) AS cnt FROM files WHERE artifact_id = ? AND file_path LIKE ? ESCAPE '\\'`).get(artifactId, likeQuery);
414
445
  return row?.cnt ?? 0;
415
446
  }
416
447
  findFirstFilePathByName(artifactId, fileName) {
@@ -418,8 +449,7 @@ export class FilesRepo {
418
449
  if (!normalized) {
419
450
  return undefined;
420
451
  }
421
- const row = this.db
422
- .prepare(`
452
+ const row = this.prepareCached(`
423
453
  SELECT file_path
424
454
  FROM files
425
455
  WHERE artifact_id = ?
@@ -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;
@@ -40,12 +46,21 @@ export declare class SymbolsRepo {
40
46
  private readonly listByArtifactStmt;
41
47
  private readonly listByArtifactKindStmt;
42
48
  private readonly listByFileStmt;
49
+ private readonly dynamicStmtCache;
50
+ private static readonly DYNAMIC_STMT_CACHE_MAX;
43
51
  constructor(db: SqliteDatabase);
52
+ private prepareCached;
44
53
  clearSymbolsForArtifact(artifactId: string): void;
45
54
  insertSymbolsForArtifact(artifactId: string, symbols: IndexedSymbol[]): void;
46
55
  replaceSymbolsForArtifact(artifactId: string, symbols: IndexedSymbol[]): void;
47
56
  findSymbols(options: FindSymbolsOptions): PagedResult<SymbolRow>;
48
57
  listSymbolsForArtifact(artifactId: string, symbolKind?: string): SymbolRow[];
58
+ /**
59
+ * Streaming variant of listSymbolsForArtifact: rows are mapped lazily so callers
60
+ * that filter (e.g. regex symbol search) never materialize the full symbol set.
61
+ * Do not run other statements on this connection while consuming the iterator.
62
+ */
63
+ iterateSymbolsForArtifact(artifactId: string, symbolKind?: string): IterableIterator<SymbolRow>;
49
64
  listSymbolsForFile(artifactId: string, filePath: string): SymbolRow[];
50
65
  listSymbolsForFiles(artifactId: string, filePaths: string[], symbolKind?: string): Map<string, SymbolRow[]>;
51
66
  findBySymbolNames(artifactId: string, symbolNames: string[]): SymbolRow[];
@@ -60,6 +60,10 @@ export class SymbolsRepo {
60
60
  listByArtifactStmt;
61
61
  listByArtifactKindStmt;
62
62
  listByFileStmt;
63
+ // Dynamic WHERE-clause builders produce a small bounded set of SQL strings;
64
+ // cache their prepared statements (LRU by SQL text) instead of re-preparing per call.
65
+ dynamicStmtCache = new Map();
66
+ static DYNAMIC_STMT_CACHE_MAX = 64;
63
67
  constructor(db) {
64
68
  this.db = db;
65
69
  this.deleteStmt = this.db.prepare(`
@@ -106,6 +110,23 @@ export class SymbolsRepo {
106
110
  ORDER BY line ASC, symbol_name ASC
107
111
  `);
108
112
  }
113
+ prepareCached(sql) {
114
+ const cached = this.dynamicStmtCache.get(sql);
115
+ if (cached) {
116
+ // Refresh recency so hot statements survive eviction.
117
+ this.dynamicStmtCache.delete(sql);
118
+ this.dynamicStmtCache.set(sql, cached);
119
+ return cached;
120
+ }
121
+ const stmt = this.db.prepare(sql);
122
+ this.dynamicStmtCache.set(sql, stmt);
123
+ if (this.dynamicStmtCache.size > SymbolsRepo.DYNAMIC_STMT_CACHE_MAX) {
124
+ const oldest = this.dynamicStmtCache.keys().next().value;
125
+ if (oldest !== undefined)
126
+ this.dynamicStmtCache.delete(oldest);
127
+ }
128
+ return stmt;
129
+ }
109
130
  clearSymbolsForArtifact(artifactId) {
110
131
  this.deleteStmt.run([artifactId]);
111
132
  }
@@ -150,7 +171,7 @@ export class SymbolsRepo {
150
171
  LIMIT ?
151
172
  `;
152
173
  const nameParam = options.exact ? symbolName : `${symbolName}%`;
153
- rows = this.db.prepare(sql).all(options.artifactId, options.symbolKind ?? "", options.symbolKind ?? "", nameParam, cursor.symbolName, cursor.symbolName, cursor.filePath, cursor.symbolName, cursor.filePath, cursor.line, Math.max(1, fetchLimit));
174
+ rows = this.prepareCached(sql).all(options.artifactId, options.symbolKind ?? "", options.symbolKind ?? "", nameParam, cursor.symbolName, cursor.symbolName, cursor.filePath, cursor.symbolName, cursor.filePath, cursor.line, Math.max(1, fetchLimit));
154
175
  }
155
176
  else {
156
177
  const queryRows = options.exact ? this.searchExactStmt : this.searchStmt;
@@ -173,16 +194,26 @@ export class SymbolsRepo {
173
194
  };
174
195
  }
175
196
  listSymbolsForArtifact(artifactId, symbolKind) {
197
+ return [...this.iterateSymbolsForArtifact(artifactId, symbolKind)];
198
+ }
199
+ /**
200
+ * Streaming variant of listSymbolsForArtifact: rows are mapped lazily so callers
201
+ * that filter (e.g. regex symbol search) never materialize the full symbol set.
202
+ * Do not run other statements on this connection while consuming the iterator.
203
+ */
204
+ *iterateSymbolsForArtifact(artifactId, symbolKind) {
176
205
  const rows = (symbolKind
177
- ? this.listByArtifactKindStmt.all(artifactId, symbolKind)
178
- : this.listByArtifactStmt.all(artifactId));
179
- return rows.map((row) => toSymbolRow(artifactId, {
180
- filePath: row.file_path,
181
- symbolKind: row.symbol_kind,
182
- symbolName: row.symbol_name,
183
- qualifiedName: row.qualified_name ?? undefined,
184
- line: row.line
185
- }));
206
+ ? this.listByArtifactKindStmt.iterate(artifactId, symbolKind)
207
+ : this.listByArtifactStmt.iterate(artifactId));
208
+ for (const row of rows) {
209
+ yield toSymbolRow(artifactId, {
210
+ filePath: row.file_path,
211
+ symbolKind: row.symbol_kind,
212
+ symbolName: row.symbol_name,
213
+ qualifiedName: row.qualified_name ?? undefined,
214
+ line: row.line
215
+ });
216
+ }
186
217
  }
187
218
  listSymbolsForFile(artifactId, filePath) {
188
219
  const rows = this.listByFileStmt.all(artifactId, filePath);
@@ -209,7 +240,7 @@ export class SymbolsRepo {
209
240
  ORDER BY file_path ASC, line ASC, symbol_name ASC
210
241
  `;
211
242
  const params = symbolKind ? [artifactId, ...uniqueFilePaths, symbolKind] : [artifactId, ...uniqueFilePaths];
212
- const rows = this.db.prepare(sql).all(...params);
243
+ const rows = this.prepareCached(sql).all(...params);
213
244
  const byFile = new Map();
214
245
  for (const row of rows) {
215
246
  const bucket = byFile.get(row.file_path) ?? [];
@@ -230,8 +261,7 @@ export class SymbolsRepo {
230
261
  return [];
231
262
  }
232
263
  const placeholders = unique.map(() => "?").join(", ");
233
- const rows = this.db
234
- .prepare(`
264
+ const rows = this.prepareCached(`
235
265
  SELECT file_path, symbol_kind, symbol_name, qualified_name, line
236
266
  FROM symbols
237
267
  WHERE artifact_id = ?
@@ -260,6 +290,12 @@ export class SymbolsRepo {
260
290
  where.push("symbol_kind = ?");
261
291
  params.push(options.symbolKind);
262
292
  }
293
+ const symbolKinds = options.symbolKinds;
294
+ if (symbolKinds && symbolKinds.length > 0) {
295
+ const placeholders = symbolKinds.map(() => "?").join(", ");
296
+ where.push(`symbol_kind IN (${placeholders})`);
297
+ params.push(...symbolKinds);
298
+ }
263
299
  if (options.packagePrefix?.trim()) {
264
300
  const normalizedPrefix = options.packagePrefix.replace(/\.+/g, "/").replace(/\/+$/, "");
265
301
  where.push("file_path LIKE ? ESCAPE '\\'");
@@ -300,7 +336,7 @@ export class SymbolsRepo {
300
336
  ORDER BY symbol_name ASC, file_path ASC, line ASC
301
337
  LIMIT ?
302
338
  `;
303
- const rows = this.db.prepare(sql).all(...params);
339
+ const rows = this.prepareCached(sql).all(...params);
304
340
  const hasMore = rows.length > rawLimit;
305
341
  const page = hasMore ? rows.slice(0, rawLimit) : rows;
306
342
  const lastRow = page.length > 0 ? page[page.length - 1] : undefined;
@@ -354,12 +390,11 @@ export class SymbolsRepo {
354
390
  WHERE artifact_id = ?
355
391
  ${where.length > 0 ? `AND ${where.join("\n AND ")}` : ""}
356
392
  `;
357
- const row = this.db.prepare(sql).get(...params);
393
+ const row = this.prepareCached(sql).get(...params);
358
394
  return row?.cnt ?? 0;
359
395
  }
360
396
  listDistinctFilePathsByKind(artifactId, symbolKind) {
361
- const rows = this.db
362
- .prepare(`
397
+ const rows = this.prepareCached(`
363
398
  SELECT DISTINCT file_path
364
399
  FROM symbols
365
400
  WHERE artifact_id = ? AND symbol_kind = ?
@@ -374,8 +409,7 @@ export class SymbolsRepo {
374
409
  if (!normalizedClassName || !normalizedSimpleName) {
375
410
  return undefined;
376
411
  }
377
- const row = this.db
378
- .prepare(`
412
+ const row = this.prepareCached(`
379
413
  SELECT file_path
380
414
  FROM symbols
381
415
  WHERE artifact_id = ?
@@ -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|sealed|non-sealed)";
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,11 +1,15 @@
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 = {
5
6
  requestId: string;
6
7
  tool: string;
7
8
  durationMs: number;
8
- warnings: string[];
9
+ /** Omitted entirely when there are no warnings (token efficiency). */
10
+ warnings?: string[];
11
+ /** Structured companion to `warnings`: one classified entry per warning string, referencing its text via `warnings[detail.index]`. */
12
+ warningDetails?: WarningDetail[];
9
13
  detailApplied?: "summary" | "standard" | "full";
10
14
  includeApplied?: string[];
11
15
  truncated?: Record<string, unknown>;
@@ -29,7 +33,7 @@ export declare const SUGGESTED_CALL_DEFAULTS: {
29
33
  readonly explain: false;
30
34
  readonly preferProjectMapping: false;
31
35
  readonly minSeverity: "all";
32
- readonly reportMode: "full";
36
+ readonly reportMode: "summary-first";
33
37
  readonly treatInfoAsWarning: true;
34
38
  readonly includeIssues: true;
35
39
  };