@adhisang/minecraft-modding-mcp 4.2.0 → 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 (120) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +6 -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 +99 -6
  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/errors.js +4 -1
  49. package/dist/index.js +138 -71
  50. package/dist/java-process.d.ts +9 -0
  51. package/dist/java-process.js +11 -5
  52. package/dist/mapping/internal-types.d.ts +7 -0
  53. package/dist/mapping/lookup.d.ts +1 -1
  54. package/dist/mapping/lookup.js +7 -1
  55. package/dist/mapping/types.d.ts +6 -0
  56. package/dist/mapping-service.d.ts +26 -2
  57. package/dist/mapping-service.js +178 -36
  58. package/dist/mcp-helpers.d.ts +1 -0
  59. package/dist/mcp-helpers.js +25 -3
  60. package/dist/minecraft-explorer-service.js +24 -11
  61. package/dist/mixin/access-validators.js +33 -1
  62. package/dist/mixin/annotation-validators.js +53 -7
  63. package/dist/mixin/parsed-validator.js +2 -2
  64. package/dist/mixin-parser.js +48 -4
  65. package/dist/mod-decompile-service.js +2 -1
  66. package/dist/mod-remap-service.d.ts +20 -1
  67. package/dist/mod-remap-service.js +95 -40
  68. package/dist/nbt/java-nbt-codec.js +87 -2
  69. package/dist/observability.d.ts +6 -0
  70. package/dist/observability.js +10 -0
  71. package/dist/page-cursor.d.ts +17 -0
  72. package/dist/page-cursor.js +0 -0
  73. package/dist/registry-service.js +25 -1
  74. package/dist/resources.js +45 -9
  75. package/dist/response-utils.d.ts +29 -4
  76. package/dist/response-utils.js +95 -5
  77. package/dist/source/access-validate.js +12 -2
  78. package/dist/source/artifact-resolver.js +1 -1
  79. package/dist/source/cache-metrics.js +2 -2
  80. package/dist/source/class-source/members-builder.d.ts +44 -1
  81. package/dist/source/class-source/members-builder.js +51 -4
  82. package/dist/source/class-source/snippet-builder.d.ts +13 -0
  83. package/dist/source/class-source/snippet-builder.js +48 -4
  84. package/dist/source/class-source-helpers.js +67 -11
  85. package/dist/source/class-source.js +97 -20
  86. package/dist/source/indexer.d.ts +31 -6
  87. package/dist/source/indexer.js +95 -25
  88. package/dist/source/search.d.ts +1 -1
  89. package/dist/source/search.js +94 -22
  90. package/dist/source/validate-mixin.d.ts +2 -1
  91. package/dist/source/validate-mixin.js +45 -4
  92. package/dist/source/workspace-target.js +15 -9
  93. package/dist/source-jar-reader.d.ts +54 -1
  94. package/dist/source-jar-reader.js +121 -10
  95. package/dist/source-service.d.ts +28 -7
  96. package/dist/storage/files-repo.d.ts +12 -0
  97. package/dist/storage/files-repo.js +24 -0
  98. package/dist/storage/symbols-repo.d.ts +6 -0
  99. package/dist/storage/symbols-repo.js +6 -0
  100. package/dist/symbols/symbol-extractor.js +35 -6
  101. package/dist/text-truncate.d.ts +7 -0
  102. package/dist/text-truncate.js +21 -0
  103. package/dist/tiny-remapper-resolver.js +10 -0
  104. package/dist/tiny-remapper-service.js +3 -3
  105. package/dist/tool-contract-manifest.js +2 -2
  106. package/dist/tool-execution-gate.js +12 -1
  107. package/dist/tool-guidance.d.ts +4 -1
  108. package/dist/tool-guidance.js +37 -12
  109. package/dist/tool-schemas.d.ts +559 -494
  110. package/dist/tool-schemas.js +99 -63
  111. package/dist/types.d.ts +1 -0
  112. package/dist/version-diff-service.js +5 -0
  113. package/dist/version-service.d.ts +3 -0
  114. package/dist/version-service.js +18 -2
  115. package/dist/warning-details.d.ts +18 -0
  116. package/dist/warning-details.js +63 -0
  117. package/docs/README-ja.md +2 -2
  118. package/docs/examples.md +26 -2
  119. package/docs/tool-reference.md +23 -17
  120. package/package.json +1 -1
