@adhisang/minecraft-modding-mcp 6.2.0 → 7.0.0-rc.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 (98) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +21 -5
  3. package/dist/cache-policy.d.ts +71 -0
  4. package/dist/cache-policy.js +83 -0
  5. package/dist/cache-registry.js +6 -6
  6. package/dist/cli.js +74 -3
  7. package/dist/compat-stdio-transport.d.ts +1 -1
  8. package/dist/compat-stdio-transport.js +13 -1
  9. package/dist/config.d.ts +3 -0
  10. package/dist/config.js +8 -2
  11. package/dist/decompiler/vineflower.d.ts +1 -0
  12. package/dist/decompiler/vineflower.js +8 -5
  13. package/dist/entry-tools/analyze-mod-service.d.ts +70 -136
  14. package/dist/entry-tools/analyze-symbol-service.d.ts +112 -150
  15. package/dist/entry-tools/compare-minecraft-service.d.ts +59 -145
  16. package/dist/entry-tools/entry-tool-schema.d.ts +38 -4
  17. package/dist/entry-tools/entry-tool-schema.js +4 -1
  18. package/dist/entry-tools/inspect-minecraft/internal.d.ts +235 -799
  19. package/dist/entry-tools/inspect-minecraft/internal.js +65 -28
  20. package/dist/entry-tools/inspect-minecraft-service.d.ts +372 -1736
  21. package/dist/entry-tools/inspect-minecraft-service.js +1 -1
  22. package/dist/entry-tools/manage-cache-service.d.ts +81 -91
  23. package/dist/entry-tools/validate-project/cases/project-summary.d.ts +7 -7
  24. package/dist/entry-tools/validate-project-service.d.ts +164 -592
  25. package/dist/entry-tools/verify-mixin-target-service.d.ts +3 -19
  26. package/dist/era-classifier.d.ts +161 -0
  27. package/dist/era-classifier.js +292 -0
  28. package/dist/error-mapping.js +9 -2
  29. package/dist/index.d.ts +42 -4
  30. package/dist/index.js +637 -475
  31. package/dist/java-process.d.ts +2 -0
  32. package/dist/java-process.js +22 -2
  33. package/dist/json-rpc-framing.d.ts +77 -1
  34. package/dist/json-rpc-framing.js +249 -13
  35. package/dist/mapping/loaders/tiny-loom-selection.d.ts +88 -0
  36. package/dist/mapping/loaders/tiny-loom-selection.js +223 -0
  37. package/dist/mapping/loaders/tiny-loom.js +45 -33
  38. package/dist/mapping/loaders/tiny-maven.js +6 -11
  39. package/dist/mapping/parsers/tiny.d.ts +57 -0
  40. package/dist/mapping/parsers/tiny.js +99 -22
  41. package/dist/mapping-service.d.ts +19 -0
  42. package/dist/mapping-service.js +93 -9
  43. package/dist/mcp-helpers.d.ts +19 -2
  44. package/dist/mcp-helpers.js +48 -6
  45. package/dist/minecraft-explorer-service.d.ts +1 -1
  46. package/dist/mixin/types.d.ts +8 -0
  47. package/dist/mod-analyzer.js +7 -7
  48. package/dist/mod-decompile-service.js +1 -0
  49. package/dist/nbt/java-nbt-codec.js +12 -2
  50. package/dist/nbt/json-patch.js +14 -3
  51. package/dist/nbt/pipeline.js +40 -3
  52. package/dist/nbt/typed-json.js +26 -1
  53. package/dist/registration-adapter.d.ts +32 -0
  54. package/dist/registration-adapter.js +52 -0
  55. package/dist/request-context.d.ts +7 -0
  56. package/dist/request-context.js +9 -0
  57. package/dist/resources.d.ts +1 -1
  58. package/dist/resources.js +25 -19
  59. package/dist/server-identity.d.ts +27 -0
  60. package/dist/server-identity.js +26 -0
  61. package/dist/source/access-validate.js +53 -0
  62. package/dist/source/artifact-resolver.d.ts +69 -1
  63. package/dist/source/artifact-resolver.js +215 -14
  64. package/dist/source/class-source.d.ts +22 -0
  65. package/dist/source/class-source.js +162 -29
  66. package/dist/source/did-you-mean.d.ts +12 -1
  67. package/dist/source/did-you-mean.js +6 -2
  68. package/dist/source/file-access.js +150 -46
  69. package/dist/source/indexer.js +1 -0
  70. package/dist/source/nested-jars.d.ts +19 -0
  71. package/dist/source/nested-jars.js +90 -21
  72. package/dist/source/shared-utils.d.ts +21 -0
  73. package/dist/source/shared-utils.js +23 -0
  74. package/dist/source-service.d.ts +12 -0
  75. package/dist/source-service.js +3 -0
  76. package/dist/stdio-supervisor.d.ts +357 -2
  77. package/dist/stdio-supervisor.js +1031 -80
  78. package/dist/storage/db.d.ts +2 -1
  79. package/dist/storage/db.js +15 -8
  80. package/dist/synthetic-decorator.d.ts +24 -0
  81. package/dist/synthetic-decorator.js +48 -0
  82. package/dist/tool-contract-manifest.js +1 -1
  83. package/dist/tool-guidance.d.ts +17 -1
  84. package/dist/tool-guidance.js +417 -13
  85. package/dist/tool-schema-registry.d.ts +2 -0
  86. package/dist/tool-schema-registry.js +4 -0
  87. package/dist/tool-schemas.d.ts +2214 -3915
  88. package/dist/tool-schemas.js +34 -7
  89. package/dist/types.d.ts +35 -0
  90. package/dist/v1-parity-schemas.d.ts +7 -0
  91. package/dist/v1-parity-schemas.js +5584 -0
  92. package/dist/version-diff-service.d.ts +33 -0
  93. package/dist/version-diff-service.js +148 -3
  94. package/dist/version-service.js +36 -14
  95. package/dist/warning-details.js +18 -1
  96. package/docs/README-ja.md +9 -3
  97. package/docs/tool-reference.md +267 -22
  98. package/package.json +12 -9
