@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.
- package/CHANGELOG.md +71 -0
- package/README.md +21 -5
- package/dist/cache-policy.d.ts +71 -0
- package/dist/cache-policy.js +83 -0
- package/dist/cache-registry.js +6 -6
- package/dist/cli.js +74 -3
- package/dist/compat-stdio-transport.d.ts +1 -1
- package/dist/compat-stdio-transport.js +13 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.js +8 -2
- package/dist/decompiler/vineflower.d.ts +1 -0
- package/dist/decompiler/vineflower.js +8 -5
- package/dist/entry-tools/analyze-mod-service.d.ts +70 -136
- package/dist/entry-tools/analyze-symbol-service.d.ts +112 -150
- package/dist/entry-tools/compare-minecraft-service.d.ts +59 -145
- package/dist/entry-tools/entry-tool-schema.d.ts +38 -4
- package/dist/entry-tools/entry-tool-schema.js +4 -1
- package/dist/entry-tools/inspect-minecraft/internal.d.ts +235 -799
- package/dist/entry-tools/inspect-minecraft/internal.js +65 -28
- package/dist/entry-tools/inspect-minecraft-service.d.ts +372 -1736
- package/dist/entry-tools/inspect-minecraft-service.js +1 -1
- package/dist/entry-tools/manage-cache-service.d.ts +81 -91
- package/dist/entry-tools/validate-project/cases/project-summary.d.ts +7 -7
- package/dist/entry-tools/validate-project-service.d.ts +164 -592
- package/dist/entry-tools/verify-mixin-target-service.d.ts +3 -19
- package/dist/era-classifier.d.ts +161 -0
- package/dist/era-classifier.js +292 -0
- package/dist/error-mapping.js +9 -2
- package/dist/index.d.ts +42 -4
- package/dist/index.js +637 -475
- package/dist/java-process.d.ts +2 -0
- package/dist/java-process.js +22 -2
- package/dist/json-rpc-framing.d.ts +77 -1
- package/dist/json-rpc-framing.js +249 -13
- package/dist/mapping/loaders/tiny-loom-selection.d.ts +88 -0
- package/dist/mapping/loaders/tiny-loom-selection.js +223 -0
- package/dist/mapping/loaders/tiny-loom.js +45 -33
- package/dist/mapping/loaders/tiny-maven.js +6 -11
- package/dist/mapping/parsers/tiny.d.ts +57 -0
- package/dist/mapping/parsers/tiny.js +99 -22
- package/dist/mapping-service.d.ts +19 -0
- package/dist/mapping-service.js +93 -9
- package/dist/mcp-helpers.d.ts +19 -2
- package/dist/mcp-helpers.js +48 -6
- package/dist/minecraft-explorer-service.d.ts +1 -1
- package/dist/mixin/types.d.ts +8 -0
- package/dist/mod-analyzer.js +7 -7
- package/dist/mod-decompile-service.js +1 -0
- package/dist/nbt/java-nbt-codec.js +12 -2
- package/dist/nbt/json-patch.js +14 -3
- package/dist/nbt/pipeline.js +40 -3
- package/dist/nbt/typed-json.js +26 -1
- package/dist/registration-adapter.d.ts +32 -0
- package/dist/registration-adapter.js +52 -0
- package/dist/request-context.d.ts +7 -0
- package/dist/request-context.js +9 -0
- package/dist/resources.d.ts +1 -1
- package/dist/resources.js +25 -19
- package/dist/server-identity.d.ts +27 -0
- package/dist/server-identity.js +26 -0
- package/dist/source/access-validate.js +53 -0
- package/dist/source/artifact-resolver.d.ts +69 -1
- package/dist/source/artifact-resolver.js +215 -14
- package/dist/source/class-source.d.ts +22 -0
- package/dist/source/class-source.js +162 -29
- package/dist/source/did-you-mean.d.ts +12 -1
- package/dist/source/did-you-mean.js +6 -2
- package/dist/source/file-access.js +150 -46
- package/dist/source/indexer.js +1 -0
- package/dist/source/nested-jars.d.ts +19 -0
- package/dist/source/nested-jars.js +90 -21
- package/dist/source/shared-utils.d.ts +21 -0
- package/dist/source/shared-utils.js +23 -0
- package/dist/source-service.d.ts +12 -0
- package/dist/source-service.js +3 -0
- package/dist/stdio-supervisor.d.ts +357 -2
- package/dist/stdio-supervisor.js +1031 -80
- package/dist/storage/db.d.ts +2 -1
- package/dist/storage/db.js +15 -8
- package/dist/synthetic-decorator.d.ts +24 -0
- package/dist/synthetic-decorator.js +48 -0
- package/dist/tool-contract-manifest.js +1 -1
- package/dist/tool-guidance.d.ts +17 -1
- package/dist/tool-guidance.js +417 -13
- package/dist/tool-schema-registry.d.ts +2 -0
- package/dist/tool-schema-registry.js +4 -0
- package/dist/tool-schemas.d.ts +2214 -3915
- package/dist/tool-schemas.js +34 -7
- package/dist/types.d.ts +35 -0
- package/dist/v1-parity-schemas.d.ts +7 -0
- package/dist/v1-parity-schemas.js +5584 -0
- package/dist/version-diff-service.d.ts +33 -0
- package/dist/version-diff-service.js +148 -3
- package/dist/version-service.js +36 -14
- package/dist/warning-details.js +18 -1
- package/docs/README-ja.md +9 -3
- package/docs/tool-reference.md +267 -22
- package/package.json +12 -9
package/dist/storage/db.d.ts
CHANGED
|
@@ -10,5 +10,6 @@ export interface InitializedDatabase {
|
|
|
10
10
|
db: SqliteDatabase;
|
|
11
11
|
schemaVersion: number;
|
|
12
12
|
}
|
|
13
|
-
|
|
13
|
+
type DatabaseConfig = Pick<Config, "sqlitePath"> & Partial<Pick<Config, "sqliteCacheKb" | "sqliteMmapSize">>;
|
|
14
|
+
export declare function openDatabase(config: DatabaseConfig, logger?: Logger): InitializedDatabase;
|
|
14
15
|
export {};
|
package/dist/storage/db.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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 |",
|
package/dist/tool-guidance.d.ts
CHANGED
|
@@ -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;
|
package/dist/tool-guidance.js
CHANGED
|
@@ -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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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 && !
|
|
732
|
-
? [...
|
|
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
|
-
...(
|
|
746
|
-
...(
|
|
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;
|