@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
@@ -22,6 +22,12 @@ const SUPPORTED_MAPPINGS = new Set([
22
22
  "intermediary",
23
23
  "yarn"
24
24
  ]);
25
+ /**
26
+ * Cap on the declaring classes named in the owner-strict not_found warning. A real
27
+ * mapping can hold dozens of same-signature methods, and the warning is guidance, not
28
+ * an inventory — find-mapping is the tool that returns the full list.
29
+ */
30
+ const MAX_REPORTED_DECLARING_OWNERS = 5;
25
31
  /* parsers extracted to src/mapping/parsers/{symbol-records,normalize,proguard,tiny}.ts */
26
32
  export class MappingService {
27
33
  config;
@@ -388,39 +394,89 @@ export class MappingService {
388
394
  const obfuscatedProjection = this.projectMethodDescriptorToTarget(graph, toObfuscatedPath, descriptor);
389
395
  acceptedDescriptors.add(obfuscatedProjection.descriptor);
390
396
  }
391
- const strictCandidates = rawCandidates.filter((candidate) => candidate.descriptor !== undefined && acceptedDescriptors.has(candidate.descriptor));
397
+ // "Strict" is the query's full advertised triple: owner + name + descriptor. The
398
+ // descriptor alone is not enough, because `lookupCandidates` also indexes methods
399
+ // under the OWNERLESS `<name><descriptor>` simple-name key — so a same-signature
400
+ // method on an unrelated class used to survive the filter and force an `ambiguous`
401
+ // verdict next to the one exact, confidence-1 candidate that answered the question.
402
+ const descriptorMatched = rawCandidates.filter((candidate) => candidate.descriptor !== undefined && acceptedDescriptors.has(candidate.descriptor));
403
+ const acceptedOwners = this.acceptedOwnersAlongPath(graph, path, owner);
404
+ const strictCandidates = descriptorMatched.filter((candidate) => candidate.owner !== undefined && acceptedOwners.has(candidate.owner));
405
+ // A candidate is attributed to the FIRST gate it failed, so the two counts partition
406
+ // the rejected set and never double-count a candidate that fails both.
407
+ const rejectedByDescriptor = rawCandidates.length - descriptorMatched.length;
408
+ const rejectedByOwner = descriptorMatched.length - strictCandidates.length;
392
409
  if (strictCandidates.length === 1) {
393
410
  const resolved = toResolutionCandidate(strictCandidates[0]);
411
+ // The verdict is computed from `strictCandidates`, so that is the set reported —
412
+ // the same rule the ambiguous branch follows. Returning the raw name-matched list
413
+ // here would publish candidates the strict filter had already rejected alongside
414
+ // `status: "resolved"`, with nothing to tell them apart from the answer.
415
+ const limitedStrictCandidates = limitResolutionCandidates([resolved], input.maxCandidates);
394
416
  return {
395
417
  querySymbol,
396
418
  mappingContext,
397
419
  resolved: true,
398
420
  status: "resolved",
399
421
  resolvedSymbol: resolved,
400
- candidates: limitedCandidates.candidates,
401
- candidateCount: limitedCandidates.candidateCount,
402
- candidatesTruncated: limitedCandidates.candidatesTruncated,
422
+ candidates: limitedStrictCandidates.candidates,
423
+ candidateCount: limitedStrictCandidates.candidateCount,
424
+ candidatesTruncated: limitedStrictCandidates.candidatesTruncated,
403
425
  warnings,
404
426
  provenance: this.provenanceForPath(graph, path)
405
427
  };
406
428
  }
407
429
  if (strictCandidates.length > 1) {
430
+ // The verdict is computed from `strictCandidates`, so that is the set reported.
431
+ // Returning the raw name-matched list here put candidates the strict filter had
432
+ // already rejected — including full-confidence exact matches on other owners —
433
+ // next to `status: "ambiguous"`, with nothing to tell them apart from the two the
434
+ // tool actually could not choose between.
435
+ const strictResolutionCandidates = strictCandidates.map(toResolutionCandidate);
436
+ const limitedStrictCandidates = limitResolutionCandidates(strictResolutionCandidates, input.maxCandidates);
408
437
  warnings.push("Exact method mapping is ambiguous for owner+method+descriptor.");
409
- if (limitedCandidates.candidatesTruncated) {
438
+ if (limitedStrictCandidates.candidatesTruncated) {
410
439
  warnings.push("Raise maxCandidates up to 200 to inspect the full candidate list, or narrow the lookup via find-mapping disambiguation hints.");
411
440
  }
441
+ // Report the counts BY REASON. Most rejections here are by owner, not by
442
+ // descriptor, so a warning that names descriptor as the cause would be wrong.
443
+ const rejectedTotal = rejectedByOwner + rejectedByDescriptor;
444
+ if (rejectedTotal > 0) {
445
+ warnings.push(`${rejectedTotal} further name-matched candidate(s) were rejected before the verdict ` +
446
+ `(${rejectedByOwner} by owner, ${rejectedByDescriptor} by descriptor) and are not reported; ` +
447
+ `use find-mapping for the unfiltered list.`);
448
+ }
412
449
  return {
413
450
  querySymbol,
414
451
  mappingContext,
415
452
  resolved: false,
416
453
  status: "ambiguous",
417
- candidates: limitedCandidates.candidates,
418
- candidateCount: limitedCandidates.candidateCount,
419
- candidatesTruncated: limitedCandidates.candidatesTruncated,
454
+ candidates: limitedStrictCandidates.candidates,
455
+ candidateCount: limitedStrictCandidates.candidateCount,
456
+ candidatesTruncated: limitedStrictCandidates.candidatesTruncated,
420
457
  warnings,
421
- provenance: this.provenanceForPath(graph, path)
458
+ provenance: this.provenanceForPath(graph, path),
459
+ ambiguityReasons: inferAmbiguityReasons(strictResolutionCandidates, pathUsesSource(graph.pairs, path, "mojang-client-mappings"))
422
460
  };
423
461
  }
462
+ // Owner-strictness has an accepted cost: a method the owner INHERITS rather than
463
+ // declares no longer resolves, because the mapping formats record declarations and
464
+ // carry no class hierarchy. Make that legible instead of returning a bare not_found.
465
+ if (rejectedByOwner > 0 && strictCandidates.length === 0) {
466
+ const declaringOwners = [
467
+ ...new Set(descriptorMatched.map((candidate) => candidate.owner).filter(Boolean))
468
+ ];
469
+ const shownOwners = declaringOwners.slice(0, MAX_REPORTED_DECLARING_OWNERS);
470
+ const ownerList = shownOwners.join(", ") +
471
+ (declaringOwners.length > shownOwners.length
472
+ ? `, and ${declaringOwners.length - shownOwners.length} more`
473
+ : "");
474
+ warnings.push(`Exact method resolution is owner-strict: "${owner}" does not itself declare ` +
475
+ `${method}${descriptor}. ${rejectedByOwner} candidate(s) with that name and descriptor ` +
476
+ `are declared by other classes (${targetMapping}: ${ownerList}). An inherited or relocated ` +
477
+ `member cannot resolve here — use find-mapping for an owner-agnostic lookup, or query the ` +
478
+ `declaring class directly.`);
479
+ }
424
480
  if (descriptorProjection.hadClassReferences && !descriptorProjection.complete) {
425
481
  warnings.push(pathUsesSource(graph.pairs, path, "mojang-client-mappings")
426
482
  ? "Method descriptor could not be preserved through mojang-client-mappings and exact resolution is unavailable."
@@ -964,6 +1020,34 @@ export class MappingService {
964
1020
  descriptor: item.record.descriptor
965
1021
  }));
966
1022
  }
1023
+ /**
1024
+ * The owner names that a query owner is allowed to appear under once candidates have
1025
+ * been projected along `path`.
1026
+ *
1027
+ * The query owner arrives in the SOURCE namespace while `mapCandidatesAlongPath`
1028
+ * hands back records whose `owner` is the declaring class in the TARGET namespace
1029
+ * (both parsers build member records with `createMethodSymbolRecord(<owner in that
1030
+ * namespace>, ...)`), so the two are only comparable after the owner itself has been
1031
+ * carried along the same path. Projecting the owner as a class record through
1032
+ * `mapCandidatesAlongPath` is that carriage, and it is the same mechanism
1033
+ * `projectMethodDescriptorToTarget` already uses for class references inside a
1034
+ * descriptor.
1035
+ *
1036
+ * `simple-name` class matches are excluded on purpose: they mean "some other class
1037
+ * that happens to share this simple name", which is exactly the foreign-owner noise
1038
+ * the strict filter exists to remove. The source-namespace name is kept as an
1039
+ * accepted alias so an owner that the mapping does not rename still matches itself.
1040
+ */
1041
+ acceptedOwnersAlongPath(graph, path, owner) {
1042
+ const accepted = new Set([normalizeMappedSymbolOutput(owner)]);
1043
+ for (const candidate of this.mapCandidatesAlongPath(graph, path, createClassSymbolRecord(owner))) {
1044
+ if (candidate.kind !== "class" || candidate.matchKind === "simple-name") {
1045
+ continue;
1046
+ }
1047
+ accepted.add(normalizeMappedSymbolOutput(candidate.symbol));
1048
+ }
1049
+ return accepted;
1050
+ }
967
1051
  projectMethodDescriptorToTarget(graph, path, descriptor) {
968
1052
  let hadClassReferences = false;
969
1053
  let complete = true;
@@ -1,4 +1,4 @@
1
- import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
1
+ import type { CallToolResult, ReadResourceResult, ServerContext } from "@modelcontextprotocol/server";
2
2
  import { type ErrorCode } from "./errors.js";
3
3
  type ObjectResultOptions = {
4
4
  isError?: boolean;
@@ -6,9 +6,26 @@ type ObjectResultOptions = {
6
6
  export declare function objectResult<T extends Record<string, unknown>>(data: T, options?: ObjectResultOptions): CallToolResult;
7
7
  export declare function textResource(uri: string, value: string): ReadResourceResult;
8
8
  export declare function objectResource(uri: string, data: Record<string, unknown>): ReadResourceResult;
9
+ /**
10
+ * Whether the request being handled carries the FULL shallow modern
11
+ * (2026-07-28) era signal in its lifted `_meta` envelope: a string
12
+ * protocol-version AND a plain-object clientCapabilities — the same shallow
13
+ * modern-signal check era-classifier's `classifyEraSignal` applies at
14
+ * supervisor admission (kept in lockstep; that module is the canonical
15
+ * definition). The version key ALONE is not enough: a claim-shaped-INVALID
16
+ * request (version present, capabilities missing/invalid) is
17
+ * legacy-permissively forwarded on a legacy-locked connection, the SDK's
18
+ * era-blind lift still surfaces the key at `ctx.mcpReq.envelope`, and the
19
+ * reply is encoded by the LEGACY codec — which serializes handler-returned
20
+ * fields verbatim. Shallow-VALID envelopes, by contrast, are answered by the
21
+ * 2026 codec (the SDK rejects envelope-less modern requests -32602
22
+ * pre-handler, and era-conflict admission keeps modern claims off legacy
23
+ * connections). Reads only public context surface (`ctx.mcpReq.envelope`).
24
+ */
25
+ export declare function isModernEraRequest(ctx: Pick<ServerContext, "mcpReq"> | undefined): boolean;
9
26
  export declare function errorResource(uri: string, error: string | {
10
27
  message: string;
11
28
  code?: ErrorCode;
12
29
  details?: unknown;
13
- }): ReadResourceResult;
30
+ }, ctx?: Pick<ServerContext, "mcpReq">): ReadResourceResult;
14
31
  export {};
@@ -1,3 +1,6 @@
1
+ import { ResourceNotFoundError } from "@modelcontextprotocol/server";
2
+ import { PROBLEM_DETAILS_READ_CACHE_FIELDS } from "./cache-policy.js";
3
+ import { CLIENT_CAPABILITIES_META_KEY, PROTOCOL_VERSION_META_KEY } from "./era-classifier.js";
1
4
  import { ERROR_CODES } from "./errors.js";
2
5
  import { retryClassForErrorCode, issueOriginForErrorCode, extractAllowlistedContext } from "./error-mapping.js";
3
6
  import { toHints, extractValidatedSuggestionAndExamples, extractFieldErrorsFromDetails } from "./tool-guidance.js";
@@ -25,15 +28,18 @@ export function objectResource(uri, data) {
25
28
  ]
26
29
  };
27
30
  }
