@adhisang/minecraft-modding-mcp 2.0.0 → 2.1.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 (37) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +109 -29
  3. package/dist/cli.js +31 -4
  4. package/dist/compat-stdio-transport.d.ts +2 -7
  5. package/dist/compat-stdio-transport.js +12 -154
  6. package/dist/index.js +392 -33
  7. package/dist/json-rpc-framing.d.ts +22 -0
  8. package/dist/json-rpc-framing.js +168 -0
  9. package/dist/mapping-pipeline-service.js +9 -1
  10. package/dist/mapping-service.d.ts +9 -0
  11. package/dist/mapping-service.js +183 -60
  12. package/dist/minecraft-explorer-service.d.ts +0 -1
  13. package/dist/minecraft-explorer-service.js +119 -23
  14. package/dist/mixin-validator.d.ts +24 -2
  15. package/dist/mixin-validator.js +223 -98
  16. package/dist/mod-decompile-service.d.ts +5 -0
  17. package/dist/mod-decompile-service.js +40 -5
  18. package/dist/mod-remap-service.js +142 -30
  19. package/dist/path-resolver.js +41 -4
  20. package/dist/registry-service.d.ts +10 -1
  21. package/dist/registry-service.js +154 -22
  22. package/dist/search-hit-accumulator.js +23 -2
  23. package/dist/source-jar-reader.js +16 -2
  24. package/dist/source-resolver.js +6 -7
  25. package/dist/source-service.d.ts +42 -4
  26. package/dist/source-service.js +781 -127
  27. package/dist/stdio-supervisor.d.ts +46 -0
  28. package/dist/stdio-supervisor.js +349 -0
  29. package/dist/storage/files-repo.d.ts +3 -9
  30. package/dist/storage/files-repo.js +66 -43
  31. package/dist/symbols/symbol-extractor.js +6 -4
  32. package/dist/tool-execution-gate.d.ts +15 -0
  33. package/dist/tool-execution-gate.js +58 -0
  34. package/dist/version-diff-service.js +10 -5
  35. package/dist/version-service.js +7 -2
  36. package/dist/workspace-mapping-service.js +12 -0
  37. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ import { analyzeModJar } from "./mod-analyzer.js";
12
12
  import { remapModJar } from "./mod-remap-service.js";
13
13
  import { registerResources } from "./resources.js";
14
14
  import { SourceService } from "./source-service.js";
15
+ import { ToolExecutionGate } from "./tool-execution-gate.js";
15
16
  if (!process.env.NODE_ENV) {
16
17
  process.env.NODE_ENV = "production";
17
18
  }
@@ -28,6 +29,16 @@ const SOURCE_MODES = ["metadata", "snippet", "full"];
28
29
  const ARTIFACT_SCOPES = ["vanilla", "merged", "loader"];
29
30
  const DECODE_COMPRESSIONS = ["none", "gzip", "auto"];
30
31
  const ENCODE_COMPRESSIONS = ["none", "gzip"];
32
+ const HEAVY_TOOL_NAMES = new Set([
33
+ "trace-symbol-lifecycle",
34
+ "diff-class-signatures",
35
+ "compare-versions",
36
+ "find-mapping",
37
+ "resolve-method-mapping-exact",
38
+ "get-class-api-matrix",
39
+ "get-registry-data"
40
+ ]);
41
+ const heavyToolExecutionGate = new ToolExecutionGate({ maxConcurrent: 1, maxQueue: 2 });
31
42
  const nonEmptyString = z.string().trim().min(1);
32
43
  const optionalNonEmptyString = z.string().trim().min(1).optional();
33
44
  const optionalPositiveInt = z.number().int().positive().optional();
@@ -59,6 +70,9 @@ const sourceLookupTargetSchema = z.discriminatedUnion("type", [
59
70
  value: nonEmptyString
60
71
  })
61
72
  ]);
73
+ const RESOLVE_ARTIFACT_TARGET_DESCRIPTION = "Object with kind and value. Example: {\"kind\":\"version\",\"value\":\"1.21.10\"}. Must be an object, not a string.";
74
+ const SOURCE_LOOKUP_TARGET_DESCRIPTION = "Object: {\"type\":\"resolve\",\"kind\":\"version\",\"value\":\"1.21.10\"} or {\"type\":\"artifact\",\"artifactId\":\"...\"}. Must be an object, not a string.";
75
+ const SOURCE_SCOPE_DESCRIPTION = 'vanilla = Mojang client jar only; merged = Loom cache discovery (default); loader = currently behaves the same as "merged".';
62
76
  const listVersionsShape = {
63
77
  includeSnapshots: z.boolean().optional().describe("default false"),
64
78
  limit: optionalPositiveInt.describe("default 20, max 200")
@@ -68,12 +82,12 @@ const resolveArtifactShape = {
68
82
  target: z.object({
69
83
  kind: targetKindSchema,
70
84
  value: nonEmptyString
71
- }).describe("Resolve target: { kind: version|jar|coordinate, value }"),
85
+ }).describe(RESOLVE_ARTIFACT_TARGET_DESCRIPTION),
72
86
  mapping: sourceMappingSchema.optional().describe("obfuscated | mojang | intermediary | yarn"),
73
87
  sourcePriority: mappingSourcePrioritySchema.optional().describe("loom-first | maven-first"),
