@adhisang/minecraft-modding-mcp 7.0.0-rc.0 → 7.0.0-rc.2

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 (58) hide show
  1. package/CHANGELOG.md +131 -49
  2. package/README.md +2 -0
  3. package/dist/cache-registry.d.ts +10 -0
  4. package/dist/cache-registry.js +44 -3
  5. package/dist/entry-tools/batch-class-members-service.js +8 -0
  6. package/dist/entry-tools/inspect-minecraft/handlers/class-members.js +21 -0
  7. package/dist/entry-tools/inspect-minecraft/internal.d.ts +4 -0
  8. package/dist/entry-tools/manage-cache-service.d.ts +2 -2
  9. package/dist/entry-tools/validate-project/cases/mixin.js +26 -6
  10. package/dist/entry-tools/validate-project/cases/project-summary.d.ts +7 -7
  11. package/dist/entry-tools/validate-project-service.d.ts +2 -2
  12. package/dist/entry-tools/verify-mixin-target-service.js +19 -1
  13. package/dist/error-mapping.d.ts +118 -0
  14. package/dist/error-mapping.js +177 -7
  15. package/dist/index.js +3 -1
  16. package/dist/json-rpc-framing.d.ts +53 -1
  17. package/dist/json-rpc-framing.js +190 -25
  18. package/dist/mapping/loaders/tiny-loom-selection.js +10 -3
  19. package/dist/mapping/parsers/tiny.js +4 -0
  20. package/dist/mapping-service.js +24 -3
  21. package/dist/maven-resolver.d.ts +61 -0
  22. package/dist/maven-resolver.js +95 -4
  23. package/dist/maven-token.d.ts +65 -0
  24. package/dist/maven-token.js +120 -0
  25. package/dist/mcp-helpers.js +14 -3
  26. package/dist/minecraft-explorer-service.d.ts +13 -1
  27. package/dist/minecraft-explorer-service.js +200 -7
  28. package/dist/repo-downloader.d.ts +260 -0
  29. package/dist/repo-downloader.js +1083 -14
  30. package/dist/source/artifact-resolver.d.ts +97 -3
  31. package/dist/source/artifact-resolver.js +228 -24
  32. package/dist/source/class-source.d.ts +15 -0
  33. package/dist/source/class-source.js +163 -18
  34. package/dist/source/indexer.js +14 -0
  35. package/dist/source/nested-jars.d.ts +16 -1
  36. package/dist/source/nested-jars.js +66 -3
  37. package/dist/source/workspace-target.js +39 -20
  38. package/dist/source-jar-reader.d.ts +30 -2
  39. package/dist/source-jar-reader.js +39 -8
  40. package/dist/source-resolver.d.ts +48 -0
  41. package/dist/source-resolver.js +735 -81
  42. package/dist/source-service.d.ts +10 -1
  43. package/dist/stdio-supervisor.d.ts +75 -1
  44. package/dist/stdio-supervisor.js +240 -14
  45. package/dist/storage/artifacts-repo.d.ts +2 -0
  46. package/dist/storage/artifacts-repo.js +22 -0
  47. package/dist/storage/db.js +32 -10
  48. package/dist/tool-guidance.js +17 -7
  49. package/dist/tool-schemas.d.ts +10 -2
  50. package/dist/tool-schemas.js +4 -0
  51. package/dist/v1-parity-schemas.js +16 -0
  52. package/dist/version-diff-service.js +25 -4
  53. package/dist/version-service.js +23 -8
  54. package/dist/warning-details.js +3 -1
  55. package/dist/workspace-mapping-service.d.ts +8 -1
  56. package/dist/workspace-mapping-service.js +28 -19
  57. package/docs/tool-reference.md +31 -7
  58. package/package.json +6 -4
@@ -26,9 +26,16 @@ const MAX_INDEX_ENTRY_BUDGET = 64_000_000;
26
26
  * overrides it outright.
27
27
  */