31
+ function isResourceNotFoundErrorCode(code) {
32
+ return (code === ERROR_CODES.FILE_NOT_FOUND ||
33
+ code === ERROR_CODES.SOURCE_NOT_FOUND ||
34
+ code === ERROR_CODES.CLASS_NOT_FOUND ||
35
+ code === ERROR_CODES.VERSION_NOT_FOUND ||
36
+ code === ERROR_CODES.JAR_NOT_FOUND);
37
+ }
28
38
  function statusForResourceErrorCode(code) {
29
39
  if (code === ERROR_CODES.INVALID_INPUT) {
30
40
  return 400;
31
41
  }
32
- if (code === ERROR_CODES.FILE_NOT_FOUND ||
33
- code === ERROR_CODES.SOURCE_NOT_FOUND ||
34
- code === ERROR_CODES.CLASS_NOT_FOUND ||
35
- code === ERROR_CODES.VERSION_NOT_FOUND ||
36
- code === ERROR_CODES.JAR_NOT_FOUND) {
42
+ if (isResourceNotFoundErrorCode(code)) {
37
43
  return 404;
38
44
  }
39
45
  if (code === ERROR_CODES.MAPPING_UNAVAILABLE ||
@@ -43,10 +49,38 @@ function statusForResourceErrorCode(code) {
43
49
  }
44
50
  return 500;
45
51
  }
46
- export function errorResource(uri, error) {
52
+ /** Shallow plain-object check, mirroring era-classifier's canonical helper. */
53
+ function isPlainObject(value) {
54
+ return typeof value === "object" && value !== null && !Array.isArray(value);
55
+ }
56
+ /**
57
+ * Whether the request being handled carries the FULL shallow modern
58
+ * (2026-07-28) era signal in its lifted `_meta` envelope: a string
59
+ * protocol-version AND a plain-object clientCapabilities — the same shallow
60
+ * modern-signal check era-classifier's `classifyEraSignal` applies at
61
+ * supervisor admission (kept in lockstep; that module is the canonical
62
+ * definition). The version key ALONE is not enough: a claim-shaped-INVALID
63
+ * request (version present, capabilities missing/invalid) is
64
+ * legacy-permissively forwarded on a legacy-locked connection, the SDK's
65
+ * era-blind lift still surfaces the key at `ctx.mcpReq.envelope`, and the
66
+ * reply is encoded by the LEGACY codec — which serializes handler-returned
67
+ * fields verbatim. Shallow-VALID envelopes, by contrast, are answered by the
68
+ * 2026 codec (the SDK rejects envelope-less modern requests -32602
69
+ * pre-handler, and era-conflict admission keeps modern claims off legacy
70
+ * connections). Reads only public context surface (`ctx.mcpReq.envelope`).
71
+ */
72
+ export function isModernEraRequest(ctx) {
73
+ const envelope = ctx?.mcpReq?.envelope;
74
+ return (typeof envelope?.[PROTOCOL_VERSION_META_KEY] === "string" &&
75
+ isPlainObject(envelope[CLIENT_CAPABILITIES_META_KEY]));
76
+ }
77
+ export function errorResource(uri, error, ctx) {
47
78
  const isStr = typeof error === "string";
48
79
  const detail = isStr ? error : error.message;
49
80
  const code = isStr ? ERROR_CODES.INVALID_INPUT : error.code ?? ERROR_CODES.INTERNAL;
81
+ if (!isStr && isModernEraRequest(ctx) && isResourceNotFoundErrorCode(code)) {
82
+ throw new ResourceNotFoundError(uri, detail);
83
+ }
50
84
  // Resource reads carry the same AppError as the equivalent tool call, so they
51
85
  // get the same recovery metadata. Classifiers are always present; the rest is
52
86
  // extracted from the AppError details (reusing the tool-error helpers as-is so
@@ -57,6 +91,14 @@ export function errorResource(uri, error) {
57
91
  const fieldErrors = extractFieldErrorsFromDetails(details);
58
92
  const context = extractAllowlistedContext(details);
59
93
  return {
94
+ // ProblemDetails-read cache override (adopted policy): STRUCTURAL
95
+ // identification — this constructor IS the error-resource path, so the
96
+ // override never re-infers from serialized text. Emitted as
97
+ // handler-returned result fields (SDK precedence rank 1, beating the
98
+ // resource's configured class-row hint) and gated to modern-era requests
99
+ // because the 2025 codec serializes handler-returned fields verbatim —
100
+ // legacy replies must stay byte-identical to the frozen golden surfaces.
101
+ ...(isModernEraRequest(ctx) ? PROBLEM_DETAILS_READ_CACHE_FIELDS : {}),
60
102
  contents: [
61
103
  {
62
104
  uri,
@@ -1,5 +1,5 @@
1
1
  import type { Config } from "./types.js";
2
- export type MappingNamespace = "obfuscated" | "mojang" | "yarn";
2
+ export type MappingNamespace = "obfuscated" | "mojang" | "intermediary" | "yarn";
3
3
  type SignatureAccess = "public" | "all";
4
4
  export interface ResponseContext {
5
5
  minecraftVersion: string;
@@ -185,6 +185,10 @@ export type AccessWidenerValidationResult = {
185
185
  invalid: number;
186
186
  };
187
187
  provenance?: RuntimeValidationProvenance<SourceMapping>;
188
+ /** True when the runtime jar used for evidence was not an exact match for the request. */
189
+ approximate?: boolean;
190
+ /** Why the verdict is approximate; present only alongside `approximate`. */
191
+ approximationReasons?: string[];
188
192
  warnings: string[];
189
193
  };
190
194
  export type AccessTransformerValidationResult = {
@@ -204,5 +208,9 @@ export type AccessTransformerValidationResult = {
204
208
  invalid: number;
205
209
  };
206
210
  provenance?: RuntimeValidationProvenance<AccessTransformerNamespace>;
211
+ /** True when the runtime jar used for evidence was not an exact match for the request. */
212
+ approximate?: boolean;
213
+ /** Why the verdict is approximate; present only alongside `approximate`. */
214
+ approximationReasons?: string[];
207
215
  warnings: string[];
208
216
  };
@@ -26,13 +26,13 @@ const fabricModJsonSchema = z
26
26
  name: z.string().optional(),
27
27
  version: z.string().optional(),
28
28
  description: z.string().optional(),
29
- entrypoints: z.record(z.array(stringOrEntrypoint)).optional(),
29
+ entrypoints: z.record(z.string(), z.array(stringOrEntrypoint)).optional(),
30
30
  mixins: z.array(stringOrMixinRef).optional(),
31
31
  accessWidener: z.string().optional(),
32
- depends: z.record(z.union([z.string(), z.array(z.string())])).optional(),
33
- recommends: z.record(z.union([z.string(), z.array(z.string())])).optional(),
34
- conflicts: z.record(z.union([z.string(), z.array(z.string())])).optional(),
35
- suggests: z.record(z.union([z.string(), z.array(z.string())])).optional(),
32
+ depends: z.record(z.string(), z.union([z.string(), z.array(z.string())])).optional(),
33
+ recommends: z.record(z.string(), z.union([z.string(), z.array(z.string())])).optional(),
34
+ conflicts: z.record(z.string(), z.union([z.string(), z.array(z.string())])).optional(),
35
+ suggests: z.record(z.string(), z.union([z.string(), z.array(z.string())])).optional(),
36
36
  jars: z.array(z.object({ file: z.string() }).passthrough()).optional()
37
37
  })
38
38
  .passthrough();
@@ -50,7 +50,7 @@ const quiltModJsonSchema = z
50
50
  })
51
51
  .passthrough()
52
52
  .optional(),
53
- entrypoints: z.record(z.array(stringOrEntrypoint)).optional(),
53
+ entrypoints: z.record(z.string(), z.array(stringOrEntrypoint)).optional(),
54
54
  depends: z.array(z.unknown()).optional()
55
55
  })
56
56
  .passthrough()
@@ -72,7 +72,7 @@ const forgeModsTomlSchema = z
72
72
  })