@@ -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;
@@ -1,10 +1,10 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { createError, ERROR_CODES } from "./errors.js";
3
- import { assertJavaAvailable, runJavaProcess } from "./java-process.js";
3
+ import { javaRunner } from "./java-process.js";
4
4
  import { log } from "./logger.js";
5
5
  export async function remapJar(tinyRemapperJarPath, options) {
6
6
  const { inputJar, outputJar, mappingsFile, fromNamespace, toNamespace, threads = 4, rebuildSourceFilenames = false, timeoutMs = 600_000, maxMemoryMb = 4096 } = options;
7
- await assertJavaAvailable();
7
+ await javaRunner.assertAvailable();
8
8
  log("info", "remap.start", {
9
9
  inputJar,
10
10
  outputJar,
@@ -21,7 +21,7 @@ export async function remapJar(tinyRemapperJarPath, options) {
21
21
  args.push("--rebuildSourceFilenames");
22
22
  }
23
23
  try {
24
- const result = await runJavaProcess({
24
+ const result = await javaRunner.run({
25
25
  jarPath: tinyRemapperJarPath,
26
26
  args,
27
27
  timeoutMs,
@@ -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
  };
@@ -1,7 +1,7 @@
1
1
  import { ZodError } from "zod";
2
2
  import { buildSuggestedCall } from "./build-suggested-call.js";
3
3
  import { ERROR_CODES, isAppError } from "./errors.js";
4
- import { statusForErrorCode } from "./error-mapping.js";
4
+ import { extractAllowlistedContext, issueOriginForErrorCode, retryClassForErrorCode, statusForErrorCode } from "./error-mapping.js";
5
5
  export const SUGGESTED_CALL_DEFAULTS = {
6
6
  allowDecompile: true,
7
7
  preferProjectVersion: false,
@@ -14,7 +14,7 @@ export const SUGGESTED_CALL_DEFAULTS = {
14
14
  explain: false,
15
15
  preferProjectMapping: false,
16
16
  minSeverity: "all",
17
- reportMode: "full",
17
+ reportMode: "summary-first",
18
18
  treatInfoAsWarning: true,
19
19
  includeIssues: true
20
20
  };
@@ -98,14 +98,19 @@ export function extractValidatedSuggestionAndExamples(details) {
98
98
  typeof ex.reason !== "string") {
99
99
  continue;
100
100
  }
101
+ // Re-validate through the example path, not the primary path: example
102
+ // calls are templates and MAY carry placeholder sentinels (e.g.
103
+ // "<version>"), which the primary gate intentionally rejects.
101
104
  const result = buildSuggestedCall({
102
105
  tool: ex.tool,
103
- params: ex.params
106
+ params: undefined,
107
+ examples: [{ params: ex.params, reason: ex.reason }]
104
108
  });
105
- if (result.suggestedCall) {
109
+ const validatedExample = result.exampleCalls?.[0];
110
+ if (validatedExample) {
106
111
  validated.push({
107
112
  tool: ex.tool,
108
- params: result.suggestedCall.params,
113
+ params: validatedExample.params,
109
114
  reason: ex.reason
110
115
  });
111
116
  }
@@ -414,12 +419,10 @@ export function buildSourceLookupSuggestedParams(tool, normalizedInput) {
414
419
  const targetValue = asNonEmptyString(record?.target);
415
420
  result.target = targetValue
416
421
  ? {
417
- type: "resolve",
418
422
  kind: inferTargetKindFromString(targetValue),
419
423
  value: targetValue
420
424
  }
421
425
  : {
422
- type: "resolve",
423
426
  kind: "version",
424
427
  value: "<minecraft-version>"
425
428
  };
@@ -607,7 +610,21 @@ export function buildValidateProjectSuggestedParams(normalizedInput) {
607
610
  return result;
608
611
  }
609
612
  function gatedGuidance(tool, hints, params) {
610
- const validated = buildSuggestedCall({ tool, params });
613
+ // Pass the reconstructed params as both the primary and a fallback example.
614
+ // When they are fully executable they become `suggestedCall`; when they still
615
+ // carry `<…>` placeholders (e.g. a missing version) the gate drops the
616
+ // primary and surfaces the same shape as an `exampleCalls` template instead
617
+ // of emitting a non-callable suggestedCall.
618
+ const validated = buildSuggestedCall({
619
+ tool,
620
+ params,
621
+ examples: [
622
+ {
623
+ params,
624
+ reason: `Example ${tool} call shape — replace any <…> placeholder values before sending.`
625
+ }
626
+ ]
627
+ });
611
628
  return {
612
629
  hints,
613
630
  ...validated,
@@ -630,8 +647,8 @@ export function buildInvalidInputGuidance(tool, normalizedInput) {
630
647
  }
631
648
  if (tool === "get-class-source" || tool === "get-class-members") {
632
649
  return gatedGuidance(tool, [
633
- `${tool}.target must be an object: {"type":"resolve","kind":"version|jar|coordinate","value":"..."} or {"type":"artifact","artifactId":"..."}.`,
634
- "Bare string targets are not accepted; wrap the value under target.type/target.kind/target.value."
650
+ `${tool}.target must be an object: {"kind":"version|jar|coordinate","value":"..."} or {"kind":"artifact","artifactId":"..."} (same shape as resolve-artifact).`,
651
+ "Bare string targets are not accepted; wrap the value under target.kind/target.value."
635
652
  ], buildSourceLookupSuggestedParams(tool, normalizedInput));
636
653
  }
637
654
  if (tool === "validate-project") {
@@ -666,6 +683,8 @@ export function mapErrorToProblem(caughtError, requestId, context) {
666
683
  status: 400,
667
684
  code: ERROR_CODES.INVALID_INPUT,
668
685
  instance: requestId,
686
+ retryClass: retryClassForErrorCode(ERROR_CODES.INVALID_INPUT),
687
+ issueOrigin: issueOriginForErrorCode(ERROR_CODES.INVALID_INPUT),
669
688
  fieldErrors: toFieldErrorsFromZod(caughtError),
670
689
  hints: hintsWithFallback,
671
690
  ...(guidance?.suggestedCall ? { suggestedCall: guidance.suggestedCall } : {}),
@@ -675,6 +694,7 @@ export function mapErrorToProblem(caughtError, requestId, context) {
675
694
  }
676
695
  if (isAppError(caughtError)) {
677
696
  const { suggestedCall, exampleCalls, primaryDropped } = extractValidatedSuggestionAndExamples(caughtError.details);
697
+ const sanitizedContext = extractAllowlistedContext(caughtError.details);
678
698
  let failedStage = extractFailedStageFromDetails(caughtError.details);
679
699
  if (!failedStage
680
700
  && context?.tool === "validate-mixin"
@@ -692,11 +712,14 @@ export function mapErrorToProblem(caughtError, requestId, context) {
692
712
  status: statusForErrorCode(caughtError.code),
693
713
  code: caughtError.code,
694
714
  instance: requestId,
715
+ retryClass: retryClassForErrorCode(caughtError.code),
716
+ issueOrigin: issueOriginForErrorCode(caughtError.code),
695
717
  fieldErrors: extractFieldErrorsFromDetails(caughtError.details),
696
718
  hints: hintsWithFallback,
697
719
  ...(suggestedCall ? { suggestedCall } : {}),
698
720
  ...(exampleCalls ? { exampleCalls } : {}),
699
- ...(failedStage ? { failedStage } : {})
721
+ ...(failedStage ? { failedStage } : {}),
722
+ ...(sanitizedContext ? { context: sanitizedContext } : {})
700
723
  };
701
724
  }
702
725
  return {
@@ -705,7 +728,9 @@ export function mapErrorToProblem(caughtError, requestId, context) {
705
728
  detail: "Unexpected server error.",
706
729
  status: 500,
707
730
  code: ERROR_CODES.INTERNAL,
708
- instance: requestId
731
+ instance: requestId,
732
+ retryClass: retryClassForErrorCode(ERROR_CODES.INTERNAL),
733
+ issueOrigin: issueOriginForErrorCode(ERROR_CODES.INTERNAL)
709
734
  };
710
735
  }
711
736
  /**