28
28
  export function resolveTinyIndexEntryBudget(value = process.env.MCP_LOOM_TINY_MAX_INDEX_ENTRIES, heapStats = v8.getHeapStatistics()) {
29
- const override = Number.parseInt(value ?? "", 10);
30
- if (Number.isFinite(override) && override > 0) {
31
- return override;
29
+ // Strict ASCII digits and safe integers only, mirroring `loadMaxFrameBytes` and
30
+ // `loadMaxDownloadBytes`. Number.parseInt stops at the first non-digit, so "1e9",
31
+ // "2_000_000", "10M" and "1.9" used to be accepted as 1, 2, 10 and 1 — a budget
32
+ // that truncates every real Loom load. No floor is applied: an explicit override
33
+ // is allowed to be small on purpose.
34
+ if (/^[0-9]+$/.test(value ?? "")) {
35
+ const override = Number(value);
36
+ if (Number.isSafeInteger(override) && override > 0) {
37
+ return override;
38
+ }
32
39
  }
33
40
  const free = Math.max(0, heapStats.heap_size_limit - heapStats.used_heap_size);
34
41
  const derived = Math.floor((free * HEAP_BUDGET_FRACTION) / HEAP_BYTES_PER_INDEX_ENTRY);
@@ -128,6 +128,10 @@ export function parseTinyMappingsInto(target, text, options) {
128
128
  for (const namespace of header.columns) {
129
129
  const value = columns[namespace.columnIndex]?.trim() ?? "";
130
130
  if (!value) {
131
+ // A class row that omits this namespace ends the previous class's name
132
+ // for it. Leaving the stale entry made the following field/method rows
133
+ // register under the PREVIOUS class as owner, at confidence 1.
134
+ currentClassNames.delete(namespace.mapping);
131
135
  continue;
132
136
  }
133
137
  currentClassNames.set(namespace.mapping, value);
@@ -145,6 +145,10 @@ export class MappingService {
145
145
  candidates: [],
146
146
  candidateCount: 0,
147
147
  warnings: [
148
+ // A truncated Loom index (or a source that yielded nothing) is often WHY the
149
+ // path is missing, so the loader's own warnings lead the no-path sentence
150
+ // instead of being replaced by it.
151
+ ...graph.warnings,
148
152
  `No mapping path is available for ${sourceMapping} -> ${targetMapping} on version "${version}".`
149
153
  ]
150
154
  };
@@ -164,7 +168,10 @@ export class MappingService {
164
168
  .map((candidate) => queryRecord.kind === "method" && queryRecord.descriptor
165
169
  ? projectLookupCandidateDescriptor(candidate, queryRecord.descriptor, projectedDescriptor)
166
170
  : candidate);
167
- const warnings = [];
171
+ // Seeded from the graph so loader-level warnings (a truncated Loom index, a tiny
172
+ // source that yielded nothing) reach the caller. ensureMappingAvailable,
173
+ // getClassApiMatrix and checkSymbolExists already do this.
174
+ const warnings = [...graph.warnings];
168
175
  // signatureMode="exact" on kind=method must not return descriptorless fallback candidates
169
176
  // (lookupCandidates adds owner+name fallbacks by design for the loose path). Without this
170
177
  // filter a caller who supplied `foo(I)V` could be told `foo(Z)V` is the exact mapping,
@@ -374,11 +381,18 @@ export class MappingService {
374
381
  candidates: [],
375
382
  candidateCount: 0,
376
383
  warnings: [
384
+ // A truncated Loom index (or a source that yielded nothing) is often WHY the
385
+ // path is missing, so the loader's own warnings lead the no-path sentence
386
+ // instead of being replaced by it.
387
+ ...graph.warnings,
377
388
  `No mapping path is available for ${sourceMapping} -> ${targetMapping} on version "${version}".`
378
389
  ]
379
390
  };
380
391
  }
381
- const warnings = [];
392
+ // Seeded from the graph so loader-level warnings (a truncated Loom index, a tiny
393
+ // source that yielded nothing) reach the caller. ensureMappingAvailable,
394
+ // getClassApiMatrix and checkSymbolExists already do this.
395
+ const warnings = [...graph.warnings];
382
396
  const descriptorProjection = this.projectMethodDescriptorToTarget(graph, path, descriptor);
383
397
  const projectedDescriptor = descriptorProjection.complete ? descriptorProjection.descriptor : undefined;
384
398
  const rawCandidates = this
@@ -475,7 +489,14 @@ export class MappingService {
475
489
  `${method}${descriptor}. ${rejectedByOwner} candidate(s) with that name and descriptor ` +
476
490
  `are declared by other classes (${targetMapping}: ${ownerList}). An inherited or relocated ` +
477
491
  `member cannot resolve here — use find-mapping for an owner-agnostic lookup, or query the ` +
478
- `declaring class directly.`);
492
+ `declaring class directly.` +
493
+ // An owner with no package never projects along the mapping path (simple-name
494
+ // class matches are deliberately excluded), so it is rejected for its shape
495
+ // rather than for inheritance. Saying so stops the message misdirecting.
496
+ (owner.includes(".")
497
+ ? ""
498
+ : ` "${owner}" is also not fully qualified: an unqualified owner cannot be projected ` +
499
+ `into ${targetMapping} and can never match, so supply the fully-qualified owner.`));
479
500
  }
480
501
  if (descriptorProjection.hadClassReferences && !descriptorProjection.complete) {
481
502
  warnings.push(pathUsesSource(graph.pairs, path, "mojang-client-mappings")
@@ -11,7 +11,68 @@ export interface MavenCandidate {
11
11
  sourceUrl?: string;
12
12
  binaryUrl?: string;
13
13
  }
14
+ /**
15
+ * Parses `group:artifact:version[:classifier]` - and is the single place a
16
+ * caller-supplied coordinate is checked before any of it becomes a path.
17
+ *
18
+ * Every segment ends up as a directory or file-name component downstream (the
19
+ * local `~/.m2` layout, the Gradle cache layout, and a remote repository URL),
20
+ * so a segment carrying `/` or `..` would walk out of the repository root it
21
+ * was resolved against. Validating here rather than at each sink means the nine
22
+ * consumers of this function inherit the rule and cannot drift apart - which is
23
+ * exactly how two different group-to-path builders came to exist.
24
+ *
25
+ * The rule is `isSafeMavenSegment`, the same one the `dependency` route now
26
+ * applies to its `group`, `name` and `version` before it probes anything. That
27
+ * route synthesises a coordinate and lands on these same sinks, so the two
28
+ * cannot be allowed to answer differently. Its `..` clause rejects a groupId
29
+ * like `a..b`; that input is contained under both builders and so is not a
30
+ * safety matter, but a doubled dot yields an empty Maven path segment and is
31
+ * not publishable, and rejecting it keeps the two routes answering identically.
32
+ *
33
+ * `version` and `classifier` additionally admit U+0020, because
34
+ * `net.fabricmc:yarn:1.14 Pre-Release 1+build.10:v2` is a published coordinate
35
+ * (see `SAFE_MAVEN_VALUE_RE`); `groupId` and `artifactId` do not.
36
+ *
37
+ * Each segment is trimmed before it is checked, so `"g : a : 1.0"` names the
38
+ * artifact it obviously means instead of failing on the padding, and a leading
39
+ * or trailing space cannot survive into a `version` either. `coordinate` in the
40
+ * error details echoes the argument this function was handed - which is the
41
+ * caller's own text only when the caller reached it directly; the public
42
+ * artifact route trims `target.value` before calling, so outer padding on a
43
+ * `target.kind="coordinate"` request is gone by the time it gets here.
44
+ */
14
45
  export declare function parseCoordinate(coordinate: string): MavenCoordinate;
46
+ /**
47
+ * Whether the artifacts behind a coordinate may change under a stable name.
48
+ *
49
+ * Maven defines exactly one mutable form: a version ending in `-SNAPSHOT`,
50
+ * which a repository is free to republish for the same coordinate. A
51
+ * timestamped unique snapshot (`1.0.0-20240101.120000-3`) is the resolved,
52
+ * concrete build and never changes, so it is immutable - as is every release
53
+ * version. Callers use this to pick a cache freshness policy: mutable
54
+ * coordinates must be revalidated, immutable ones can be served from cache
55
+ * forever.
56
+ *
57
+ * The suffix is matched case-insensitively, unlike Maven's own case-sensitive
58
+ * `ArtifactUtils.isSnapshot`. The two error directions are not symmetric: being
59
+ * stricter than a repository that publishes `-snapshot` would serve a mutable
60
+ * artifact from cache forever, while being looser costs one conditional request
61
+ * per resolve on a version that almost certainly meant `-SNAPSHOT` anyway.
62
+ */
63
+ export declare function isMutableMavenCoordinate(coordinate: string): boolean;
64
+ /**
65
+ * The one group-to-path conversion. Exported so no caller writes its own.
66
+ *
67
+ * `split(".").filter(Boolean).join("/")` is not the same as
68
+ * `replace(/\./g, "/")`: dropping the empty segments means a groupId of `.`,
69
+ * `..` or `.a` yields `""`, `""` and `"a"` rather than `"/"`, `"//"` and
70
+ * `"/a"`. A leading slash is an absolute path, so under `path.resolve` the
71
+ * naive form escapes the repository root entirely - no `/` and no `..` needed
72
+ * in the input. This form cannot produce one, which makes the group leg safe by
73
+ * construction rather than by whatever filter runs upstream of it.
74
+ */
75
+ export declare function groupToPath(groupId: string): string;
15
76
  export declare function normalizedCoordinateValue(coordinate: string): string;
16
77
  export declare function resolveLocalM2Candidate(localM2Path: string, coordinateValue: string): MavenCandidate;
17
78
  export declare function localArtifactPathsFromCoordinate(localM2Path: string, coordinate: string): MavenCandidate;
@@ -1,6 +1,38 @@
1
1
  import { accessSync, readdirSync, statSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { ERROR_CODES, createError } from "./errors.js";
4
+ import { describeSafeMavenSegmentRule, isSafeMavenSegment } from "./maven-token.js";
5
+ /**
6
+ * Parses `group:artifact:version[:classifier]` - and is the single place a
7
+ * caller-supplied coordinate is checked before any of it becomes a path.
8
+ *
9
+ * Every segment ends up as a directory or file-name component downstream (the
10
+ * local `~/.m2` layout, the Gradle cache layout, and a remote repository URL),
11
+ * so a segment carrying `/` or `..` would walk out of the repository root it
12
+ * was resolved against. Validating here rather than at each sink means the nine
13
+ * consumers of this function inherit the rule and cannot drift apart - which is
14
+ * exactly how two different group-to-path builders came to exist.
15
+ *
16
+ * The rule is `isSafeMavenSegment`, the same one the `dependency` route now
17
+ * applies to its `group`, `name` and `version` before it probes anything. That
18
+ * route synthesises a coordinate and lands on these same sinks, so the two
19
+ * cannot be allowed to answer differently. Its `..` clause rejects a groupId
20
+ * like `a..b`; that input is contained under both builders and so is not a
21
+ * safety matter, but a doubled dot yields an empty Maven path segment and is
22
+ * not publishable, and rejecting it keeps the two routes answering identically.
23
+ *
24
+ * `version` and `classifier` additionally admit U+0020, because
25
+ * `net.fabricmc:yarn:1.14 Pre-Release 1+build.10:v2` is a published coordinate
26
+ * (see `SAFE_MAVEN_VALUE_RE`); `groupId` and `artifactId` do not.
27
+ *
28
+ * Each segment is trimmed before it is checked, so `"g : a : 1.0"` names the
29
+ * artifact it obviously means instead of failing on the padding, and a leading
30
+ * or trailing space cannot survive into a `version` either. `coordinate` in the
31
+ * error details echoes the argument this function was handed - which is the
32
+ * caller's own text only when the caller reached it directly; the public
33
+ * artifact route trims `target.value` before calling, so outer padding on a
34
+ * `target.kind="coordinate"` request is gone by the time it gets here.
35
+ */
4
36
  export function parseCoordinate(coordinate) {
5
37
  const values = coordinate.trim().split(":");
6
38
  if (values.length !== 3 && values.length !== 4) {
@@ -10,22 +42,81 @@ export function parseCoordinate(coordinate) {
10
42
  details: { coordinate }
11
43
  });
12
44
  }
13
- const [groupId, artifactId, version, classifier] = values;
45
+ const groupId = values[0]?.trim() ?? "";
46
+ const artifactId = values[1]?.trim() ?? "";
47
+ const version = values[2]?.trim() ?? "";
48
+ const classifier = values[3]?.trim() || undefined;
14
49
  if (!groupId || !artifactId || !version) {
50
+ // Names the empty segment too. Every other rejection below carries
51
+ // `details.component`, and a caller branching on it should not have to
52
+ // special-case the one branch that dropped it.
53
+ const emptyComponent = !groupId
54
+ ? "groupId"
55
+ : !artifactId
56
+ ? "artifactId"
57
+ : "version";
15
58
  throw createError({
16
59
  code: ERROR_CODES.COORDINATE_PARSE_FAILED,
17
60
  message: `Invalid maven coordinate "${coordinate}". All fields must be non-empty.`,
18
- details: { coordinate }
61
+ details: { coordinate, component: emptyComponent }
19
62
  });
20
63
  }
64
+ const segments = [
65
+ ["groupId", groupId],
66
+ ["artifactId", artifactId],
67
+ ["version", version],
68
+ // An absent classifier is not a segment; an empty or whitespace-only one
69
+ // has already normalised to absent above.
70
+ ...(classifier ? [["classifier", classifier]] : [])
71
+ ];
72
+ for (const [component, value] of segments) {
73
+ if (!isSafeMavenSegment(value, component)) {
74
+ throw createError({
75
+ code: ERROR_CODES.COORDINATE_PARSE_FAILED,
76
+ message: `Invalid maven coordinate "${coordinate}". ${describeSafeMavenSegmentRule(component)}`,
77
+ details: { coordinate, component }
78
+ });
79
+ }
80
+ }
21
81
  return {
22
82
  groupId,
23
83
  artifactId,
24
84
  version,
25
- classifier: classifier?.trim() || undefined
85
+ classifier
26
86
  };
27
87
  }
28
- function groupToPath(groupId) {
88
+ /**
89
+ * Whether the artifacts behind a coordinate may change under a stable name.
90
+ *
91
+ * Maven defines exactly one mutable form: a version ending in `-SNAPSHOT`,
92
+ * which a repository is free to republish for the same coordinate. A
93
+ * timestamped unique snapshot (`1.0.0-20240101.120000-3`) is the resolved,
94
+ * concrete build and never changes, so it is immutable - as is every release
95
+ * version. Callers use this to pick a cache freshness policy: mutable
96
+ * coordinates must be revalidated, immutable ones can be served from cache
97
+ * forever.
98
+ *
99
+ * The suffix is matched case-insensitively, unlike Maven's own case-sensitive
100
+ * `ArtifactUtils.isSnapshot`. The two error directions are not symmetric: being
101
+ * stricter than a repository that publishes `-snapshot` would serve a mutable
102
+ * artifact from cache forever, while being looser costs one conditional request
103
+ * per resolve on a version that almost certainly meant `-SNAPSHOT` anyway.
104
+ */
105
+ export function isMutableMavenCoordinate(coordinate) {
106
+ return /-SNAPSHOT$/i.test(parseCoordinate(coordinate).version);
107
+ }
108
+ /**
109
+ * The one group-to-path conversion. Exported so no caller writes its own.
110
+ *
111
+ * `split(".").filter(Boolean).join("/")` is not the same as
112
+ * `replace(/\./g, "/")`: dropping the empty segments means a groupId of `.`,
113
+ * `..` or `.a` yields `""`, `""` and `"a"` rather than `"/"`, `"//"` and
114
+ * `"/a"`. A leading slash is an absolute path, so under `path.resolve` the
115
+ * naive form escapes the repository root entirely - no `/` and no `..` needed
116
+ * in the input. This form cannot produce one, which makes the group leg safe by
117
+ * construction rather than by whatever filter runs upstream of it.
118
+ */
119
+ export function groupToPath(groupId) {
29
120
  return groupId.split(".").filter(Boolean).join("/");
30
121
  }
31
122
  export function normalizedCoordinateValue(coordinate) {
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The one definition of "safe Maven coordinate segment".
3
+ *
4
+ * Every route that turns caller-supplied text into a filesystem path component
5
+ * - the `coordinate` target parsed by `parseCoordinate`, and the `dependency`
6
+ * target that synthesises a coordinate from `group`/`name`/`version` - has to
7
+ * agree on what a segment may contain, because they share the same downstream
8
+ * sinks. Two copies of this rule would be two chances to disagree.
9
+ *
10
+ * Deliberately a leaf module: it imports nothing. `maven-resolver.ts` is the
11
+ * chokepoint that enforces the rule and pulls in only `node:fs`, `node:path`
12
+ * and `./errors.js`; importing it from `workspace-mapping-service.ts`, where
13
+ * the rule used to live, would drag fast-glob and the whole workspace-detection
14
+ * surface into the resolver's import graph.
15
+ *
16
+ * The name records where the rule was first enforced (a dependency target's
17
+ * `version`). The grammar is now shared by all four segments of a coordinate,
18
+ * with one deliberate split between them - see `SAFE_MAVEN_VALUE_RE` - and one
19
+ * deliberately narrower variant for tokens read off disk rather than supplied
20
+ * by a caller - see `isSafeMavenVersionToken`.
21
+ */
22
+ /** The four components of `group:artifact:version[:classifier]`. */
23
+ export type MavenSegmentComponent = "groupId" | "artifactId" | "version" | "classifier";
24
+ /**
25
+ * Characters an IDENTIFIER segment (`groupId`, `artifactId`) may contain.
26
+ *
27
+ * Everything else is out, path separators and NUL and control characters
28
+ * included - a segment becomes a directory or file name verbatim.
29
+ */
30
+ export declare const SAFE_MAVEN_TOKEN_RE: RegExp;
31
+ /**
32
+ * The shared rule for a segment of a CALLER-SUPPLIED coordinate, per component.
33
+ *
34
+ * Both routes go through this and only this: `parseCoordinate` for a
35
+ * `coordinate` target, and `synthesizeDependencyTarget` for the `group`, `name`
36
+ * and explicit `version` of a `dependency` target. They cannot drift apart,
37
+ * because there is no second definition for them to drift towards.
38
+ */
39
+ export declare function isSafeMavenSegment(value: string, component: MavenSegmentComponent): boolean;
40
+ /**
41
+ * The identifier grammar on its own, for a version token DISCOVERED in the
42
+ * workspace rather than stated by a caller: a `gradle.properties` value, a
43
+ * `modules-2` directory entry, an umbrella POM's `<version>`.
44
+ *
45
+ * It deliberately does NOT take the space branch, so this is narrower than
46
+ * `isSafeMavenSegment(value, "version")`. The two inputs are not the same kind
47
+ * of thing. A caller naming `1.14 Pre-Release 1+build.10` has stated which
48
+ * artifact they want, and refusing it loses them a real one. A scan of
49
+ * `~/.gradle/caches/modules-2/.../<name>/` is guessing which of somebody else's
50
+ * directories is a version, and every name it admits becomes a candidate that
51
+ * can make the resolution ambiguous or supply a version the project never
52
+ * declared - so the narrower rule is the right default where nothing is lost by
53
+ * it. Widening it is a separate decision with its own evidence, not a
54
+ * consequence of this one.
55
+ */
56
+ export declare function isSafeMavenVersionToken(token: string): boolean;
57
+ /**
58
+ * The rule in words, for an error message that must not drift from the code.
59
+ *
60
+ * `label` names the field that failed as the CALLER spells it, so a caller
61
+ * reading the message can tell which of the four segments to fix; it defaults
62
+ * to the component name, and the `dependency` route passes its own field names
63
+ * (`group`, `name`) instead.
64
+ */
65
+ export declare function describeSafeMavenSegmentRule(component: MavenSegmentComponent, label?: string): string;
@@ -0,0 +1,120 @@
1
+ /**
2
+ * The one definition of "safe Maven coordinate segment".
3
+ *
4
+ * Every route that turns caller-supplied text into a filesystem path component
5
+ * - the `coordinate` target parsed by `parseCoordinate`, and the `dependency`
6
+ * target that synthesises a coordinate from `group`/`name`/`version` - has to
7
+ * agree on what a segment may contain, because they share the same downstream
8
+ * sinks. Two copies of this rule would be two chances to disagree.
9
+ *
10
+ * Deliberately a leaf module: it imports nothing. `maven-resolver.ts` is the
11
+ * chokepoint that enforces the rule and pulls in only `node:fs`, `node:path`
12
+ * and `./errors.js`; importing it from `workspace-mapping-service.ts`, where
13
+ * the rule used to live, would drag fast-glob and the whole workspace-detection
14
+ * surface into the resolver's import graph.
15
+ *
16
+ * The name records where the rule was first enforced (a dependency target's
17
+ * `version`). The grammar is now shared by all four segments of a coordinate,
18
+ * with one deliberate split between them - see `SAFE_MAVEN_VALUE_RE` - and one
19
+ * deliberately narrower variant for tokens read off disk rather than supplied
20
+ * by a caller - see `isSafeMavenVersionToken`.
21
+ */
22
+ /**
23
+ * Characters an IDENTIFIER segment (`groupId`, `artifactId`) may contain.
24
+ *
25
+ * Everything else is out, path separators and NUL and control characters
26
+ * included - a segment becomes a directory or file name verbatim.
27
+ */
28
+ export const SAFE_MAVEN_TOKEN_RE = /^[A-Za-z0-9._+-]+$/;
29
+ /**
30
+ * The same set plus U+0020, for a VALUE segment (`version`, `classifier`).
31
+ *
32
+ * Fabric publishes Yarn against Minecraft's own pre-release ids, and those ids
33
+ * contain spaces: `net.fabricmc:yarn:1.14 Pre-Release 1+build.10:v2` is a real,
34
+ * published coordinate. A rule without this branch refuses to resolve it - a
35
+ * false rejection of a genuine artifact, which is the worse of the two failure
36
+ * directions here, because a space is not a path separator and cannot traverse
37
+ * a directory. Permitting it costs nothing in containment: the segment still
38
+ * becomes exactly one path component, and every other escape character stays
39
+ * out.
40
+ *
41
+ * `groupId` and `artifactId` stay on the identifier rule. Maven identifiers
42
+ * never contain spaces and no real ecosystem coordinate uses one there, so
43
+ * widening them would buy no artifact and only enlarge the accepted surface.
44
+ */
45
+ const SAFE_MAVEN_VALUE_RE = /^[A-Za-z0-9._+ -]+$/;
46
+ /**
47
+ * Upper bound on a segment. A sanity limit ONLY - it is not, and cannot be, a
48
+ * control on the length of the file name a coordinate produces: `artifactId`,
49
+ * `version` and `classifier` are concatenated into one name
50
+ * (`<artifact>-<version>-<classifier>-sources.jar`), so three segments at this
51
+ * bound still build a name past 600 characters. What the bound does buy is that
52
+ * a pathological megabyte-long input cannot be spent building paths; the
53
+ * filesystem's own NAME_MAX is what refuses an overlong file name, and it does
54
+ * so on open, harmlessly.
55
+ */
56
+ const MAX_MAVEN_TOKEN_LENGTH = 200;
57
+ /** Length, leading dot and `..` - the clauses every token shares. */
58
+ function passesCommonTokenRules(value) {
59
+ if (typeof value !== "string" || value.length === 0 || value.length > MAX_MAVEN_TOKEN_LENGTH) {
60
+ return false;
61
+ }
62
+ if (value.startsWith(".") || value.includes("..")) {
63
+ return false;
64
+ }
65
+ // An already-trimmed value is the contract: a segment with leading or
66
+ // trailing whitespace is rejected rather than repaired here, so the one place
67
+ // that trims (each route, before it validates) stays the one place that
68
+ // decides what "the caller's segment" is. That also keeps the space branch
69
+ // from admitting a version of nothing but spaces.
70
+ return value.trim() === value;
71
+ }
72
+ /**
73
+ * The shared rule for a segment of a CALLER-SUPPLIED coordinate, per component.
74
+ *
75
+ * Both routes go through this and only this: `parseCoordinate` for a
76
+ * `coordinate` target, and `synthesizeDependencyTarget` for the `group`, `name`
77
+ * and explicit `version` of a `dependency` target. They cannot drift apart,
78
+ * because there is no second definition for them to drift towards.
79
+ */
80
+ export function isSafeMavenSegment(value, component) {
81
+ if (!passesCommonTokenRules(value)) {
82
+ return false;
83
+ }
84
+ const pattern = component === "version" || component === "classifier" ? SAFE_MAVEN_VALUE_RE : SAFE_MAVEN_TOKEN_RE;
85
+ return pattern.test(value);
86
+ }
87
+ /**
88
+ * The identifier grammar on its own, for a version token DISCOVERED in the
89
+ * workspace rather than stated by a caller: a `gradle.properties` value, a
90
+ * `modules-2` directory entry, an umbrella POM's `<version>`.
91
+ *
92
+ * It deliberately does NOT take the space branch, so this is narrower than
93
+ * `isSafeMavenSegment(value, "version")`. The two inputs are not the same kind
94
+ * of thing. A caller naming `1.14 Pre-Release 1+build.10` has stated which
95
+ * artifact they want, and refusing it loses them a real one. A scan of
96
+ * `~/.gradle/caches/modules-2/.../<name>/` is guessing which of somebody else's
97
+ * directories is a version, and every name it admits becomes a candidate that
98
+ * can make the resolution ambiguous or supply a version the project never
99
+ * declared - so the narrower rule is the right default where nothing is lost by
100
+ * it. Widening it is a separate decision with its own evidence, not a
101
+ * consequence of this one.
102
+ */
103
+ export function isSafeMavenVersionToken(token) {
104
+ return passesCommonTokenRules(token) && SAFE_MAVEN_TOKEN_RE.test(token);
105
+ }
106
+ /**
107
+ * The rule in words, for an error message that must not drift from the code.
108
+ *
109
+ * `label` names the field that failed as the CALLER spells it, so a caller
110
+ * reading the message can tell which of the four segments to fix; it defaults
111
+ * to the component name, and the `dependency` route passes its own field names
112
+ * (`group`, `name`) instead.
113
+ */
114
+ export function describeSafeMavenSegmentRule(component, label = component) {
115
+ const allowed = component === "version" || component === "classifier"
116
+ ? "[A-Za-z0-9._+-] characters or the space character"
117
+ : "[A-Za-z0-9._+-] characters";
118
+ return `${label} must contain only ${allowed}, must not start with '.', and must not include '..'.`;
119
+ }
120
+ //# sourceMappingURL=maven-token.js.map
@@ -2,7 +2,7 @@ import { ResourceNotFoundError } from "@modelcontextprotocol/server";
2
2
  import { PROBLEM_DETAILS_READ_CACHE_FIELDS } from "./cache-policy.js";
3
3
  import { CLIENT_CAPABILITIES_META_KEY, PROTOCOL_VERSION_META_KEY } from "./era-classifier.js";
4
4
  import { ERROR_CODES } from "./errors.js";
5
- import { retryClassForErrorCode, issueOriginForErrorCode, extractAllowlistedContext } from "./error-mapping.js";
5
+ import { problemClassification, extractAllowlistedContext, extractDidYouMean, extractNestedJarsField } from "./error-mapping.js";
6
6
  import { toHints, extractValidatedSuggestionAndExamples, extractFieldErrorsFromDetails } from "./tool-guidance.js";
7
7
  export function objectResult(data, options = {}) {
8
8
  return {
@@ -90,6 +90,12 @@ export function errorResource(uri, error, ctx) {
90
90
  const { suggestedCall, exampleCalls } = extractValidatedSuggestionAndExamples(details);
91
91
  const fieldErrors = extractFieldErrorsFromDetails(details);
92
92
  const context = extractAllowlistedContext(details);
93
+ const didYouMean = extractDidYouMean(details);
94
+ // The field-returning form, so a SHORTENED inventory says so here too. The
95
+ // inventory-only view would publish a trimmed list indistinguishable from a
96
+ // complete one, and a caller reading it would conclude the class is bundled
97
+ // in none of the inner jars.
98
+ const nestedJarsField = extractNestedJarsField(details);
93
99
  return {
94
100
  // ProblemDetails-read cache override (adopted policy): STRUCTURAL
95
101
  // identification — this constructor IS the error-resource path, so the
@@ -114,12 +120,17 @@ export function errorResource(uri, error, ctx) {
114
120
  status: statusForResourceErrorCode(code),
115
121
  code,
116
122
  instance: uri,
117
- retryClass: retryClassForErrorCode(code),
118
- issueOrigin: issueOriginForErrorCode(code),
123
+ // Shared with the tool path via the one classification builder, so
124
+ // a throw site that classified itself (e.g. a tool-resolved
125
+ // artifact with no binary jar) is not silently re-labelled
126
+ // caller-fixable just because the caller used a resource URI.
127
+ ...problemClassification(code, details),
119
128
  ...(fieldErrors ? { fieldErrors } : {}),
120
129
  ...(hints ? { hints } : {}),
121
130
  ...(suggestedCall ? { suggestedCall } : {}),
122
131
  ...(exampleCalls ? { exampleCalls } : {}),
132
+ ...(didYouMean ? { didYouMean } : {}),
133
+ ...nestedJarsField,
123
134
  ...(context ? { context } : {})
124
135
  },
125
136
  meta: { uri }
@@ -5,7 +5,12 @@ export interface ResponseContext {
5
5
  minecraftVersion: string;
6
6
  mappingType: string;
7
7
  mappingNamespace: MappingNamespace;
8
- jarHash: string;
8
+ /**
9
+ * Cheap per-response staleness stamp derived from the jar's path, mtime and
10
+ * size — NOT a hash of the jar's byte content. Named accordingly so callers
11
+ * do not mistake it for a content identity key.
12
+ */
13
+ jarSignature: string;
9
14
  generatedAt: string;
10
15
  }
11
16
  export interface SignatureMember {
@@ -27,6 +32,13 @@ export interface GetSignatureInput {
27
32
  access?: SignatureAccess;
28
33
  includeSynthetic?: boolean;
29
34
  includeInherited?: boolean;
35
+ /**
36
+ * True when the jar being read is a resolved dependency (Maven/Gradle)
37
+ * artifact rather than a vanilla Minecraft version jar. Dependency jars live
38
+ * under cache paths whose embedded numbers are not Minecraft versions, so the
39
+ * path-derived version heuristic must be skipped for them.
40
+ */
41
+ dependencyOrigin?: boolean;
30
42
  }
31
43
  export interface GetSignatureOutput {
32
44
  classAccessFlags?: number;