73
73
  .passthrough())
74
74
  .optional(),
75
- dependencies: z.record(z.array(z.unknown())).optional(),
75
+ dependencies: z.record(z.string(), z.array(z.unknown())).optional(),
76
76
  mixins: z.array(z.object({ config: z.string() }).passthrough()).optional(),
77
77
  accessTransformers: z.array(z.object({ file: z.string().optional() }).passthrough()).optional()
78
78
  })
@@ -193,6 +193,7 @@ export class ModDecompileService {
193
193
  const decompileResult = await decompileBinaryJar(jarPath, this.config.cacheDir, {
194
194
  vineflowerJarPath: vineflowerPath,
195
195
  timeoutMs: DECOMPILE_TIMEOUT_MS,
196
+ maxMemoryMb: this.config.decompileMaxMemoryMb,
196
197
  signature: cacheKey
197
198
  });
198
199
  const files = decompileResult.javaFiles.map((entry) => entry.filePath);
@@ -16,18 +16,28 @@ const TAG_ID_BY_NAME = {
16
16
  longArray: 12
17
17
  };
18
18
  const TAG_NAME_BY_ID = Object.fromEntries(Object.entries(TAG_ID_BY_NAME).map(([name, id]) => [id, name]));
19
+ // Default repair guidance, mirroring the per-stage `nextAction` the size-limit path
20
+ // already sets. `toHints()` reads only `details.nextAction`, so without one these
21
+ // rejections reach the client with nothing actionable at all. A caller-supplied
22
+ // `nextAction` still wins: the spread comes last.
23
+ const PARSE_NEXT_ACTION = "The payload is not a well-formed Java NBT stream at the reported offset. Check that " +
24
+ "nbtBase64 holds the whole file and that compression matches how it was written " +
25
+ '(use compression "auto" to detect gzip).';
26
+ const ENCODE_NEXT_ACTION = "The typed document could not be written as Java NBT. Correct the node named by " +
27
+ "jsonPointer, then retry; round-tripping a real payload through nbt-to-json yields a " +
28
+ "document this encoder accepts.";
19
29
  function parseError(message, details) {
20
30
  return createError({
21
31
  code: ERROR_CODES.NBT_PARSE_FAILED,
22
32
  message,
23
- details
33
+ details: { nextAction: PARSE_NEXT_ACTION, ...details }
24
34
  });
25
35
  }
26
36
  function encodeError(message, details) {
27
37
  return createError({
28
38
  code: ERROR_CODES.NBT_ENCODE_FAILED,
29
39
  message,
30
- details
40
+ details: { nextAction: ENCODE_NEXT_ACTION, ...details }
31
41
  });
32
42
  }
33
43
  function resolveTagName(tagId, pointer) {
@@ -7,25 +7,36 @@ function isRecord(value) {
7
7
  !Array.isArray(value) &&
8
8
  !ArrayBuffer.isView(value));
9
9
  }
10
+ // Default repair guidance, mirroring the per-stage `nextAction` the NBT size-limit path
11
+ // already sets. `toHints()` reads only `details.nextAction`, so without one these
12
+ // rejections reach the client with nothing actionable. A caller-supplied `nextAction`
13
+ // still wins: the spread comes last.
14
+ const INVALID_PATCH_NEXT_ACTION = 'The patch must be an RFC6902 array of {"op": "add"|"remove"|"replace"|"test", "path": ' +
15
+ '"/json/pointer", "value": <typed-nbt-node>} objects. Paths address the typed document ' +
16
+ '(for example "/root/value/<key>"), and an added or replaced value is a typed node, not a bare scalar.';
17
+ const UNSUPPORTED_FEATURE_NEXT_ACTION = "This operation is not expressible in Java NBT. Rewrite it in terms the format supports " +
18
+ "(homogeneous lists, typed compound entries, decimal-string longs) and retry.";
19
+ const PATCH_CONFLICT_NEXT_ACTION = "The document did not hold what the patch expected at that path. Re-read the current " +
20
+ "document with nbt-to-json and rebuild the patch against it.";
10
21
  function invalidPatch(message, details) {
11
22
  throw createError({
12
23
  code: ERROR_CODES.JSON_PATCH_INVALID,
13
24
  message,
14
- details
25
+ details: { nextAction: INVALID_PATCH_NEXT_ACTION, ...details }
15
26
  });
16
27
  }
17
28
  function unsupportedFeature(message, details) {
18
29
  throw createError({
19
30
  code: ERROR_CODES.NBT_UNSUPPORTED_FEATURE,
20
31
  message,
21
- details
32
+ details: { nextAction: UNSUPPORTED_FEATURE_NEXT_ACTION, ...details }
22
33
  });
23
34
  }
24
35
  function patchConflict(message, details) {
25
36
  throw createError({
26
37
  code: ERROR_CODES.JSON_PATCH_CONFLICT,
27
38
  message,
28
- details
39
+ details: { nextAction: PATCH_CONFLICT_NEXT_ACTION, ...details }
29
40
  });
30
41
  }
31
42
  function parsePatchOperations(patch) {
@@ -1,5 +1,6 @@
1
1
  import { gunzipSync, gzipSync } from "node:zlib";
2
- import { createError, ERROR_CODES } from "../errors.js";
2
+ import { buildSuggestedCall } from "../build-suggested-call.js";
3
+ import { AppError, createError, ERROR_CODES } from "../errors.js";
3
4
  import { decodeJavaNbt, encodeJavaNbt } from "./java-nbt-codec.js";
4
5
  import { applyJsonPatch } from "./json-patch.js";
5
6
  import { assertValidTypedNbtDocument } from "./typed-json.js";
@@ -46,6 +47,42 @@ function limitExceeded(stage, field, actual, limit) {
46
47
  }
47
48
  });