@@ -10,5 +10,6 @@ export interface InitializedDatabase {
10
10
  db: SqliteDatabase;
11
11
  schemaVersion: number;
12
12
  }
13
- export declare function openDatabase(config: Config, logger?: Logger): InitializedDatabase;
13
+ type DatabaseConfig = Pick<Config, "sqlitePath"> & Partial<Pick<Config, "sqliteCacheKb" | "sqliteMmapSize">>;
14
+ export declare function openDatabase(config: DatabaseConfig, logger?: Logger): InitializedDatabase;
14
15
  export {};
@@ -5,6 +5,8 @@ import Database from "./sqlite.js";
5
5
  import { runMigrations } from "./migrations.js";
6
6
  import { createError, ERROR_CODES, isAppError } from "../errors.js";
7
7
  import { log } from "../logger.js";
8
+ const DEFAULT_SQLITE_CACHE_KB = 8_000;
9
+ const DEFAULT_SQLITE_MMAP_SIZE = 268_435_456;
8
10
  function ensureParentDirectory(path) {
9
11
  mkdirSync(dirname(path), { recursive: true });
10
12
  }
@@ -18,6 +20,17 @@ function runIntegrityCheck(db) {
18
20
  });
19
21
  }
20
22
  }
23
+ function applyPragmas(db, config) {
24
+ const sqliteCacheKb = config.sqliteCacheKb ?? DEFAULT_SQLITE_CACHE_KB;
25
+ const sqliteMmapSize = config.sqliteMmapSize ?? DEFAULT_SQLITE_MMAP_SIZE;
26
+ db.pragma("foreign_keys = ON");
27
+ db.pragma("journal_mode = WAL");
28
+ db.pragma("synchronous = NORMAL");
29
+ db.pragma("busy_timeout = 5000");
30
+ db.pragma(`cache_size = -${sqliteCacheKb}`);
31
+ db.pragma(`mmap_size = ${sqliteMmapSize}`);
32
+ db.pragma("temp_store = MEMORY");
33
+ }
21
34
  function backupCorruptedDb(sqlitePath) {
22
35
  const backupPath = `${sqlitePath}.corrupted.${Date.now()}`;
23
36
  renameSync(sqlitePath, backupPath);
@@ -90,10 +103,7 @@ export function openDatabase(config, logger = buildDefaultLogger()) {
90
103
  try {
91
104
  ensureParentDirectory(config.sqlitePath);
92
105
  db = new Database(config.sqlitePath);
93
- db.pragma("foreign_keys = ON");
94
- db.pragma("journal_mode = WAL");
95
- db.pragma("synchronous = NORMAL");
96
- db.pragma("busy_timeout = 5000");
106
+ applyPragmas(db, config);
97
107
  const schemaVersion = runMigrations(db);
98
108
  runIntegrityCheck(db);
99
109
  return { db, schemaVersion };
@@ -133,10 +143,7 @@ export function openDatabase(config, logger = buildDefaultLogger()) {
133
143
  backupPath
134
144
  });
135
145
  const rebuilt = new Database(config.sqlitePath);
136
- rebuilt.pragma("foreign_keys = ON");
137
- rebuilt.pragma("journal_mode = WAL");
138
- rebuilt.pragma("synchronous = NORMAL");
139
- rebuilt.pragma("busy_timeout = 5000");
146
+ applyPragmas(rebuilt, config);
140
147
  const schemaVersion = runMigrations(rebuilt);
141
148
  runIntegrityCheck(rebuilt);
142
149
  return { db: rebuilt, schemaVersion };
@@ -0,0 +1,24 @@
1
+ import type { JSONRPCResponse } from "@modelcontextprotocol/server";
2
+ import type { Era } from "./era-classifier.js";
3
+ /**
4
+ * Centralized modern-era decoration for supervisor-synthesized replies.
5
+ *
6
+ * Contract:
7
+ * - Decorates ONLY when the originating request's snapshot era is "modern"
8
+ * (the era captured at admission; it is the ONLY captured-context field
9
+ * synthetic decoration may read — protocolVersion/clientCapabilities/
10
+ * clientInfo stay unread, and the identity comes from the canonical
11
+ * src/server-identity.ts module, never from request context).
12
+ * - Modern-era RESULT envelopes gain the modern public fields the SDK stamps
13
+ * on every result it encodes: top-level `resultType: "complete"` (every
14
+ * result this server emits is complete — synthetic results are all
15
+ * tools/call-shaped, where "complete" is always correct) and
16
+ * `_meta[SERVER_INFO_META_KEY]` carrying the canonical identity.
17
+ * - Raw JSON-RPC ERROR envelopes and non-modern eras pass through UNCHANGED
18
+ * (same reference), keeping the legacy synthetic shapes byte-identical to
19
+ * the premigration fixtures (pinned by the synthetic-inventory suite).
20
+ * - Cache fields (ttlMs/cacheScope) are NEVER added: they belong only to the
21
+ * six cacheable list/read/discover methods, none of which has a structured
22
+ * synthetic path.
23
+ */
24
+ export declare function decorateSyntheticReply(reply: JSONRPCResponse, era: Era | undefined): JSONRPCResponse;
@@ -0,0 +1,48 @@
1
+ import { SERVER_INFO_META_KEY } from "@modelcontextprotocol/server";
2
+ import { serverIdentitySnapshot } from "./server-identity.js";
3
+ /**
4
+ * Centralized modern-era decoration for supervisor-synthesized replies.
5
+ *
6
+ * Contract:
7
+ * - Decorates ONLY when the originating request's snapshot era is "modern"
8
+ * (the era captured at admission; it is the ONLY captured-context field
9
+ * synthetic decoration may read — protocolVersion/clientCapabilities/
10
+ * clientInfo stay unread, and the identity comes from the canonical
11
+ * src/server-identity.ts module, never from request context).
12
+ * - Modern-era RESULT envelopes gain the modern public fields the SDK stamps
13
+ * on every result it encodes: top-level `resultType: "complete"` (every
14
+ * result this server emits is complete — synthetic results are all
15
+ * tools/call-shaped, where "complete" is always correct) and
16
+ * `_meta[SERVER_INFO_META_KEY]` carrying the canonical identity.
17
+ * - Raw JSON-RPC ERROR envelopes and non-modern eras pass through UNCHANGED
18
+ * (same reference), keeping the legacy synthetic shapes byte-identical to
19
+ * the premigration fixtures (pinned by the synthetic-inventory suite).
20
+ * - Cache fields (ttlMs/cacheScope) are NEVER added: they belong only to the
21
+ * six cacheable list/read/discover methods, none of which has a structured
22
+ * synthetic path.
23
+ */
24
+ export function decorateSyntheticReply(reply, era) {
25
+ if (era !== "modern") {
26
+ return reply;
27
+ }
28
+ const asRecord = reply;
29
+ if ("error" in asRecord || !("result" in asRecord)) {
30
+ return reply;
31
+ }
32
+ const result = asRecord.result;
33
+ const existingMeta = result._meta !== null && typeof result._meta === "object" && !Array.isArray(result._meta)
34
+ ? result._meta
35
+ : {};
36
+ return {
37
+ ...asRecord,
38
+ result: {
39
+ ...result,
40
+ resultType: "complete",
41
+ _meta: {
42
+ ...existingMeta,
43
+ [SERVER_INFO_META_KEY]: serverIdentitySnapshot()
44
+ }
45
+ }
46
+ };
47
+ }
48
+ //# sourceMappingURL=synthetic-decorator.js.map
@@ -32,7 +32,7 @@ const SECTION_ROWS = {
32
32
  en: [
33
33
  "| `list-versions` | List available Minecraft versions from Mojang metadata and local cache |",
34
34
  "| `resolve-artifact` | Resolve source artifacts from versions, JAR paths, or Maven coordinates |",
35
- "| `find-class` | Find simple or fully-qualified class names inside an artifact |",
35
+ "| `find-class` | Find simple or fully-qualified class names, including classes bundled in nested JARs |",
36
36
  "| `get-class-source` | Read class source from an artifact or resolve the backing artifact on demand |",
37
37
  "| `get-class-members` | List constructors, fields, and methods from bytecode |",
38
38
  "| `search-class-source` | Search indexed class source by symbol, text, or path |",
@@ -43,7 +43,7 @@ export declare const ANALYZE_MOD_LEGACY_METADATA_INCLUDES: readonly ["metadata",
43
43
  export declare const VALIDATE_PROJECT_INCLUDE_GROUPS: readonly ["warnings", "issues", "workspace", "recovery"];
44
44
  export declare const VALIDATE_PROJECT_LEGACY_WORKSPACE_INCLUDES: readonly ["detectedConfig", "mixins", "accessWideners"];
45
45
  export declare const VALIDATION_FALLBACK_HINT = "suggested call payload failed schema validation; using fallback examples";
46
- export declare function toFieldErrorsFromZod(error: ZodError): ProblemFieldError[];
46
+ export declare function toFieldErrorsFromZod(error: ZodError, ...rawInput: [unknown?]): ProblemFieldError[];
47
47
  export declare function toHints(details: unknown): string[] | undefined;
48
48
  export declare function extractValidatedSuggestionAndExamples(details: unknown): {
49
49
  suggestedCall?: SuggestedCall;
@@ -73,6 +73,22 @@ type InvalidInputGuidance = {
73
73
  primaryDropped?: boolean;
74
74
  };
75
75
  export declare function buildInvalidInputGuidance(tool: string, normalizedInput: unknown): InvalidInputGuidance | undefined;
76
+ /**
77
+ * Removes an ask for a parameter the caller already sent.
78
+ *
79
+ * `validate-access-transformer` failed on the canonical NeoForge workspace with
80
+ * "Provide projectPath for a Forge/NeoForge workspace ..." — while projectPath
81
+ * WAS supplied. A hint that asks for what it already has is worse than no hint:
82
+ * it sends the agent into a retry loop with an identical payload. The producing
83
+ * sites word their guidance conditionally; this is the backstop that keeps any
84
+ * remaining site from contradicting the request.
85
+ *
86
+ * The ask is a leading clause, so an "..., or <alternative>" tail survives as
87
+ * its own sentence; a hint that is nothing BUT a satisfied ask is dropped.
88
+ * Applies only to execution errors — an input-validation failure may legitimately
89
+ * ask again for a parameter that was present but malformed.
90
+ */
91
+ export declare function dropSatisfiedParameterAsks(hints: string[], normalizedInput: unknown): string[];
76
92
  export declare function mapErrorToProblem(caughtError: unknown, requestId: string, context?: {
77
93
  tool?: string;
78
94
  normalizedInput?: unknown;
@@ -26,12 +26,241 @@ export const ANALYZE_MOD_LEGACY_METADATA_INCLUDES = ["metadata", "entrypoints",
26
26
  export const VALIDATE_PROJECT_INCLUDE_GROUPS = ["warnings", "issues", "workspace", "recovery"];
27
27
  export const VALIDATE_PROJECT_LEGACY_WORKSPACE_INCLUDES = ["detectedConfig", "mixins", "accessWideners"];
28
28
  export const VALIDATION_FALLBACK_HINT = "suggested call payload failed schema validation; using fallback examples";
29
- export function toFieldErrorsFromZod(error) {
30
- return error.issues.map((issue) => ({
31
- path: issue.path.join(".") || "$",
32
- message: issue.message,
33
- code: issue.code
34
- }));
29
+ /** zod3's getParsedType vocabulary for `received` words. */
30
+ function zod3ParsedType(data) {
31
+ switch (typeof data) {
32
+ case "undefined":
33
+ return "undefined";
34
+ case "string":
35
+ return "string";
36
+ case "number":
37
+ return Number.isNaN(data) ? "nan" : "number";
38
+ case "boolean":
39
+ return "boolean";
40
+ case "function":
41
+ return "function";
42
+ case "bigint":
43
+ return "bigint";
44
+ case "symbol":
45
+ return "symbol";
46
+ case "object":
47
+ if (data === null)
48
+ return "null";
49
+ if (Array.isArray(data))
50
+ return "array";
51
+ if (typeof data.then === "function")
52
+ return "promise";
53
+ if (data instanceof Map)
54
+ return "map";
55
+ if (data instanceof Set)
56
+ return "set";
57
+ if (data instanceof Date)
58
+ return "date";
59
+ return "object";
60
+ default:
61
+ return "unknown";
62
+ }
63
+ }
64
+ /** zod3's util.joinValues: single-quote strings, stringify the rest. */
65
+ function zod3JoinValues(values, separator = " | ") {
66
+ return values
67
+ .map((value) => (typeof value === "string" ? `'${value}'` : String(value)))
68
+ .join(separator);
69
+ }
70
+ function valueAtPath(root, path) {
71
+ let current = root;
72
+ for (const segment of path) {
73
+ if (current === null || typeof current !== "object")
74
+ return undefined;
75
+ current = current[segment];
76
+ }
77
+ return current;
78
+ }
79
+ function zod3SizeMessage(issue, kind) {
80
+ const origin = issue.origin === "int" ? "number" : issue.origin;
81
+ const limit = kind === "small" ? issue.minimum : issue.maximum;
82
+ if (limit === undefined)
83
+ return undefined;
84
+ const inclusive = issue.inclusive !== false;
85
+ const exact = issue.exact === true;
86
+ switch (origin) {
87
+ case "array":
88
+ case "set":
89
+ return kind === "small"
90
+ ? `Array must contain ${exact ? "exactly" : inclusive ? "at least" : "more than"} ${limit} element(s)`
91
+ : `Array must contain ${exact ? "exactly" : inclusive ? "at most" : "less than"} ${limit} element(s)`;
92
+ case "string":
93
+ return kind === "small"
94
+ ? `String must contain ${exact ? "exactly" : inclusive ? "at least" : "over"} ${limit} character(s)`
95
+ : `String must contain ${exact ? "exactly" : inclusive ? "at most" : "under"} ${limit} character(s)`;
96
+ case "number":
97
+ return kind === "small"
98
+ ? `Number must be ${exact ? "exactly equal to" : inclusive ? "greater than or equal to" : "greater than"} ${limit}`
99
+ : `Number must be ${exact ? "exactly" : inclusive ? "less than or equal to" : "less than"} ${limit}`;
100
+ case "bigint":
101
+ return kind === "small"
102
+ ? `BigInt must be ${exact ? "exactly equal to" : inclusive ? "greater than or equal to" : "greater than"} ${limit}`
103
+ : `BigInt must be ${exact ? "exactly" : inclusive ? "less than or equal to" : "less than"} ${limit}`;
104
+ case "date":
105
+ return kind === "small"
106
+ ? `Date must be ${exact ? "exactly equal to" : inclusive ? "greater than or equal to" : "greater than"} ${new Date(Number(limit))}`
107
+ : `Date must be ${exact ? "exactly" : inclusive ? "smaller than or equal to" : "smaller than"} ${new Date(Number(limit))}`;
108
+ default:
109
+ return undefined;
110
+ }
111
+ }
112
+ const ZOD4_INVALID_TYPE_RE = /^Invalid input: expected (\S+), received (.+)$/;
113
+ const ZOD4_ENUM_RE = /^Invalid option: expected one of /;
114
+ const ZOD4_LITERAL_RE = /^Invalid input: expected \S+$/;
115
+ const ZOD4_TOO_SMALL_RE = /^Too small: /;
116
+ const ZOD4_TOO_BIG_RE = /^Too big: /;
117
+ const ZOD4_UNRECOGNIZED_RE = /^Unrecognized keys?: /;
118
+ /**
119
+ * Maps one zod4 issue to its zod3-era {code, message}. Issues whose message
120
+ * does not match the zod4 default pattern carry app-supplied custom text and
121
+ * pass through unchanged (zod3 surfaced custom messages verbatim too).
122
+ * `hasInput`/`rawInput` supply the original parse input for the `received`
123
+ * clauses zod4 no longer records on the issue.
124
+ */
125
+ function toZod3ParityIssue(issue, rawInput, hasInput) {
126
+ const code = issue.code ?? "custom";
127
+ const receivedValue = () => {
128
+ if ("input" in issue)
129
+ return { known: true, value: issue.input };
130
+ if (hasInput)
131
+ return { known: true, value: valueAtPath(rawInput, issue.path) };
132
+ return { known: false, value: undefined };
133
+ };
134
+ switch (code) {
135
+ case "invalid_type": {
136
+ const match = ZOD4_INVALID_TYPE_RE.exec(issue.message);
137
+ if (!match)
138
+ return { code, message: issue.message };
139
+ const received = receivedValue();
140
+ const receivedWord = received.known
141
+ ? zod3ParsedType(received.value)
142
+ : match[2] === "NaN"
143
+ ? "nan"
144
+ : match[2];
145
+ if (receivedWord === "undefined") {
146
+ return { code, message: "Required" };
147
+ }
148
+ // zod3's .int() check emitted the hardcoded pair "integer"/"float"
149
+ // (ZodNumber._parse), not the parsed-type vocabulary. zod4 reports
150
+ // expected "int" and a plain "number" received.
151
+ if ((issue.expected ?? match[1]) === "int") {
152
+ const isNonIntegerNumber = received.known && typeof received.value === "number" && !Number.isInteger(received.value);
153
+ if (isNonIntegerNumber || (!received.known && match[2] === "number")) {
154
+ return { code, message: "Expected integer, received float" };
155
+ }
156
+ return { code, message: `Expected integer, received ${receivedWord}` };
157
+ }
158
+ return { code, message: `Expected ${issue.expected ?? match[1]}, received ${receivedWord}` };
159
+ }
160
+ case "invalid_value": {
161
+ const values = issue.values ?? [];
162
+ // zod3 reported a MISSING enum/literal field as invalid_type/"Required"
163
+ // (the type check on `undefined` fired before value matching); zod4
164
+ // folds it into invalid_value. Restore the zod3 classification.
165
+ const missing = receivedValue();
166
+ if (missing.known && missing.value === undefined) {
167
+ return { code: "invalid_type", message: "Required" };
168
+ }
169
+ // zod3's ZodEnum type-checked BEFORE value matching: a non-string
170
+ // received value produced invalid_type with the joined options as the
171
+ // "expected" word and the parsed type as "received".
172
+ if (values.length > 1 && missing.known && typeof missing.value !== "string") {
173
+ return {
174
+ code: "invalid_type",
175
+ message: `Expected ${zod3JoinValues(values)}, received ${zod3ParsedType(missing.value)}`
176
+ };
177
+ }
178
+ if (values.length > 1 && ZOD4_ENUM_RE.test(issue.message)) {
179
+ const received = receivedValue();
180
+ return {
181
+ code: "invalid_enum_value",
182
+ message: `Invalid enum value. Expected ${zod3JoinValues(values)}, received '${String(received.value)}'`
183
+ };
184
+ }
185
+ if (values.length === 1 && ZOD4_LITERAL_RE.test(issue.message)) {
186
+ return {
187
+ code: "invalid_literal",
188
+ message: `Invalid literal value, expected ${JSON.stringify(values[0])}`
189
+ };
190
+ }
191
+ return { code: values.length > 1 ? "invalid_enum_value" : "invalid_literal", message: issue.message };
192
+ }
193
+ case "invalid_union": {
194
+ if (issue.note === "No matching discriminator") {
195
+ return {
196
+ code: "invalid_union_discriminator",
197
+ message: Array.isArray(issue.options)
198
+ ? `Invalid discriminator value. Expected ${zod3JoinValues(issue.options)}`
199
+ : issue.message
200
+ };
201
+ }
202
+ return { code, message: issue.message === "Invalid input" ? "Invalid input" : issue.message };
203
+ }
204
+ case "too_small": {
205
+ if (!ZOD4_TOO_SMALL_RE.test(issue.message))
206
+ return { code, message: issue.message };
207
+ return { code, message: zod3SizeMessage(issue, "small") ?? issue.message };
208
+ }
209
+ case "too_big": {
210
+ if (!ZOD4_TOO_BIG_RE.test(issue.message))
211
+ return { code, message: issue.message };
212
+ return { code, message: zod3SizeMessage(issue, "big") ?? issue.message };
213
+ }
214
+ case "unrecognized_keys": {
215
+ if (!ZOD4_UNRECOGNIZED_RE.test(issue.message))
216
+ return { code, message: issue.message };
217
+ const keys = issue.keys ?? [];
218
+ return {
219
+ code,
220
+ message: `Unrecognized key(s) in object: ${zod3JoinValues(keys, ", ")}`
221
+ };
222
+ }
223
+ case "not_multiple_of": {
224
+ return {
225
+ code,
226
+ message: issue.divisor === undefined
227
+ ? issue.message
228
+ : `Number must be a multiple of ${issue.divisor}`
229
+ };
230
+ }
231
+ case "invalid_format": {
232
+ if (!issue.message.startsWith("Invalid "))
233
+ return { code: "invalid_string", message: issue.message };
234
+ switch (issue.format) {
235
+ case "regex":
236
+ return { code: "invalid_string", message: "Invalid" };
237
+ case "starts_with":
238
+ return { code: "invalid_string", message: `Invalid input: must start with "${issue.prefix ?? ""}"` };
239
+ case "ends_with":
240
+ return { code: "invalid_string", message: `Invalid input: must end with "${issue.suffix ?? ""}"` };
241
+ case "includes":
242
+ return { code: "invalid_string", message: `Invalid input: must include "${issue.includes ?? ""}"` };
243
+ default:
244
+ return { code: "invalid_string", message: `Invalid ${issue.format ?? "string"}` };
245
+ }
246
+ }
247
+ case "custom":
248
+ return { code, message: issue.message };
249
+ default:
250
+ return { code, message: issue.message };
251
+ }
252
+ }
253
+ export function toFieldErrorsFromZod(error, ...rawInput) {
254
+ const hasInput = rawInput.length > 0;
255
+ const input = rawInput[0];
256
+ return error.issues.map((issue) => {
257
+ const parity = toZod3ParityIssue(issue, input, hasInput);
258
+ return {
259
+ path: issue.path.join(".") || "$",
260
+ message: parity.message,
261
+ code: parity.code
262
+ };
263
+ });
35
264
  }
36
265
  export function toHints(details) {
37
266
  if (typeof details !== "object" || details == null) {
@@ -656,7 +885,103 @@ function gatedGuidance(tool, hints, params) {
656
885
  primaryDropped: !validated.suggestedCall
657
886
  };
658
887
  }
888
+ function buildInspectWorkspaceSubject(source) {
889
+ const subject = {
890
+ kind: "workspace",
891
+ projectPath: asNonEmptyString(source.projectPath) ?? "<workspace-path>"
892
+ };
893
+ if (typeof source.mapping === "string" &&
894
+ ["obfuscated", "mojang", "intermediary", "yarn"].includes(source.mapping)) {
895
+ subject.mapping = source.mapping;
896
+ }
897
+ if (typeof source.scope === "string" &&
898
+ ["vanilla", "merged", "loader"].includes(source.scope)) {
899
+ subject.scope = source.scope;
900
+ }
901
+ const gradleUserHome = asNonEmptyString(source.gradleUserHome);
902
+ if (gradleUserHome) {
903
+ subject.gradleUserHome = gradleUserHome;
904
+ }
905
+ if (typeof source.preferProjectVersion === "boolean") {
906
+ subject.preferProjectVersion = source.preferProjectVersion;
907
+ }
908
+ if (typeof source.strictVersion === "boolean") {
909
+ subject.strictVersion = source.strictVersion;
910
+ }
911
+ return subject;
912
+ }
913
+ function inspectTaskForFocus(task, focusKind) {
914
+ if (focusKind === "class" &&
915
+ (task === "auto" || task === "class-overview" || task === "class-source" || task === "class-members")) {
916
+ return task;
917
+ }
918
+ if (focusKind === "search" && (task === "auto" || task === "search")) {
919
+ return task;
920
+ }
921
+ if (focusKind === "file" && (task === "auto" || task === "file")) {
922
+ return task;
923
+ }
924
+ return "auto";
925
+ }
926
+ function buildInspectMinecraftInvalidFocusGuidance(normalizedInput) {
927
+ const input = asObjectRecord(normalizedInput);
928
+ const originalSubject = asObjectRecord(input?.subject);
929
+ if (originalSubject?.kind !== "workspace" ||
930
+ originalSubject.focus === undefined ||
931
+ asObjectRecord(originalSubject.focus)) {
932
+ return undefined;
933
+ }
934
+ const requestedTask = asNonEmptyString(input?.task);
935
+ const workspaceSubject = buildInspectWorkspaceSubject(originalSubject);
936
+ const examples = [
937
+ {
938
+ params: {
939
+ task: inspectTaskForFocus(requestedTask, "class"),
940
+ subject: {
941
+ ...workspaceSubject,
942
+ focus: { kind: "class", className: "<fully-qualified-class-name>" }
943
+ }
944
+ },
945
+ reason: "Use class focus for class overview, source, or member inspection."
946
+ },
947
+ {
948
+ params: {
949
+ task: inspectTaskForFocus(requestedTask, "search"),
950
+ subject: {
951
+ ...workspaceSubject,
952
+ focus: { kind: "search", query: "<search-query>" }
953
+ }
954
+ },
955
+ reason: "Use search focus for symbol, text, or path search."
956
+ },
957
+ {
958
+ params: {
959
+ task: inspectTaskForFocus(requestedTask, "file"),
960
+ subject: {
961
+ ...workspaceSubject,
962
+ focus: { kind: "file", filePath: "<artifact-relative-file-path>" }
963
+ }
964
+ },
965
+ reason: "Use file focus for an artifact-relative file read."
966
+ }
967
+ ];
968
+ const validated = buildSuggestedCall({
969
+ tool: "inspect-minecraft",
970
+ params: undefined,
971
+ examples
972
+ });
973
+ return {
974
+ hints: [
975
+ "inspect-minecraft subject.focus must be a structured object, not a string.",
976
+ "Choose focus.kind=class with className, search with query, or file with filePath; task=auto dispatches from that kind and does not interpret prose."
977
+ ],
978
+ ...(validated.exampleCalls ? { exampleCalls: validated.exampleCalls } : {})
979
+ };
980
+ }
659
981
  export function buildInvalidInputGuidance(tool, normalizedInput) {
982
+ if (tool === "inspect-minecraft") {
983
+ return buildInspectMinecraftInvalidFocusGuidance(normalizedInput);
984
+ }
660
985
  if (tool === "validate-mixin") {
661
986
  return gatedGuidance(tool, [
662
987
  "validate-mixin.input must be an object with input.mode = \"inline\" | \"path\" | \"paths\" | \"config\" | \"project\".",
@@ -692,6 +1017,78 @@ export function buildInvalidInputGuidance(tool, normalizedInput) {
692
1017
  }
693
1018
  return undefined;
694
1019
  }
1020
+ /**
1021
+ * Parameters a hint may ask for, and which are meaningful to check against the
1022
+ * caller's own payload. Restricted to top-level scalar/object inputs so the
1023
+ * check never mistakes a nested field for a supplied argument.
1024
+ */
1025
+ const HINT_ASKABLE_PARAMETERS = [
1026
+ "projectPath",
1027
+ "gradleUserHome",
1028
+ "version",
1029
+ "artifactId",
1030
+ "mapping",
1031
+ "atNamespace",
1032
+ "awNamespace",
1033
+ "scope",
1034
+ "className",
1035
+ "content",
1036
+ "jarPath",
1037
+ "mixinConfigPath",
1038
+ "sourcePriority"
1039
+ ];
1040
+ const HINT_ASK_PATTERN = new RegExp(`\\b(?:Provide|Pass|Specify|Supply) (?:a |an |the )?(${HINT_ASKABLE_PARAMETERS.join("|")})\\b`);
1041
+ function isSuppliedInputValue(value) {
1042
+ if (value == null)
1043
+ return false;
1044
+ if (typeof value === "string")
1045
+ return value.trim().length > 0;
1046
+ if (Array.isArray(value))
1047
+ return value.length > 0;
1048
+ return true;
1049
+ }
1050
+ /**
1051
+ * Removes an ask for a parameter the caller already sent.
1052
+ *
1053
+ * `validate-access-transformer` failed on the canonical NeoForge workspace with
1054
+ * "Provide projectPath for a Forge/NeoForge workspace ..." — while projectPath
1055
+ * WAS supplied. A hint that asks for what it already has is worse than no hint:
1056
+ * it sends the agent into a retry loop with an identical payload. The producing
1057
+ * sites word their guidance conditionally; this is the backstop that keeps any
1058
+ * remaining site from contradicting the request.
1059
+ *
1060
+ * The ask is a leading clause, so an "..., or <alternative>" tail survives as
1061
+ * its own sentence; a hint that is nothing BUT a satisfied ask is dropped.
1062
+ * Applies only to execution errors — an input-validation failure may legitimately
1063
+ * ask again for a parameter that was present but malformed.
1064
+ */
1065
+ export function dropSatisfiedParameterAsks(hints, normalizedInput) {
1066
+ if (hints.length === 0 ||
1067
+ typeof normalizedInput !== "object" ||
1068
+ normalizedInput == null ||
1069
+ Array.isArray(normalizedInput)) {
1070
+ return hints;
1071
+ }
1072
+ const supplied = normalizedInput;
1073
+ const rewritten = [];
1074
+ for (const hint of hints) {
1075
+ const match = HINT_ASK_PATTERN.exec(hint);
1076
+ if (!match || !isSuppliedInputValue(supplied[match[1]])) {
1077
+ rewritten.push(hint);
1078
+ continue;
1079
+ }
1080
+ const alternative = hint.indexOf(", or ");
1081
+ if (alternative < 0) {
1082
+ continue;
1083
+ }
1084
+ const tail = hint.slice(alternative + ", or ".length).trim();
1085
+ if (!tail) {
1086
+ continue;
1087
+ }
1088
+ rewritten.push(tail.charAt(0).toUpperCase() + tail.slice(1));
1089
+ }
1090
+ return rewritten;
1091
+ }
695
1092
  export function mapErrorToProblem(caughtError, requestId, context) {
696
1093
  if (caughtError instanceof ZodError) {
697
1094
  const guidance = context?.tool
@@ -710,7 +1107,7 @@ export function mapErrorToProblem(caughtError, requestId, context) {
710
1107
  instance: requestId,
711
1108
  retryClass: retryClassForErrorCode(ERROR_CODES.INVALID_INPUT),
712
1109
  issueOrigin: issueOriginForErrorCode(ERROR_CODES.INVALID_INPUT),
713
- fieldErrors: toFieldErrorsFromZod(caughtError),
1110
+ fieldErrors: toFieldErrorsFromZod(caughtError, context?.normalizedInput),
714
1111
  hints: hintsWithFallback,
715
1112
  ...(guidance?.suggestedCall ? { suggestedCall: guidance.suggestedCall } : {}),
716
1113
  ...(guidance?.exampleCalls ? { exampleCalls: guidance.exampleCalls } : {}),
@@ -719,6 +1116,13 @@ export function mapErrorToProblem(caughtError, requestId, context) {
719
1116
  }
720
1117
  if (isAppError(caughtError)) {
721
1118
  const { suggestedCall, exampleCalls, primaryDropped } = extractValidatedSuggestionAndExamples(caughtError.details);
1119
+ const invalidInputGuidance = context?.tool === "inspect-minecraft" && caughtError.code === ERROR_CODES.INVALID_INPUT
1120
+ ? buildInvalidInputGuidance(context.tool, context.normalizedInput)
1121
+ : undefined;
1122
+ const effectiveSuggestedCall = invalidInputGuidance
1123
+ ? invalidInputGuidance.suggestedCall
1124
+ : suggestedCall;
1125
+ const effectiveExampleCalls = invalidInputGuidance?.exampleCalls ?? exampleCalls;
722
1126
  const sanitizedContext = extractAllowlistedContext(caughtError.details);
723
1127
  const extractedDidYouMean = extractDidYouMean(caughtError.details);
724
1128
  let failedStage = extractFailedStageFromDetails(caughtError.details);
@@ -727,10 +1131,10 @@ export function mapErrorToProblem(caughtError, requestId, context) {
727
1131
  && caughtError.code === ERROR_CODES.INVALID_INPUT) {
728
1132
  failedStage = "input-validation";
729
1133
  }
730
- const baseHints = toHints(caughtError.details);
731
- const hintsWithFallback = primaryDropped && !suggestedCall
732
- ? [...(baseHints ?? []), VALIDATION_FALLBACK_HINT]
733
- : baseHints;
1134
+ const baseHints = dropSatisfiedParameterAsks([...(toHints(caughtError.details) ?? []), ...(invalidInputGuidance?.hints ?? [])], context?.normalizedInput);
1135
+ const hintsWithFallback = primaryDropped && !effectiveSuggestedCall
1136
+ ? [...baseHints, VALIDATION_FALLBACK_HINT]
1137
+ : baseHints.length > 0 ? baseHints : undefined;
734
1138
  return {
735
1139
  type: `https://minecraft-modding-mcp.dev/problems/${caughtError.code.toLowerCase()}`,
736
1140
  title: "Tool execution error",
@@ -742,8 +1146,8 @@ export function mapErrorToProblem(caughtError, requestId, context) {
742
1146
  issueOrigin: issueOriginForErrorCode(caughtError.code),
743
1147
  fieldErrors: extractFieldErrorsFromDetails(caughtError.details),
744
1148
  hints: hintsWithFallback,
745
- ...(suggestedCall ? { suggestedCall } : {}),
746
- ...(exampleCalls ? { exampleCalls } : {}),
1149
+ ...(effectiveSuggestedCall ? { suggestedCall: effectiveSuggestedCall } : {}),
1150
+ ...(effectiveExampleCalls ? { exampleCalls: effectiveExampleCalls } : {}),
747
1151
  ...(extractedDidYouMean ? { didYouMean: extractedDidYouMean } : {}),
748
1152
  ...(failedStage ? { failedStage } : {}),
749
1153
  ...(sanitizedContext ? { context: sanitizedContext } : {})
@@ -13,4 +13,6 @@ export type ValidateToolParamsResult = {
13
13
  };
14
14
  export declare function validateToolParams(name: string, params: unknown): ValidateToolParamsResult;
15
15
  export declare function listRegisteredTools(): string[];
16
+ /** O(1) registry size — the populated-registry probe for per-request paths. */
17
+ export declare function registeredToolCount(): number;
16
18
  export declare function clearRegisteredToolsForTest(): void;