74
88
  allowDecompile: z.boolean().optional().describe("default true"),
75
89
  projectPath: optionalNonEmptyString.describe("Optional workspace root path for Loom cache-assisted source resolution"),
76
- scope: artifactScopeSchema.optional().describe("vanilla = Mojang client jar only; merged = Loom cache discovery (default); loader = loader-specific"),
90
+ scope: artifactScopeSchema.optional().describe(SOURCE_SCOPE_DESCRIPTION),
77
91
  preferProjectVersion: z.boolean().optional().describe("When true, detect MC version from gradle.properties and override target.value"),
78
92
  strictVersion: z.boolean().optional().describe("When true, reject version-approximated results instead of returning them. Default false.")
79
93
  };
@@ -81,12 +95,12 @@ const resolveArtifactSchema = z.object(resolveArtifactShape);
81
95
  const getClassSourceShape = {
82
96
  className: nonEmptyString,
83
97
  mode: sourceModeSchema.optional().describe("metadata (default) = symbol outline only; snippet = source with default maxLines=200; full = entire source"),
84
- target: sourceLookupTargetSchema.describe("Either { type: 'artifact', artifactId } or { type: 'resolve', kind, value }"),
98
+ target: sourceLookupTargetSchema.describe(SOURCE_LOOKUP_TARGET_DESCRIPTION),
85
99
  mapping: sourceMappingSchema.optional().describe("obfuscated | mojang | intermediary | yarn"),
86
100
  sourcePriority: mappingSourcePrioritySchema.optional().describe("loom-first | maven-first"),
87
101
  allowDecompile: z.boolean().optional().describe("default true"),
88
102
  projectPath: optionalNonEmptyString.describe("Optional workspace root path for Loom cache-assisted source resolution"),
89
- scope: artifactScopeSchema.optional().describe("vanilla = Mojang client jar only; merged = Loom cache discovery (default); loader = loader-specific"),
103
+ scope: artifactScopeSchema.optional().describe(SOURCE_SCOPE_DESCRIPTION),
90
104
  preferProjectVersion: z.boolean().optional().describe("When true, detect MC version from gradle.properties and override target.value"),
91
105
  strictVersion: z.boolean().optional().describe("When true, reject version-approximated results instead of returning them. Default false."),
92
106
  startLine: optionalPositiveInt,
@@ -110,7 +124,7 @@ const getClassSourceSchema = z
110
124
  });
111
125
  const getClassMembersShape = {
112
126
  className: nonEmptyString,
113
- target: sourceLookupTargetSchema.describe("Either { type: 'artifact', artifactId } or { type: 'resolve', kind, value }"),
127
+ target: sourceLookupTargetSchema.describe(SOURCE_LOOKUP_TARGET_DESCRIPTION),
114
128
  mapping: sourceMappingSchema.optional().describe("obfuscated | mojang | intermediary | yarn (default obfuscated)"),
115
129
  sourcePriority: mappingSourcePrioritySchema.optional().describe("loom-first | maven-first"),
116
130
  allowDecompile: z.boolean().optional().describe("default true"),
@@ -120,7 +134,7 @@ const getClassMembersShape = {
120
134
  memberPattern: optionalNonEmptyString,
121
135
  maxMembers: optionalPositiveInt.describe("default 500, max 5000"),
122
136
  projectPath: optionalNonEmptyString,
123
- scope: artifactScopeSchema.optional().describe("vanilla | merged | loader"),
137
+ scope: artifactScopeSchema.optional().describe(SOURCE_SCOPE_DESCRIPTION),
124
138
  preferProjectVersion: z.boolean().optional().describe("When true, detect MC version from gradle.properties and override version"),
125
139
  strictVersion: z.boolean().optional().describe("When true, reject version-approximated results instead of returning them. Default false.")
126
140
  };
@@ -176,7 +190,8 @@ const diffClassSignaturesShape = {
176
190
  fromVersion: nonEmptyString,
177
191
  toVersion: nonEmptyString,
178
192
  mapping: sourceMappingSchema.optional().describe("obfuscated | mojang | intermediary | yarn (default obfuscated)"),
179
- sourcePriority: mappingSourcePrioritySchema.optional().describe("loom-first | maven-first")
193
+ sourcePriority: mappingSourcePrioritySchema.optional().describe("loom-first | maven-first"),
194
+ includeFullDiff: z.boolean().optional().describe("When false, omit from/to snapshots from modified entries and keep only key+changed")
180
195
  };
181
196
  const diffClassSignaturesSchema = z.object(diffClassSignaturesShape);
182
197
  const findMappingShape = {
@@ -194,7 +209,8 @@ const findMappingShape = {
194
209
  descriptorHint: optionalNonEmptyString
195
210
  })
196
211
  .partial()
197
- .optional()
212
+ .optional(),
213
+ maxCandidates: optionalPositiveInt.describe("Limit returned candidates (default 200, max 200)")
198
214
  };