48
49
  }
50
+ /**
51
+ * `assertValidTypedNbtDocument` has six call sites and knows nothing about tools, so the
52
+ * recovery suggestion is attached here — the only layer that knows which tool the caller
53
+ * invoked. The suggestion is a TEMPLATE (it needs the caller's own base64 payload), so it
54
+ * travels as `exampleCalls`; `params: undefined` means the primary-drop marker is never
55
+ * set and no "payload failed schema validation" hint is appended.
56
+ */
57
+ function assertValidTypedNbtDocumentForTool(value, tool) {
58
+ try {
59
+ assertValidTypedNbtDocument(value);
60
+ }
61
+ catch (error) {
62
+ if (!(error instanceof AppError) || error.code !== ERROR_CODES.NBT_INVALID_TYPED_JSON) {
63
+ throw error;
64
+ }
65
+ throw createError({
66
+ code: error.code,
67
+ // Frozen: an approved-deviation ProblemDetails golden asserts this text verbatim.
68
+ message: error.message,
69
+ details: {
70
+ ...error.details,
71
+ ...buildSuggestedCall({
72
+ tool: "nbt-to-json",
73
+ params: undefined,
74
+ examples: [
75
+ {
76
+ params: { nbtBase64: "<base64-encoded-nbt-payload>" },
77
+ reason: `Decode a real NBT payload with nbt-to-json to obtain a well-formed typedJson ` +
78
+ `document, edit that, then retry ${tool} with it.`
79
+ }
80
+ ]
81
+ })
82
+ }
83
+ });
84
+ }
85
+ }
49
86
  function assertByteLimit(stage, field, actual, limit) {
50
87
  if (actual > limit) {
51
88
  limitExceeded(stage, field, actual, limit);
@@ -149,7 +186,7 @@ export function nbtBase64ToTypedJson(input, limits = DEFAULT_NBT_LIMITS) {
149
186
  }
150
187
  export function typedJsonToNbtBase64(input, limits = DEFAULT_NBT_LIMITS) {
151
188
  const compression = parseEncodeCompression(input.compression);
152
- assertValidTypedNbtDocument(input.typedJson);
189
+ assertValidTypedNbtDocumentForTool(input.typedJson, "json-to-nbt");
153
190
  let output = encodeJavaNbt(input.typedJson);
154
191
  if (compression === "gzip") {
155
192
  output = gzipSync(output);
@@ -165,7 +202,7 @@ export function typedJsonToNbtBase64(input, limits = DEFAULT_NBT_LIMITS) {
165
202
  };
166
203
  }
167
204
  export function applyNbtJsonPatch(input, limits = DEFAULT_NBT_LIMITS) {
168
- assertValidTypedNbtDocument(input.typedJson);
205
+ assertValidTypedNbtDocumentForTool(input.typedJson, "nbt-apply-json-patch");
169
206
  const patched = applyJsonPatch(input.typedJson, input.patch);
170
207
  assertByteLimit("patch-output", "typedJson", jsonUtf8Bytes(patched.typedJson), limits.maxResponseBytes);
171
208
  return patched;
@@ -205,13 +205,38 @@ export function validateTypedNbtDocument(value) {
205
205
  }
206
206
  return validateNode(value.root, "/root");
207
207
  }
208
+ /**
209
+ * Prose form of the document contract. The `typedJson` argument is advertised on the
210
+ * wire as an empty JSON Schema (`{}`) and those bytes are frozen for legacy parity, so
211
+ * the rejection is the only place a caller can be told what the document must look
212
+ * like. Kept in sync with `docs/tool-reference.md` -> "Typed NBT document shape".
213
+ */
214
+ const TYPED_NBT_DOCUMENT_SHAPE = 'A typed NBT document is {"rootName": <string>, "root": <node>}, where every node is ' +
215
+ '{"type": "byte|short|int|long|float|double|byteArray|string|list|compound|intArray|longArray", "value": ...}; ' +
216
+ 'a "list" node also carries "elementType", and "long"/"longArray" values are decimal strings.';
208
217
  export function assertValidTypedNbtDocument(value) {
209
218
  const validation = validateTypedNbtDocument(value);
210
219
  if (!validation.ok) {
220
+ const { jsonPointer, expectedType, actualType } = validation.issue;
221
+ // A root-level failure has an empty JSON pointer, which is useless as a field path;
222
+ // fall back to the argument name so the caller has something to act on.
223
+ const fieldPath = jsonPointer || "typedJson";
211
224
  throw createError({
212
225
  code: ERROR_CODES.NBT_INVALID_TYPED_JSON,
213
226
  message: "Invalid typed NBT JSON document.",
214
- details: { ...validation.issue }
227
+ details: {
228
+ // The raw issue triple stays in place: existing readers index it directly.
229
+ ...validation.issue,
230
+ fieldErrors: [
231
+ {
232
+ path: fieldPath,
233
+ message: `expected ${expectedType}, received ${actualType}`,
234
+ code: "nbt_invalid_node"
235
+ }
236
+ ],
237
+ nextAction: `Correct ${fieldPath}: expected ${expectedType}, received ${actualType}. ` +
238
+ TYPED_NBT_DOCUMENT_SHAPE
239
+ }
215
240
  });
216
241
  }
217
242
  }
@@ -0,0 +1,32 @@
1
+ import { z } from "zod";
2
+ import type { CallToolResult, McpServer, ServerContext, StandardSchemaWithJSON, ToolAnnotations } from "@modelcontextprotocol/server";
3
+ /**
4
+ * Validation-bypassing Standard Schema adapter for MCP SDK v2 tool
5
+ * registration.
6
+ *
7
+ * The v2 SDK validates tool arguments through `schema["~standard"].validate`
8
+ * before invoking the handler and returns generic InvalidParams text on
9
+ * failure. This adapter's identity `validate` passes ALL object args through
10
+ * raw so runTool() remains the single source of truth for validation and
11
+ * error envelopes (matching pre-migration behavior, where the equivalent v1
12
+ * layer was bypassed).
13
+ *
14
+ * `jsonSchema` is a Converter object whose `input()` serves the pinned
15
+ * pre-migration wire schema for the tool (V1_PARITY_SCHEMAS), advertised
16
+ * VERBATIM in tools/list. The zod fallback only applies to tools without a
17
+ * frozen fixture (none today).
18
+ */
19
+ export declare function appValidated<S extends z.ZodObject>(name: string, schema: S): StandardSchemaWithJSON<unknown, z.output<S>>;
20
+ /**
21
+ * Tool handler shape used by all app registrations: raw (unvalidated) args
22
+ * plus the v2 ServerContext. Handlers delegate validation to runTool().
23
+ */
24
+ export type AppToolHandler = (args: unknown, ctx: ServerContext) => Promise<CallToolResult>;
25
+ /**
26
+ * Registers a tool through the v2 `registerTool` API with the
27
+ * validation-bypassing adapter, preserving the v1 registration surface
28
+ * (name, description, zod raw shape, annotations, handler).
29
+ */
30
+ export declare function registerAppTool(server: McpServer, name: string, description: string, shape: z.ZodRawShape, annotations: ToolAnnotations, handler: AppToolHandler): void;
31
+ export declare const EXPERT_TOOL_NOTE = " Expert tool: prefer the entry tools (inspect-minecraft, analyze-symbol, compare-minecraft, analyze-mod, validate-project) first.";
32
+ export declare function registerExpertTool(server: McpServer, name: string, description: string, shape: z.ZodRawShape, annotations: ToolAnnotations, handler: AppToolHandler): void;
@@ -0,0 +1,52 @@
1
+ import { z } from "zod";
2
+ import { V1_PARITY_SCHEMAS } from "./v1-parity-schemas.js";
3
+ /**
4
+ * Validation-bypassing Standard Schema adapter for MCP SDK v2 tool
5
+ * registration.
6
+ *
7
+ * The v2 SDK validates tool arguments through `schema["~standard"].validate`
8
+ * before invoking the handler and returns generic InvalidParams text on
9
+ * failure. This adapter's identity `validate` passes ALL object args through
10
+ * raw so runTool() remains the single source of truth for validation and
11
+ * error envelopes (matching pre-migration behavior, where the equivalent v1
12
+ * layer was bypassed).
13
+ *
14
+ * `jsonSchema` is a Converter object whose `input()` serves the pinned
15
+ * pre-migration wire schema for the tool (V1_PARITY_SCHEMAS), advertised
16
+ * VERBATIM in tools/list. The zod fallback only applies to tools without a
17
+ * frozen fixture (none today).
18
+ */
19
+ export function appValidated(name, schema) {
20
+ return {
21
+ "~standard": {
22
+ version: 1,
23
+ vendor: "app",
24
+ validate: (value) => ({ value: value }),
25
+ jsonSchema: {
26
+ input: () => V1_PARITY_SCHEMAS[name] ?? z.toJSONSchema(schema, { io: "input" })
27
+ }
28
+ }
29
+ };
30
+ }
31
+ /**
32
+ * Registers a tool through the v2 `registerTool` API with the
33
+ * validation-bypassing adapter, preserving the v1 registration surface
34
+ * (name, description, zod raw shape, annotations, handler).
35
+ */
36
+ export function registerAppTool(server, name, description, shape, annotations, handler) {
37
+ server.registerTool(name, {
38
+ description,
39
+ inputSchema: appValidated(name, z.object(shape)),
40
+ annotations
41
+ }, handler);
42
+ }
43
+ // Low-level/expert tools duplicate capability that the six entry tools expose
44
+ // in a single resolve+fetch call. registerExpertTool() registers them exactly
45
+ // like registerAppTool() but appends a note steering agents to the entry tools
46
+ // first. Entry tools, batch tools, and the NBT/runtime utilities (which have no
47
+ // entry equivalent) keep their plain descriptions via registerAppTool().
48
+ export const EXPERT_TOOL_NOTE = " Expert tool: prefer the entry tools (inspect-minecraft, analyze-symbol, compare-minecraft, analyze-mod, validate-project) first.";
49
+ export function registerExpertTool(server, name, description, shape, annotations, handler) {
50
+ registerAppTool(server, name, description + EXPERT_TOOL_NOTE, shape, annotations, handler);
51
+ }
52
+ //# sourceMappingURL=registration-adapter.js.map
@@ -0,0 +1,7 @@
1
+ export type RequestContext = {
2
+ requestId: string;
3
+ deadlineAt?: number;
4
+ signal?: AbortSignal;
5
+ };
6
+ export declare function runWithRequestContext<T>(context: RequestContext, fn: () => T): T;
7
+ export declare function getRequestContext(): RequestContext | undefined;
@@ -0,0 +1,9 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ const requestContextStorage = new AsyncLocalStorage();
3
+ export function runWithRequestContext(context, fn) {
4
+ return requestContextStorage.run(context, fn);
5
+ }
6
+ export function getRequestContext() {
7
+ return requestContextStorage.getStore();
8
+ }
9
+ //# sourceMappingURL=request-context.js.map
@@ -1,3 +1,3 @@
1
- import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1
+ import type { McpServer } from "@modelcontextprotocol/server";
2
2
  import type { SourceService } from "./source-service.js";
3
3
  export declare function registerResources(server: McpServer, sourceService: SourceService): void;