199
215
  const findMappingSchema = z.object(findMappingShape).superRefine((value, ctx) => {
200
216
  if (value.kind === "class") {
@@ -260,7 +276,8 @@ const resolveMethodMappingExactShape = {
260
276
  descriptor: nonEmptyString.describe("required JVM descriptor"),
261
277
  sourceMapping: sourceMappingSchema.describe("obfuscated | mojang | intermediary | yarn"),
262
278
  targetMapping: sourceMappingSchema.describe("obfuscated | mojang | intermediary | yarn"),
263
- sourcePriority: mappingSourcePrioritySchema.optional().describe("loom-first | maven-first")
279
+ sourcePriority: mappingSourcePrioritySchema.optional().describe("loom-first | maven-first"),
280
+ maxCandidates: optionalPositiveInt.describe("Limit returned candidates (default 200, max 200)")
264
281
  };
265
282
  const resolveMethodMappingExactSchema = z
266
283
  .object(resolveMethodMappingExactShape)
@@ -298,7 +315,8 @@ const getClassApiMatrixShape = {
298
315
  className: nonEmptyString,
299
316
  classNameMapping: sourceMappingSchema.describe("obfuscated | mojang | intermediary | yarn"),
300
317
  includeKinds: classApiKindsSchema.optional().describe("comma-separated: class,field,method"),
301
- sourcePriority: mappingSourcePrioritySchema.optional().describe("loom-first | maven-first")
318
+ sourcePriority: mappingSourcePrioritySchema.optional().describe("loom-first | maven-first"),
319
+ maxRows: optionalPositiveInt.describe("Limit returned rows (max 5000)")
302
320
  };
303
321
  const getClassApiMatrixSchema = z.object(getClassApiMatrixShape);
304
322
  const resolveWorkspaceSymbolShape = {
@@ -309,7 +327,8 @@ const resolveWorkspaceSymbolShape = {
309
327
  owner: optionalNonEmptyString,
310
328
  descriptor: optionalNonEmptyString,
311
329
  sourceMapping: sourceMappingSchema.describe("obfuscated | mojang | intermediary | yarn"),
312
- sourcePriority: mappingSourcePrioritySchema.optional().describe("loom-first | maven-first")
330
+ sourcePriority: mappingSourcePrioritySchema.optional().describe("loom-first | maven-first"),
331
+ maxCandidates: optionalPositiveInt.describe("Limit returned candidates for field/method lookups (default 200, max 200)")
313
332
  };
314
333
  const resolveWorkspaceSymbolSchema = z
315
334
  .object(resolveWorkspaceSymbolShape)
@@ -380,7 +399,8 @@ const checkSymbolExistsShape = {
380
399
  sourcePriority: mappingSourcePrioritySchema.optional().describe("loom-first | maven-first"),
381
400
  nameMode: classNameModeSchema.optional().describe("fqcn | auto (default fqcn)"),
382
401
  signatureMode: z.enum(["exact", "name-only"]).optional()
383
- .describe("exact (default): require descriptor for methods; name-only: match by owner+name only")
402
+ .describe("exact (default): require descriptor for methods; name-only: match by owner+name only"),
403
+ maxCandidates: optionalPositiveInt.describe("Limit returned candidates (default 200, max 200)")
384
404
  };
385
405
  const checkSymbolExistsSchema = z.object(checkSymbolExistsShape).superRefine((value, ctx) => {
386
406
  if (value.kind === "class") {
@@ -483,14 +503,18 @@ const validateMixinShape = {
483
503
  z.object({
484
504
  mode: z.literal("config"),
485
505
  configPaths: z.array(nonEmptyString).min(1).describe("Path array to mixin config JSON files (e.g. modid.mixins.json)")
506
+ }),
507
+ z.object({
508
+ mode: z.literal("project"),
509
+ path: nonEmptyString.describe("Workspace root path used to discover *.mixins.json files automatically")
486
510
  })
487
- ]),
511
+ ]).describe("One of { mode: 'inline', source }, { mode: 'path', path }, { mode: 'paths', paths[] }, { mode: 'config', configPaths[] }, or { mode: 'project', path }."),
488
512
  sourceRoots: z.array(z.string().min(1)).optional()
489
513
  .describe("Array of source roots for multi-module projects (e.g. ['common/src/main/java', 'neoforge/src/main/java'])"),
490
514
  version: nonEmptyString.describe("Minecraft version"),
491
515
  mapping: sourceMappingSchema.optional().describe("obfuscated | mojang | intermediary | yarn"),
492
516
  sourcePriority: mappingSourcePrioritySchema.optional().describe("loom-first | maven-first"),
493
- scope: artifactScopeSchema.optional().describe("vanilla | merged | loader"),
517
+ scope: artifactScopeSchema.optional().describe(SOURCE_SCOPE_DESCRIPTION),
494
518
  projectPath: optionalNonEmptyString.describe("Optional workspace root path for Loom cache-assisted source resolution"),
495
519
  preferProjectVersion: z.boolean().optional().describe("When true, detect MC version from gradle.properties and override version"),
496
520
  minSeverity: z.enum(["error", "warning", "all"]).optional()
@@ -503,12 +527,14 @@ const validateMixinShape = {
503
527
  .describe("'full'=all warnings (default), 'aggregated'=group warnings by category with counts and samples"),
504
528
  preferProjectMapping: z.boolean().optional()
505
529
  .describe("When true, auto-detect mapping from project config even if mapping is explicitly provided"),
506
- reportMode: z.enum(["compact", "full"]).optional()
507
- .describe("'compact' omits resolvedMembers/structuredWarnings/toolHealth details, 'full'=everything (default)"),
530
+ reportMode: z.enum(["compact", "full", "summary-first"]).optional()
531
+ .describe("'compact' omits heavy per-result detail, 'summary-first' hoists shared provenance/warnings/incomplete reasons, 'full'=everything (default)"),
508
532
  warningCategoryFilter: z.array(z.enum(["mapping", "configuration", "validation", "resolution", "parse"])).optional()
509
533
  .describe("Only include warnings/issues matching these categories (default: all)"),
510
534
  treatInfoAsWarning: z.boolean().optional()
511
- .describe("When false, suppress info-severity structured warnings from output (default true)")
535
+ .describe("When false, suppress info-severity structured warnings from output (default true)"),
536
+ includeIssues: z.boolean().optional()
537
+ .describe("When false, keep summary fields but omit per-result issues[] payloads")
512
538
  };
513
539
  const validateMixinSchema = z.object(validateMixinShape);
514
540
  const validateAccessWidenerShape = {
@@ -525,7 +551,9 @@ const analyzeModJarShape = {
525
551
  const analyzeModJarSchema = z.object(analyzeModJarShape);
526
552
  const getRegistryDataShape = {
527
553
  version: nonEmptyString.describe("Minecraft version (e.g. 1.21)"),
528
- registry: optionalNonEmptyString.describe('Optional registry name (e.g. "block", "item", "minecraft:biome"). Omit to list all registries.')
554
+ registry: optionalNonEmptyString.describe('Optional registry name (e.g. "block", "item", "minecraft:biome"). Omit to list all registries.'),
555
+ includeData: z.boolean().optional().describe("When false, return registry names/counts without full entry bodies"),
556
+ maxEntriesPerRegistry: optionalPositiveInt.describe("Limit returned entries per registry body")
529
557
  };
530
558
  const getRegistryDataSchema = z.object(getRegistryDataShape);
531
559
  const COMPARE_VERSIONS_CATEGORIES = ["classes", "registry", "all"];
@@ -540,7 +568,9 @@ const compareVersionsShape = {
540
568
  const compareVersionsSchema = z.object(compareVersionsShape);
541
569
  const decompileModJarShape = {
542
570
  jarPath: nonEmptyString.describe("Local path to the mod JAR file"),
543
- className: optionalNonEmptyString.describe("Optional fully-qualified class name to view source. Omit to list all classes.")
571
+ className: optionalNonEmptyString.describe("Optional fully-qualified class name to view source. Omit to list all classes."),
572
+ includeFiles: z.boolean().optional().describe("When false, omit the full class list and return counts only"),
573
+ maxFiles: optionalPositiveInt.describe("Limit returned class names when files are included")
544
574
  };
545
575
  const decompileModJarSchema = z.object(decompileModJarShape);
546
576
  const getModClassSourceShape = {
@@ -588,6 +618,9 @@ const server = new McpServer({
588
618
  name: "@adhisang/minecraft-modding-mcp",
589
619
  version: SERVER_VERSION
590
620
  });
621
+ // The SDK validates tool args before invoking handlers and returns generic InvalidParams text.
622
+ // Bypass that layer so runTool() remains the single source of truth for validation and error envelopes.
623
+ server.validateToolInput = async (_tool, args) => args;
591
624
  const config = loadConfig();
592
625
  const nbtLimits = {
593
626
  maxInputBytes: config.maxNbtInputBytes,
@@ -768,8 +801,314 @@ function extractFieldErrorsFromDetails(details) {
768
801
  .filter((entry) => entry != null);
769
802
  return normalized.length > 0 ? normalized : undefined;
770
803
  }
771
- function mapErrorToProblem(caughtError, requestId) {
804
+ function asObjectRecord(value) {
805
+ return typeof value === "object" && value != null && !Array.isArray(value)
806
+ ? value
807
+ : undefined;
808
+ }
809
+ function asNonEmptyString(value) {
810
+ return typeof value === "string" && value.trim() ? value : undefined;
811
+ }
812
+ function asStringArray(value) {
813
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string" && entry.trim())
814
+ ? value
815
+ : undefined;
816
+ }
817
+ function truncateSuggestionText(value, maxLength = 500) {
818
+ return value.length > maxLength
819
+ ? `${value.slice(0, maxLength)}...`
820
+ : value;
821
+ }
822
+ function parseJsonObjectString(value) {
823
+ if (!value.trim().startsWith("{")) {
824
+ return undefined;
825
+ }
826
+ try {
827
+ const parsed = JSON.parse(value);
828
+ return asObjectRecord(parsed);
829
+ }
830
+ catch {
831
+ return undefined;
832
+ }
833
+ }
834
+ function inferTargetKindFromString(value) {
835
+ if (/[\\/]/.test(value) || /\.jar$/i.test(value)) {
836
+ return "jar";
837
+ }
838
+ if (value.split(":").length >= 3) {
839
+ return "coordinate";
840
+ }
841
+ return "version";
842
+ }
843
+ function copySourceLookupSuggestionFields(tool, source) {
844
+ const result = {};
845
+ const stringFields = tool === "get-class-source"
846
+ ? ["className", "mode", "mapping", "sourcePriority", "projectPath", "scope", "outputFile"]
847
+ : ["className", "mapping", "sourcePriority", "projectPath", "scope", "access", "memberPattern"];
848
+ for (const field of stringFields) {
849
+ const value = source[field];
850
+ if (typeof value === "string" && value.trim()) {
851
+ result[field] = value;
852
+ }
853
+ }
854
+ const numericFields = tool === "get-class-source"
855
+ ? ["startLine", "endLine", "maxLines", "maxChars"]
856
+ : ["maxMembers"];
857
+ for (const field of numericFields) {
858
+ const value = source[field];
859
+ if (typeof value === "number" && Number.isFinite(value)) {
860
+ result[field] = value;
861
+ }
862
+ }
863
+ const booleanFields = tool === "get-class-source"
864
+ ? ["allowDecompile", "preferProjectVersion", "strictVersion"]
865
+ : ["allowDecompile", "preferProjectVersion", "strictVersion", "includeSynthetic", "includeInherited"];
866
+ for (const field of booleanFields) {
867
+ const value = source[field];
868
+ if (typeof value === "boolean") {
869
+ result[field] = value;
870
+ }
871
+ }
872
+ return result;
873
+ }
874
+ function copyValidateMixinSharedParams(source) {
875
+ const result = {};
876
+ const stringFields = [
877
+ "version",
878
+ "mapping",
879
+ "sourcePriority",
880
+ "scope",
881
+ "projectPath",
882
+ "minSeverity",
883
+ "warningMode",
884
+ "reportMode"
885
+ ];
886
+ for (const field of stringFields) {
887
+ const value = source[field];
888
+ if (typeof value === "string" && value.trim()) {
889
+ result[field] = value;
890
+ }
891
+ }
892
+ const booleanFields = [
893
+ "preferProjectVersion",
894
+ "hideUncertain",
895
+ "explain",
896
+ "preferProjectMapping",
897
+ "treatInfoAsWarning",
898
+ "includeIssues"
899
+ ];
900
+ for (const field of booleanFields) {
901
+ const value = source[field];
902
+ if (typeof value === "boolean") {
903
+ result[field] = value;
904
+ }
905
+ }
906
+ const sourceRoots = asStringArray(source.sourceRoots);
907
+ if (sourceRoots) {
908
+ result.sourceRoots = sourceRoots;
909
+ }
910
+ const warningCategoryFilter = asStringArray(source.warningCategoryFilter);
911
+ if (warningCategoryFilter) {
912
+ result.warningCategoryFilter = warningCategoryFilter;
913
+ }
914
+ return result;
915
+ }
916
+ function buildValidateMixinSuggestedParams(normalizedInput) {
917
+ const record = asObjectRecord(normalizedInput);
918
+ if (!record) {
919
+ return {
920
+ input: {
921
+ mode: "inline",
922
+ source: "<Mixin Java source>"
923
+ },
924
+ version: "<minecraft-version>"
925
+ };
926
+ }
927
+ const inputRecord = asObjectRecord(record.input);
928
+ const shared = copyValidateMixinSharedParams(record);
929
+ const version = asNonEmptyString(record.version) ?? "<minecraft-version>";
930
+ const inlineSource = asNonEmptyString(record.input) ??
931
+ asNonEmptyString(inputRecord?.source) ??
932
+ asNonEmptyString(record.source);
933
+ if (inlineSource) {
934
+ const parsedInlineObject = parseJsonObjectString(inlineSource);
935
+ if (parsedInlineObject && typeof parsedInlineObject.mode === "string") {
936
+ return {
937
+ ...shared,
938
+ input: parsedInlineObject,
939
+ version
940
+ };
941
+ }
942
+ return {
943
+ ...shared,
944
+ input: {
945
+ mode: "inline",
946
+ source: truncateSuggestionText(inlineSource)
947
+ },
948
+ version
949
+ };
950
+ }
951
+ const path = asNonEmptyString(inputRecord?.path) ??
952
+ asNonEmptyString(record.sourcePath);
953
+ if (path) {
954
+ return {
955
+ ...shared,
956
+ input: {
957
+ mode: "path",
958
+ path
959
+ },
960
+ version
961
+ };
962
+ }
963
+ const paths = asStringArray(inputRecord?.paths) ??
964
+ asStringArray(record.sourcePaths);
965
+ if (paths) {
966
+ return {
967
+ ...shared,
968
+ input: {
969
+ mode: "paths",
970
+ paths
971
+ },
972
+ version
973
+ };
974
+ }
975
+ const configPaths = asStringArray(inputRecord?.configPaths) ??
976
+ (asNonEmptyString(record.mixinConfigPath) ? [record.mixinConfigPath] : undefined);
977
+ if (configPaths) {
978
+ return {
979
+ ...shared,
980
+ input: {
981
+ mode: "config",
982
+ configPaths
983
+ },
984
+ version
985
+ };
986
+ }
987
+ const projectPath = asNonEmptyString(record.projectPath) ??
988
+ (inputRecord?.mode === "project" ? asNonEmptyString(inputRecord.path) : undefined);
989
+ if (projectPath) {
990
+ return {
991
+ ...shared,
992
+ input: {
993
+ mode: "project",
994
+ path: projectPath
995
+ },
996
+ version
997
+ };
998
+ }
999
+ return {
1000
+ ...shared,
1001
+ input: {
1002
+ mode: "inline",
1003
+ source: "<Mixin Java source>"
1004
+ },
1005
+ version
1006
+ };
1007
+ }
1008
+ function buildResolveArtifactSuggestedParams(normalizedInput) {
1009
+ const record = asObjectRecord(normalizedInput);
1010
+ if (!record) {
1011
+ return {
1012
+ target: {
1013
+ kind: "version",
1014
+ value: "<minecraft-version>"
1015
+ }
1016
+ };
1017
+ }
1018
+ const targetValue = asNonEmptyString(record.target);
1019
+ const result = {
1020
+ target: targetValue
1021
+ ? {
1022
+ kind: inferTargetKindFromString(targetValue),
1023
+ value: targetValue
1024
+ }
1025
+ : {
1026
+ kind: "version",
1027
+ value: "<minecraft-version>"
1028
+ }
1029
+ };
1030
+ const stringFields = ["mapping", "sourcePriority", "projectPath", "scope"];
1031
+ for (const field of stringFields) {
1032
+ const value = record[field];
1033
+ if (typeof value === "string" && value.trim()) {
1034
+ result[field] = value;
1035
+ }
1036
+ }
1037
+ const booleanFields = ["allowDecompile", "preferProjectVersion", "strictVersion"];
1038
+ for (const field of booleanFields) {
1039
+ const value = record[field];
1040
+ if (typeof value === "boolean") {
1041
+ result[field] = value;
1042
+ }
1043
+ }
1044
+ return result;
1045
+ }
1046
+ function buildSourceLookupSuggestedParams(tool, normalizedInput) {
1047
+ const record = asObjectRecord(normalizedInput);
1048
+ const result = record ? copySourceLookupSuggestionFields(tool, record) : {};
1049
+ const targetValue = asNonEmptyString(record?.target);
1050
+ result.target = targetValue
1051
+ ? {
1052
+ type: "resolve",
1053
+ kind: inferTargetKindFromString(targetValue),
1054
+ value: targetValue
1055
+ }
1056
+ : {
1057
+ type: "resolve",
1058
+ kind: "version",
1059
+ value: "<minecraft-version>"
1060
+ };
1061
+ if (!asNonEmptyString(result.className)) {
1062
+ result.className = "<fully-qualified-class-name>";
1063
+ }
1064
+ return result;
1065
+ }
1066
+ function buildInvalidInputGuidance(tool, normalizedInput) {
1067
+ if (tool === "validate-mixin") {
1068
+ const hints = [
1069
+ "validate-mixin.input must be an object with input.mode = \"inline\" | \"path\" | \"paths\" | \"config\" | \"project\".",
1070
+ "Whole-project example: {\"input\":{\"mode\":\"project\",\"path\":\"/workspace\"},\"version\":\"1.21.10\",\"preferProjectVersion\":true,\"preferProjectMapping\":true}.",
1071
+ "Legacy top-level source/sourcePath/sourcePaths/mixinConfigPath fields are no longer accepted; wrap them under input.mode instead."
1072
+ ];
1073
+ return {
1074
+ hints,
1075
+ suggestedCall: {
1076
+ tool,
1077
+ params: buildValidateMixinSuggestedParams(normalizedInput)
1078
+ }
1079
+ };
1080
+ }
1081
+ if (tool === "resolve-artifact") {
1082
+ return {
1083
+ hints: [
1084
+ "resolve-artifact.target must be an object: {\"kind\":\"version|jar|coordinate\",\"value\":\"...\"}.",
1085
+ "Bare string targets are not accepted; wrap the value under target.kind and target.value."
1086
+ ],
1087
+ suggestedCall: {
1088
+ tool,
1089
+ params: buildResolveArtifactSuggestedParams(normalizedInput)
1090
+ }
1091
+ };
1092
+ }
1093
+ if (tool === "get-class-source" || tool === "get-class-members") {
1094
+ return {
1095
+ hints: [
1096
+ `${tool}.target must be an object: {"type":"resolve","kind":"version|jar|coordinate","value":"..."} or {"type":"artifact","artifactId":"..."}.`,
1097
+ "Bare string targets are not accepted; wrap the value under target.type/target.kind/target.value."
1098
+ ],
1099
+ suggestedCall: {
1100
+ tool,
1101
+ params: buildSourceLookupSuggestedParams(tool, normalizedInput)
1102
+ }
1103
+ };
1104
+ }
1105
+ return undefined;
1106
+ }
1107
+ function mapErrorToProblem(caughtError, requestId, context) {
772
1108
  if (caughtError instanceof ZodError) {
1109
+ const guidance = context?.tool
1110
+ ? buildInvalidInputGuidance(context.tool, context.normalizedInput)
1111
+ : undefined;
773
1112
  return {
774
1113
  type: "https://minecraft-modding-mcp.dev/problems/invalid-input",
775
1114
  title: "Invalid input",
@@ -778,7 +1117,8 @@ function mapErrorToProblem(caughtError, requestId) {
778
1117
  code: ERROR_CODES.INVALID_INPUT,
779
1118
  instance: requestId,
780
1119
  fieldErrors: toFieldErrorsFromZod(caughtError),
781
- hints: ["Check fieldErrors and submit a valid tool argument payload."]
1120
+ hints: guidance?.hints ?? ["Check fieldErrors and submit a valid tool argument payload."],
1121
+ ...(guidance?.suggestedCall ? { suggestedCall: guidance.suggestedCall } : {})
782
1122
  };
783
1123
  }
784
1124
  if (isAppError(caughtError)) {
@@ -823,8 +1163,11 @@ function splitWarnings(data) {
823
1163
  async function runTool(tool, rawInput, schema, action) {
824
1164
  const requestId = buildRequestId();
825
1165
  const startedAt = Date.now();
1166
+ let normalizedInput = rawInput;
826
1167
  try {
827
- const { normalizedInput, removedOfficialPaths, suggestedReplacementInput } = prepareToolInput(rawInput);
1168
+ const preparedInput = prepareToolInput(rawInput);
1169
+ normalizedInput = preparedInput.normalizedInput;
1170
+ const { removedOfficialPaths, suggestedReplacementInput } = preparedInput;
828
1171
  if (removedOfficialPaths.length > 0) {
829
1172
  throw createError({
830
1173
  code: ERROR_CODES.INVALID_INPUT,
@@ -846,7 +1189,9 @@ async function runTool(tool, rawInput, schema, action) {
846
1189
  });
847
1190
  }
848
1191
  const parsedInput = schema.parse(normalizedInput);
849
- const payload = await action(parsedInput);
1192
+ const payload = await (HEAVY_TOOL_NAMES.has(tool)
1193
+ ? heavyToolExecutionGate.run(tool, () => action(parsedInput))
1194
+ : action(parsedInput));
850
1195
  const { result, warnings } = splitWarnings(payload);
851
1196
  return objectResult({
852
1197
  result,
@@ -859,7 +1204,10 @@ async function runTool(tool, rawInput, schema, action) {
859
1204
  });
860
1205
  }
861
1206
  catch (caughtError) {
862
- const problem = mapErrorToProblem(caughtError, requestId);
1207
+ const problem = mapErrorToProblem(caughtError, requestId, {
1208
+ tool,
1209
+ normalizedInput
1210
+ });
863
1211
  if (isAppError(caughtError)) {
864
1212
  const isSevere = caughtError.code === ERROR_CODES.DB_FAILURE ||
865
1213
  caughtError.code === ERROR_CODES.REPO_FETCH_FAILED ||
@@ -1008,7 +1356,8 @@ server.tool("diff-class-signatures", "Compare one class signature between two Mi
1008
1356
  fromVersion: input.fromVersion,
1009
1357
  toVersion: input.toVersion,
1010
1358
  mapping: input.mapping,
1011
- sourcePriority: input.sourcePriority
1359
+ sourcePriority: input.sourcePriority,
1360
+ includeFullDiff: input.includeFullDiff
1012
1361
  })));
1013
1362
  server.tool("find-mapping", "Find symbol mapping candidates between namespaces using structured symbol inputs for a specific Minecraft version.", findMappingShape, { readOnlyHint: true }, async (args) => runTool("find-mapping", args, findMappingSchema, async (input) => sourceService.findMapping({
1014
1363
  version: input.version,
@@ -1019,7 +1368,8 @@ server.tool("find-mapping", "Find symbol mapping candidates between namespaces u
1019
1368
  sourceMapping: input.sourceMapping,
1020
1369
  targetMapping: input.targetMapping,
1021
1370
  sourcePriority: input.sourcePriority,
1022
- disambiguation: input.disambiguation
1371
+ disambiguation: input.disambiguation,
1372
+ maxCandidates: input.maxCandidates
1023
1373
  })));
1024
1374
  server.tool("resolve-method-mapping-exact", "Resolve one method mapping exactly by owner+name+descriptor between namespaces and report resolved/not_found/ambiguous.", resolveMethodMappingExactShape, { readOnlyHint: true }, async (args) => runTool("resolve-method-mapping-exact", args, resolveMethodMappingExactSchema, async (input) => sourceService.resolveMethodMappingExact({
1025
1375
  version: input.version,
@@ -1028,14 +1378,16 @@ server.tool("resolve-method-mapping-exact", "Resolve one method mapping exactly
1028
1378
  descriptor: input.descriptor,
1029
1379
  sourceMapping: input.sourceMapping,
1030
1380
  targetMapping: input.targetMapping,
1031
- sourcePriority: input.sourcePriority
1381
+ sourcePriority: input.sourcePriority,
1382
+ maxCandidates: input.maxCandidates
1032
1383
  })));
1033
1384
  server.tool("get-class-api-matrix", "List class/member API rows across obfuscated/mojang/intermediary/yarn mappings for one class and Minecraft version.", getClassApiMatrixShape, { readOnlyHint: true }, async (args) => runTool("get-class-api-matrix", args, getClassApiMatrixSchema, async (input) => sourceService.getClassApiMatrix({
1034
1385
  version: input.version,
1035
1386
  className: input.className,
1036
1387
  classNameMapping: input.classNameMapping,
1037
1388
  includeKinds: parseClassApiKinds(input.includeKinds),
1038
- sourcePriority: input.sourcePriority
1389
+ sourcePriority: input.sourcePriority,
1390
+ maxRows: input.maxRows
1039
1391
  })));
1040
1392
  server.tool("resolve-workspace-symbol", "Resolve class/field/method names as seen at compile time for a workspace by reading Gradle Loom mapping settings.", resolveWorkspaceSymbolShape, { readOnlyHint: true }, async (args) => runTool("resolve-workspace-symbol", args, resolveWorkspaceSymbolSchema, async (input) => sourceService.resolveWorkspaceSymbol({
1041
1393
  projectPath: input.projectPath,
@@ -1045,7 +1397,8 @@ server.tool("resolve-workspace-symbol", "Resolve class/field/method names as see
1045
1397
  owner: input.owner,
1046
1398
  descriptor: input.descriptor,
1047
1399
  sourceMapping: input.sourceMapping,
1048
- sourcePriority: input.sourcePriority
1400
+ sourcePriority: input.sourcePriority,
1401
+ maxCandidates: input.maxCandidates
1049
1402
  })));
1050
1403
  server.tool("check-symbol-exists", "Check whether a class/field/method symbol exists in a specific mapping namespace for one Minecraft version.", checkSymbolExistsShape, { readOnlyHint: true }, async (args) => runTool("check-symbol-exists", args, checkSymbolExistsSchema, async (input) => sourceService.checkSymbolExists({
1051
1404
  version: input.version,
@@ -1056,7 +1409,8 @@ server.tool("check-symbol-exists", "Check whether a class/field/method symbol ex
1056
1409
  sourceMapping: input.sourceMapping,
1057
1410
  sourcePriority: input.sourcePriority,
1058
1411
  nameMode: input.nameMode,
1059
- signatureMode: input.signatureMode
1412
+ signatureMode: input.signatureMode,
1413
+ maxCandidates: input.maxCandidates
1060
1414
  })));
1061
1415
  server.tool("nbt-to-json", "Decode Java Edition NBT binary payload (base64) into typed JSON.", nbtToJsonShape, { readOnlyHint: true }, async (args) => runTool("nbt-to-json", args, nbtToJsonSchema, async (input) => Promise.resolve(nbtBase64ToTypedJson({
1062
1416
  nbtBase64: input.nbtBase64,
@@ -1091,7 +1445,8 @@ server.tool("validate-mixin", "Validate Mixin source against Minecraft bytecode
1091
1445
  preferProjectMapping: input.preferProjectMapping,
1092
1446
  reportMode: input.reportMode,
1093
1447
  warningCategoryFilter: input.warningCategoryFilter,
1094
- treatInfoAsWarning: input.treatInfoAsWarning
1448
+ treatInfoAsWarning: input.treatInfoAsWarning,
1449
+ includeIssues: input.includeIssues
1095
1450
  })));
1096
1451
  server.tool("validate-access-widener", "Validate Access Widener file entries against Minecraft bytecode signatures for a given version.", validateAccessWidenerShape, { readOnlyHint: true }, async (args) => runTool("validate-access-widener", args, validateAccessWidenerSchema, async (input) => sourceService.validateAccessWidener({
1097
1452
  content: input.content,
@@ -1107,7 +1462,9 @@ server.tool("analyze-mod-jar", "Analyze a Minecraft mod JAR to extract loader ty
1107
1462
  }));
1108
1463
  server.tool("get-registry-data", "Get Minecraft registry data (blocks, items, biomes, etc.) for a specific version by running the server data generator.", getRegistryDataShape, { readOnlyHint: true }, async (args) => runTool("get-registry-data", args, getRegistryDataSchema, async (input) => sourceService.getRegistryData({
1109
1464
  version: input.version,
1110
- registry: input.registry
1465
+ registry: input.registry,
1466
+ includeData: input.includeData,
1467
+ maxEntriesPerRegistry: input.maxEntriesPerRegistry
1111
1468
  })));
1112
1469
  server.tool("compare-versions", "Compare two Minecraft versions to find added/removed classes and registry entry changes. Useful for understanding what changed between versions during mod migration.", compareVersionsShape, { readOnlyHint: true }, async (args) => runTool("compare-versions", args, compareVersionsSchema, async (input) => sourceService.compareVersions({
1113
1470
  fromVersion: input.fromVersion,
@@ -1118,7 +1475,9 @@ server.tool("compare-versions", "Compare two Minecraft versions to find added/re
1118
1475
  })));
1119
1476
  server.tool("decompile-mod-jar", "Decompile a Minecraft mod JAR using Vineflower and list available classes, or view a specific class source. Builds on analyze-mod-jar by exposing the actual source code.", decompileModJarShape, { readOnlyHint: true }, async (args) => runTool("decompile-mod-jar", args, decompileModJarSchema, async (input) => sourceService.decompileModJar({
1120
1477
  jarPath: input.jarPath,
1121
- className: input.className
1478
+ className: input.className,
1479
+ includeFiles: input.includeFiles,
1480
+ maxFiles: input.maxFiles
1122
1481
  })));
1123
1482
  server.tool("get-mod-class-source", "Get decompiled source code for a specific class in a mod JAR. The mod JAR will be decompiled if not already cached.", getModClassSourceShape, { readOnlyHint: true }, async (args) => runTool("get-mod-class-source", args, getModClassSourceSchema, async (input) => sourceService.getModClassSource({
1124
1483
  jarPath: input.jarPath,