@adhisang/minecraft-modding-mcp 4.1.0 → 4.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project aims to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [4.1.1] - 2026-05-10
9
+
10
+ ### Documentation
11
+ - `package.json` `description` and `keywords` now mirror the GitHub repository's `description` and topics. `description` summarizes the actual feature surface (source inspection, Mojang/Yarn/Intermediary mappings, version diffs, Fabric/Forge/NeoForge mod JAR analysis, Mixin/Access Widener/Access Transformer validation); `keywords` expands from three entries to the published 15-topic set.
12
+
13
+ ### Fixed
14
+ - Concurrent class source/member lookups in the same MCP server process now share one in-flight source indexing/decompile rebuild for the same binary fallback artifact.
15
+ - `validate-project task="project-summary"` no longer performs heavyweight source indexing for its `minecraft.artifact.resolved` task probe. The probe now uses lightweight artifact metadata while preserving the public `tasks` response shape.
16
+ - `validate-mixin` mapping-health checks no longer load the full Tiny mapping graph for `obfuscated` or `mojang` requests, preventing `validate-project task="project-summary"` worker restarts on large Mojang-mapped workspaces.
17
+ - `validate-project task="project-summary"` task report no longer marks executed validators as `skipped` when the lightweight `minecraft.artifact.resolved` probe fails. Mixin / access-widener / access-transformer entries now reflect their real outcome (`ok` with `counts`, or `error`).
18
+ - `validate-mixin` `toolHealth.tinyMappingsAvailable` now means "Tiny is sufficient for the request": `true` when Tiny is not required (`obfuscated` / `mojang`) or is loaded, `false` only when `intermediary` / `yarn` was requested and Tiny is unavailable. `confidenceScore` is no longer penalized when Tiny is intentionally skipped, including on graph-load failure.
19
+ - `validate-project task="project-summary"` lightweight artifact probe defaults omitted `scope` to `"vanilla"` to match `validate-mixin`'s resolve stage, skipping workspace-wide source-jar discovery unless the caller explicitly requests `scope="merged"` or `scope="loader"`.
20
+ - `validate-project task="project-summary"` stage notifications are now best-effort. Telemetry/transport failures no longer abort the summary, count as validation errors, hide nested `validate-mixin` work behind zero-count summaries, or surface as `ERR_ARTIFACT_PROBE_FAILED` in `tasks["minecraft.artifact.resolved"]`.
21
+
8
22
  ## [4.1.0] - 2026-05-09
9
23
 
10
24
  ### Documentation
package/README.md CHANGED
@@ -159,6 +159,8 @@ These notes cover high-frequency decisions during onboarding. For the full pitfa
159
159
  - For unobfuscated releases such as `26.1+`, `check-symbol-exists` and `analyze-symbol task="exists"` validate `mojang` lookups against runtime bytecode when no mapping graph exists, and return `mapping_unavailable` when the runtime JAR itself is unreachable.
160
160
  - `analyze-mod` and `validate-project` require structured `subject` objects and canonical `include` groups; stale string-subject or domain-include payloads return `ERR_INVALID_INPUT` with a retryable `suggestedCall`.
161
161
  - `validate-project task="project-summary"` propagates `preferProjectVersion=true` across discovered Mixin, Access Widener, and Access Transformer checks. If no version can be resolved from the request or `gradle.properties`, the summary returns recovery guidance instead of guessing.
162
+ - `validate-mixin` and `validate-project` keep `mapping-health` lightweight for `obfuscated` and `mojang` validation, avoiding full Tiny mapping graph loads unless `intermediary` or `yarn` namespaces are requested.
163
+ - `validate-project task="project-summary"` uses a lightweight artifact probe for `tasks["minecraft.artifact.resolved"]`; it does not decompile Minecraft or rebuild the source index just to report per-probe status. Set `VALIDATE_PROJECT_TASKS_OFF=1` to omit the additive `tasks` field.
162
164
 
163
165
  ### Inspect Minecraft source from a version
164
166
 
@@ -362,6 +364,8 @@ Tools for querying generated registry data and inspecting server runtime state.
362
364
 
363
365
  Tools that share one resolved artifact or Minecraft version across a fixed shortlist. Results include one status per item plus an aggregate `summary`. See [Batch lookup contract](docs/tool-reference.md#batch-lookup-contract) for failure handling and retry mapping.
364
366
 
367
+ Within one MCP server process, batch class lookups that need the same binary fallback share one in-flight source indexing/decompile rebuild for that artifact.
368
+
365
369
  <!-- BEGIN GENERATED TOOL TABLE: batch-lookup -->
366
370
  | Tool | Purpose |
367
371
  | --- | --- |
@@ -1,7 +1,11 @@
1
1
  import { type DetailLevel } from "../../response-contract.js";
2
+ import type { StageEmitter } from "../../../stage-emitter.js";
2
3
  import type { ValidateProjectInput } from "../../validate-project-service.js";
3
4
  import { type ValidateProjectDeps } from "../internal.js";
4
- export declare function handleProjectSummary(deps: ValidateProjectDeps, input: ValidateProjectInput, detail: DetailLevel, include: string[]): Promise<{
5
+ type ProjectSummaryOptions = {
6
+ stageEmitter?: StageEmitter;
7
+ };
8
+ export declare function handleProjectSummary(deps: ValidateProjectDeps, input: ValidateProjectInput, detail: DetailLevel, include: string[], options?: ProjectSummaryOptions): Promise<{
5
9
  warnings: string[];
6
10
  tasks?: {
7
11
  "workspace.detected": {
@@ -95,3 +99,4 @@ export declare function handleProjectSummary(deps: ValidateProjectDeps, input: V
95
99
  };
96
100
  } | undefined;
97
101
  }>;
102
+ export {};
@@ -2,7 +2,24 @@ import { readFile } from "node:fs/promises";
2
2
  import { buildEntryToolResult, createSummarySubject } from "../../response-contract.js";
3
3
  import { ERROR_CODES, createError } from "../../../errors.js";
4
4
  import { buildEarlyTasksForBlocked, buildFullTaskStatusReport } from "../internal.js";
5
- export async function handleProjectSummary(deps, input, detail, include) {
5
+ // Telemetry failures must not change validation outcomes; swallow rejections
6
+ // so a broken emitter does not abort the summary or count as a validation error.
7
+ async function safeEmit(emitter, stage, payload) {
8
+ if (!emitter)
9
+ return;
10
+ try {
11
+ await emitter(stage, payload);
12
+ }
13
+ catch {
14
+ // swallow telemetry failure
15
+ }
16
+ }
17
+ export async function handleProjectSummary(deps, input, detail, include, options = {}) {
18
+ // Forwarded emitter for nested validators and probes; same swallow contract
19
+ // as safeEmit so a rejecting raw emitter cannot leak into their outcomes.
20
+ const wrappedEmitter = options.stageEmitter
21
+ ? (stage, meta) => safeEmit(options.stageEmitter, stage, meta)
22
+ : undefined;
6
23
  if (input.subject.kind !== "workspace") {
7
24
  throw createError({
8
25
  code: ERROR_CODES.INVALID_INPUT,
@@ -42,6 +59,10 @@ export async function handleProjectSummary(deps, input, detail, include) {
42
59
  }
43
60
  }
44
61
  });
62
+ await safeEmit(options.stageEmitter, "validate-project:task-report", {
63
+ projectPath: input.subject.projectPath,
64
+ reason: "missing-version"
65
+ });
45
66
  const tasks = await buildEarlyTasksForBlocked(input.subject.projectPath, detail, include);
46
67
  return {
47
68
  ...baseResult,
@@ -50,11 +71,15 @@ export async function handleProjectSummary(deps, input, detail, include) {
50
71
  };
51
72
  }
52
73
  const projectPath = input.subject.projectPath;
74
+ const discover = input.subject.discover ?? ["mixins", "access-wideners"];
75
+ await safeEmit(options.stageEmitter, "validate-project:workspace-discovery", {
76
+ projectPath,
77
+ discover
78
+ });
53
79
  const detectedProjectVersion = input.preferProjectVersion
54
80
  ? await deps.detectProjectMinecraftVersion?.(projectPath)
55
81
  : undefined;
56
82
  const resolvedVersion = detectedProjectVersion ?? input.version;
57
- const discover = input.subject.discover ?? ["mixins", "access-wideners"];
58
83
  const [mixinConfigs, accessWideners, accessTransformers] = await Promise.all([
59
84
  discover.includes("mixins")
60
85
  ? deps.discoverMixins(projectPath, input.configPaths)
@@ -102,6 +127,13 @@ export async function handleProjectSummary(deps, input, detail, include) {
102
127
  }
103
128
  }
104
129
  });
130
+ await safeEmit(options.stageEmitter, "validate-project:task-report", {
131
+ projectPath,
132
+ reason: "version-unresolved",
133
+ mixinDiscoveryCount: mixinConfigs.length,
134
+ awDiscoveryCount: accessWideners.length,
135
+ atDiscoveryCount: accessTransformers.length
136
+ });
105
137
  const tasks = await buildEarlyTasksForBlocked(projectPath, detail, include, {
106
138
  mixinDiscoveryCount: mixinConfigs.length,
107
139
  awDiscoveryCount: accessWideners.length,
@@ -144,6 +176,10 @@ export async function handleProjectSummary(deps, input, detail, include) {
144
176
  }
145
177
  }
146
178
  });
179
+ await safeEmit(options.stageEmitter, "validate-project:task-report", {
180
+ projectPath,
181
+ reason: "version-not-required"
182
+ });
147
183
  const tasks = await buildEarlyTasksForBlocked(projectPath, detail, include);
148
184
  return {
149
185
  ...baseResult,
@@ -158,8 +194,16 @@ export async function handleProjectSummary(deps, input, detail, include) {
158
194
  let partialMixins = 0;
159
195
  let invalidMixins = 0;
160
196
  let mixinCaughtErrors = 0;
161
- for (const configPath of mixinConfigs) {
197
+ await safeEmit(options.stageEmitter, "validate-project:mixin-validation", {
198
+ targetTotal: mixinConfigs.length
199
+ });
200
+ for (const [mixinIndex, configPath] of mixinConfigs.entries()) {
162
201
  try {
202
+ await safeEmit(options.stageEmitter, "validate-project:mixin-validation", {
203
+ targetIndex: mixinIndex + 1,
204
+ targetTotal: mixinConfigs.length,
205
+ configPath
206
+ });
163
207
  const mixinResult = await deps.validateMixin({
164
208
  input: {
165
209
  mode: "config",
@@ -180,6 +224,8 @@ export async function handleProjectSummary(deps, input, detail, include) {
180
224
  warningCategoryFilter: input.warningCategoryFilter,
181
225
  treatInfoAsWarning: input.treatInfoAsWarning,
182
226
  includeIssues: input.includeIssues
227
+ }, {
228
+ stageEmitter: wrappedEmitter
183
229
  });
184
230
  const summary = mixinResult.summary;
185
231
  validMixins += summary?.valid ?? 0;
@@ -202,8 +248,16 @@ export async function handleProjectSummary(deps, input, detail, include) {
202
248
  let validAw = 0;
203
249
  let invalidAw = 0;
204
250
  let awCaughtErrors = 0;
205
- for (const awPath of accessWideners) {
251
+ await safeEmit(options.stageEmitter, "validate-project:access-widener-validation", {
252
+ targetTotal: accessWideners.length
253
+ });
254
+ for (const [awIndex, awPath] of accessWideners.entries()) {
206
255
  try {
256
+ await safeEmit(options.stageEmitter, "validate-project:access-widener-validation", {
257
+ targetIndex: awIndex + 1,
258
+ targetTotal: accessWideners.length,
259
+ filePath: awPath
260
+ });
207
261
  const output = await deps.validateAccessWidener({
208
262
  content: await readFile(awPath, "utf8"),
209
263
  version: validationVersion,
@@ -236,8 +290,16 @@ export async function handleProjectSummary(deps, input, detail, include) {
236
290
  let validAt = 0;
237
291
  let invalidAt = 0;
238
292
  let atCaughtErrors = 0;
239
- for (const atPath of accessTransformers) {
293
+ await safeEmit(options.stageEmitter, "validate-project:access-transformer-validation", {
294
+ targetTotal: accessTransformers.length
295
+ });
296
+ for (const [atIndex, atPath] of accessTransformers.entries()) {
240
297
  try {
298
+ await safeEmit(options.stageEmitter, "validate-project:access-transformer-validation", {
299
+ targetIndex: atIndex + 1,
300
+ targetTotal: accessTransformers.length,
301
+ filePath: atPath
302
+ });
241
303
  if (!deps.validateAccessTransformer) {
242
304
  throw createError({
243
305
  code: ERROR_CODES.CONTEXT_UNRESOLVED,
@@ -315,6 +377,12 @@ export async function handleProjectSummary(deps, input, detail, include) {
315
377
  },
316
378
  alwaysBlocks: ["project"]
317
379
  });
380
+ await safeEmit(options.stageEmitter, "validate-project:task-report", {
381
+ projectPath,
382
+ mixinDiscoveryCount: mixinConfigs.length,
383
+ awDiscoveryCount: accessWideners.length,
384
+ atDiscoveryCount: accessTransformers.length
385
+ });
318
386
  const tasks = await buildFullTaskStatusReport(deps, {
319
387
  projectPath,
320
388
  detail,
@@ -335,7 +403,8 @@ export async function handleProjectSummary(deps, input, detail, include) {
335
403
  atDiscoveryCount: accessTransformers.length,
336
404
  atCaughtErrors,
337
405
  atCounts: { ok: validAt, invalid: invalidAt },
338
- atDurationMs
406
+ atDurationMs,
407
+ stageEmitter: wrappedEmitter
339
408
  });
340
409
  return {
341
410
  ...baseResult,
@@ -1,3 +1,4 @@
1
+ import type { StageEmitter } from "../../stage-emitter.js";
1
2
  import type { SourceMapping } from "../../types.js";
2
3
  type TaskStatus = "ok" | "skipped" | "missing" | "error";
3
4
  type TaskEntryBase = {
@@ -44,8 +45,26 @@ type TaskStatusReport = {
44
45
  };
45
46
  };
46
47
  };
48
+ type MinecraftArtifactProbeInput = {
49
+ target: {
50
+ kind: "version";
51
+ value: string;
52
+ };
53
+ mapping?: "obfuscated" | "mojang" | "intermediary" | "yarn";
54
+ sourcePriority?: "loom-first" | "maven-first";
55
+ projectPath?: string;
56
+ scope?: "vanilla" | "merged" | "loader";
57
+ preferProjectVersion?: boolean;
58
+ };
59
+ type MinecraftArtifactProbeOutput = {
60
+ artifactId: string;
61
+ mappingApplied: SourceMapping;
62
+ warnings?: string[];
63
+ };
47
64
  export type ValidateProjectDeps = {
48
- validateMixin: (input: Record<string, unknown>) => Promise<Record<string, unknown> & {
65
+ validateMixin: (input: Record<string, unknown>, options?: {
66
+ stageEmitter?: StageEmitter;
67
+ }) => Promise<Record<string, unknown> & {
49
68
  warnings?: string[];
50
69
  }>;
51
70
  validateAccessWidener: (input: {
@@ -74,21 +93,8 @@ export type ValidateProjectDeps = {
74
93
  discoverAccessWideners: (projectPath: string) => Promise<string[]>;
75
94
  discoverAccessTransformers?: (projectPath: string) => Promise<string[]>;
76
95
  detectProjectMinecraftVersion?: (projectPath: string) => Promise<string | undefined>;
77
- resolveArtifact?: (input: {
78
- target: {
79
- kind: "version";
80
- value: string;
81
- };
82
- mapping?: "obfuscated" | "mojang" | "intermediary" | "yarn";
83
- sourcePriority?: "loom-first" | "maven-first";
84
- projectPath?: string;
85
- scope?: "vanilla" | "merged" | "loader";
86
- preferProjectVersion?: boolean;
87
- }) => Promise<{
88
- artifactId: string;
89
- mappingApplied: SourceMapping;
90
- warnings?: string[];
91
- }>;
96
+ probeMinecraftArtifact?: (input: MinecraftArtifactProbeInput) => Promise<MinecraftArtifactProbeOutput>;
97
+ resolveArtifact?: (input: MinecraftArtifactProbeInput) => Promise<MinecraftArtifactProbeOutput>;
92
98
  };
93
99
  export declare function runUpstreamProbes(projectPath: string): Promise<{
94
100
  workspace: TaskStatusReport["workspace.detected"];
@@ -131,5 +137,6 @@ export declare function buildFullTaskStatusReport(deps: ValidateProjectDeps, arg
131
137
  invalid: number;
132
138
  };
133
139
  atDurationMs: number;
140
+ stageEmitter?: StageEmitter;
134
141
  }): Promise<TaskStatusReport | undefined>;
135
142
  export {};
@@ -126,10 +126,23 @@ async function probeLoomCacheFound(projectPath) {
126
126
  };
127
127
  }
128
128
  }
129
- async function probeMinecraftArtifactResolved(resolveArtifact, args) {
129
+ async function probeMinecraftArtifactResolved(artifactProbe, args, stageEmitter) {
130
130
  const startedAt = Date.now();
131
+ // Stage notification kept outside the probe's try block: a telemetry
132
+ // failure must not be classified as ERR_ARTIFACT_PROBE_FAILED.
131
133
  try {
132
- const output = await resolveArtifact({
134
+ await stageEmitter?.("validate-project:artifact-probe", {
135
+ version: args.version,
136
+ mapping: args.mapping ?? "obfuscated",
137
+ projectPath: args.projectPath,
138
+ scope: args.scope ?? null
139
+ });
140
+ }
141
+ catch {
142
+ // swallow telemetry failure
143
+ }
144
+ try {
145
+ const output = await artifactProbe({
133
146
  target: { kind: "version", value: args.version },
134
147
  mapping: args.mapping,
135
148
  sourcePriority: args.sourcePriority,
@@ -168,16 +181,18 @@ function downstreamSkipReason(report, upstream) {
168
181
  return undefined;
169
182
  }
170
183
  function buildValidationEntryWithCounts(upstream, discoveredCount, errorCount, counts, durationMs) {
184
+ // Real validator outcomes win over upstream skip when validators ran.
185
+ // The artifact probe is informational; its failure must not erase real counts.
186
+ if (discoveredCount > 0 || errorCount > 0) {
187
+ if (errorCount > 0) {
188
+ return { status: "error", durationMs, counts };
189
+ }
190
+ return { status: "ok", durationMs, counts };
191
+ }
171
192
  if (upstream) {
172
193
  return upstream;
173
194
  }
174
- if (discoveredCount === 0) {
175
- return { status: "missing", durationMs };
176
- }
177
- if (errorCount > 0) {
178
- return { status: "error", durationMs, counts };
179
- }
180
- return { status: "ok", durationMs, counts };
195
+ return { status: "missing", durationMs };
181
196
  }
182
197
  function projectTaskEntry(entry, detail, include) {
183
198
  const fullDetail = detail !== "summary" && include.includes("workspace");
@@ -252,15 +267,16 @@ export async function buildFullTaskStatusReport(deps, args) {
252
267
  if (workspace.status !== "ok" || gradle.status !== "ok") {
253
268
  minecraftArtifactResolved = { status: "skipped" };
254
269
  }
255
- else if (deps.resolveArtifact) {
256
- minecraftArtifactResolved = await probeMinecraftArtifactResolved(deps.resolveArtifact, {
270
+ else if (deps.probeMinecraftArtifact ?? deps.resolveArtifact) {
271
+ const artifactProbe = deps.probeMinecraftArtifact ?? deps.resolveArtifact;
272
+ minecraftArtifactResolved = await probeMinecraftArtifactResolved(artifactProbe, {
257
273
  version: args.resolvedVersion,
258
274
  mapping: args.mapping,
259
275
  sourcePriority: args.sourcePriority,
260
276
  projectPath: args.projectPath,
261
277
  scope: args.scope,
262
278
  preferProjectVersion: args.preferProjectVersion
263
- });
279
+ }, args.stageEmitter);
264
280
  }
265
281
  else {
266
282
  minecraftArtifactResolved = { status: "skipped" };
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import type { StageEmitter } from "../stage-emitter.js";
2
3
  import type { SourceMapping } from "../types.js";
3
4
  import { type ValidateProjectDeps } from "./validate-project/internal.js";
4
5
  export type TaskStatus = "ok" | "skipped" | "missing" | "error";
@@ -683,13 +684,16 @@ export declare const validateProjectSchema: z.ZodEffects<z.ZodObject<{
683
684
  include?: string[] | undefined;
684
685
  }>;
685
686
  export type ValidateProjectInput = z.infer<typeof validateProjectSchema>;
687
+ export type ValidateProjectExecuteOptions = {
688
+ stageEmitter?: StageEmitter;
689
+ };
686
690
  export declare function discoverWorkspaceMixins(projectPath: string, configPaths?: string[]): Promise<string[]>;
687
691
  export declare function discoverWorkspaceAccessWideners(projectPath: string): Promise<string[]>;
688
692
  export declare function discoverWorkspaceAccessTransformers(projectPath: string): Promise<string[]>;
689
693
  export declare class ValidateProjectService {
690
694
  private readonly deps;
691
695
  constructor(deps: ValidateProjectDeps);
692
- execute(input: ValidateProjectInput): Promise<Record<string, unknown> & {
696
+ execute(input: ValidateProjectInput, options?: ValidateProjectExecuteOptions): Promise<Record<string, unknown> & {
693
697
  warnings?: string[];
694
698
  }>;
695
699
  }
@@ -263,7 +263,7 @@ export class ValidateProjectService {
263
263
  constructor(deps) {
264
264
  this.deps = deps;
265
265
  }
266
- async execute(input) {
266
+ async execute(input, options = {}) {
267
267
  const detail = resolveDetail(input.detail);
268
268
  const include = resolveInclude(input.include);
269
269
  switch (input.task) {
@@ -274,7 +274,7 @@ export class ValidateProjectService {
274
274
  case "access-transformer":
275
275
  return handleAccessTransformer(this.deps, input, detail, include);
276
276
  case "project-summary":
277
- return handleProjectSummary(this.deps, input, detail, include);
277
+ return handleProjectSummary(this.deps, input, detail, include, options);
278
278
  }
279
279
  }
280
280
  }
package/dist/index.js CHANGED
@@ -129,7 +129,7 @@ const analyzeModService = new AnalyzeModService({
129
129
  remapModJar: (input) => remapModJar(input, config)
130
130
  });
131
131
  const validateProjectService = new ValidateProjectService({
132
- validateMixin: (input) => sourceService.validateMixin(input),
132
+ validateMixin: (input, options) => sourceService.validateMixin(input, options),
133
133
  validateAccessWidener: (input) => sourceService.validateAccessWidener(input),
134
134
  validateAccessTransformer: (input) => sourceService.validateAccessTransformer(input),
135
135
  discoverMixins: discoverWorkspaceMixins,
@@ -150,7 +150,8 @@ const validateProjectService = new ValidateProjectService({
150
150
  mappingApplied: output.mappingApplied,
151
151
  warnings: output.warnings
152
152
  };
153
- }
153
+ },
154
+ probeMinecraftArtifact: (input) => sourceService.probeMinecraftArtifact(input)
154
155
  });
155
156
  const manageCacheService = new ManageCacheService({
156
157
  registry: createCacheRegistry({
@@ -413,7 +414,9 @@ server.tool("compare-minecraft", "High-level v3 entry tool for version compariso
413
414
  registerToolSchema("compare-minecraft", compareMinecraftSchema);
414
415
  server.tool("analyze-mod", "High-level v3 entry tool for mod metadata inspection, decompile/search flows, class source, and safe remap previews/applies.", analyzeModShape, { readOnlyHint: false }, async (args) => runTool("analyze-mod", args, analyzeModSchema, async (input) => analyzeModService.execute(input)));
415
416
  registerToolSchema("analyze-mod", analyzeModSchema);
416
- server.tool("validate-project", "High-level v3 entry tool for project summary, direct mixin validation, and access widener/access transformer validation.", validateProjectShape, { readOnlyHint: true }, async (args) => runTool("validate-project", args, validateProjectSchema, async (input) => validateProjectService.execute(input)));
417
+ server.tool("validate-project", "High-level v3 entry tool for project summary, direct mixin validation, and access widener/access transformer validation.", validateProjectShape, { readOnlyHint: true }, async (args, extra) => runTool("validate-project", args, validateProjectSchema, async (input) => validateProjectService.execute(input, {
418
+ stageEmitter: makeStageEmitter(extra)
419
+ })));
417
420
  registerToolSchema("validate-project", validateProjectSchema);
418
421
  server.tool("manage-cache", "High-level v3 entry tool for cache summaries, listing, verification, previewed mutation, and explicit apply operations.", manageCacheShape, { readOnlyHint: false }, async (args) => runTool("manage-cache", args, manageCacheSchema, async (input) => manageCacheService.execute(input)));
419
422
  registerToolSchema("manage-cache", manageCacheSchema);
@@ -31,7 +31,11 @@ export declare class MappingService {
31
31
  private provenanceForPath;
32
32
  /**
33
33
  * Probe the mapping graph health for a given version.
34
- * Returns availability of mojang mappings, tiny mappings, and member remap paths.
34
+ *
35
+ * `tinyMappingsAvailable` reports whether Tiny is sufficient for the
36
+ * request: `true` when Tiny is not required (obfuscated/mojang) or is
37
+ * loaded, `false` only when intermediary/yarn was requested and Tiny is
38
+ * unavailable.
35
39
  */
36
40
  checkMappingHealth(input: {
37
41
  version: string;
@@ -926,11 +926,16 @@ export class MappingService {
926
926
  }
927
927
  /**
928
928
  * Probe the mapping graph health for a given version.
929
- * Returns availability of mojang mappings, tiny mappings, and member remap paths.
929
+ *
930
+ * `tinyMappingsAvailable` reports whether Tiny is sufficient for the
931
+ * request: `true` when Tiny is not required (obfuscated/mojang) or is
932
+ * loaded, `false` only when intermediary/yarn was requested and Tiny is
933
+ * unavailable.
930
934
  */
931
935
  async checkMappingHealth(input) {
932
936
  const priority = mappingPriorityFromInput(this.config.mappingSourcePriority, input.sourcePriority);
933
937
  const degradations = [];
938
+ const needsTinyMappings = input.requestedMapping === "intermediary" || input.requestedMapping === "yarn";
934
939
  if (isUnobfuscatedVersion(input.version)) {
935
940
  const requestFulfillable = input.requestedMapping === "obfuscated" || input.requestedMapping === "mojang";
936
941
  if (!requestFulfillable) {
@@ -938,19 +943,19 @@ export class MappingService {
938
943
  }
939
944
  return {
940
945
  mojangMappingsAvailable: true,
941
- tinyMappingsAvailable: false,
946
+ tinyMappingsAvailable: !needsTinyMappings,
942
947
  memberRemapAvailable: requestFulfillable,
943
948
  degradations
944
949
  };
945
950
  }
946
951
  let graph;
947
952
  try {
948
- graph = await this.loadGraph(input.version, priority, "full");
953
+ graph = await this.loadGraph(input.version, priority, needsTinyMappings ? "full" : "obfuscated-mojang-only");
949
954
  }
950
955
  catch {
951
956
  return {
952
957
  mojangMappingsAvailable: false,
953
- tinyMappingsAvailable: false,
958
+ tinyMappingsAvailable: !needsTinyMappings,
954
959
  memberRemapAvailable: false,
955
960
  degradations: ["Mapping graph could not be loaded."]
956
961
  };
@@ -967,7 +972,7 @@ export class MappingService {
967
972
  if (!mojangAvailable) {
968
973
  degradations.push("Mojang client mappings are not available for this version.");
969
974
  }
970
- if (!tinyAvailable) {
975
+ if (needsTinyMappings && !tinyAvailable) {
971
976
  degradations.push("No intermediary/yarn tiny mappings were found for this version.");
972
977
  }
973
978
  // Check if member remap path exists (requestedMapping → obfuscated)
@@ -984,7 +989,7 @@ export class MappingService {
984
989
  }
985
990
  return {
986
991
  mojangMappingsAvailable: mojangAvailable,
987
- tinyMappingsAvailable: tinyAvailable,
992
+ tinyMappingsAvailable: needsTinyMappings ? tinyAvailable : true,
988
993
  memberRemapAvailable,
989
994
  degradations
990
995
  };
@@ -1,5 +1,5 @@
1
1
  import type { SourceService } from "../source-service.js";
2
- import type { ArtifactContentsSummary, ResolveArtifactInput, ResolveArtifactOutput } from "../source-service.js";
2
+ import type { ArtifactContentsSummary, ProbeMinecraftArtifactInput, ProbeMinecraftArtifactOutput, ResolveArtifactInput, ResolveArtifactOutput } from "../source-service.js";
3
3
  import type { AccessTransformerNamespace, ArtifactProvenance, ArtifactScope, ArtifactTargetKind, ResolvedSourceArtifact, RuntimeValidationProvenance, SourceMapping } from "../types.js";
4
4
  import type { WorkspaceProjectLoader } from "../workspace-mapping-service.js";
5
5
  export type VersionSourceDiscovery = {
@@ -33,6 +33,7 @@ export declare function discoverVersionSourceJar(_svc: SourceService, input: {
33
33
  version: string;
34
34
  projectPath?: string;
35
35
  }): Promise<VersionSourceDiscovery>;
36
+ export declare function probeMinecraftArtifact(svc: SourceService, input: ProbeMinecraftArtifactInput): Promise<ProbeMinecraftArtifactOutput>;
36
37
  export declare function discoverAccessWidenerRuntimeCandidates(_svc: SourceService, input: {
37
38
  version: string;
38
39
  projectPath?: string;
@@ -7,8 +7,9 @@ import { log } from "../logger.js";
7
7
  import { applyMappingPipeline } from "../mapping-pipeline-service.js";
8
8
  import { parseCoordinate } from "../maven-resolver.js";
9
9
  import { resolveMojangTinyFile } from "../mojang-tiny-mapping-service.js";
10
+ import { artifactSignatureFromFile } from "../path-resolver.js";
10
11
  import { detectFabricLikeInputNamespace, listJavaEntries } from "../source-jar-reader.js";
11
- import { resolveSourceTarget as resolveSourceTargetInternal } from "../source-resolver.js";
12
+ import { artifactIdForJar, resolveSourceTarget as resolveSourceTargetInternal } from "../source-resolver.js";
12
13
  import { resolveTinyRemapperJar } from "../tiny-remapper-resolver.js";
13
14
  import { isUnobfuscatedVersion } from "../version-service.js";
14
15
  import { dedupeQualityFlags, normalizeMapping, normalizeOptionalString, normalizePathStyle } from "./shared-utils.js";
@@ -209,6 +210,102 @@ export async function discoverVersionSourceJar(_svc, input) {
209
210
  selectedHasMinecraftNamespace: selected?.hasMinecraftNamespace
210
211
  };
211
212
  }
213
+ export async function probeMinecraftArtifact(svc, input) {
214
+ let value = input.target.value.trim();
215
+ const warnings = [];
216
+ const requestedMapping = normalizeMapping(input.mapping);
217
+ if (input.preferProjectVersion && input.projectPath) {
218
+ const detected = await svc.workspaceMappingService.detectProjectMinecraftVersion(input.projectPath);
219
+ if (detected && detected !== value) {
220
+ warnings.push(`Overriding version "${value}" with project version "${detected}" from gradle.properties.`);
221
+ }
222
+ value = detected ?? value;
223
+ }
224
+ if (!value) {
225
+ throw createError({
226
+ code: ERROR_CODES.INVALID_INPUT,
227
+ message: "target.value must be non-empty.",
228
+ details: { target: input.target }
229
+ });
230
+ }
231
+ const versionJar = await svc.versionService.resolveVersionJar(value);
232
+ const resolvedVersion = versionJar.version;
233
+ const runtimeNamesUnobfuscated = isUnobfuscatedVersion(resolvedVersion);
234
+ warnings.push(`Resolved Minecraft ${versionJar.version} from ${versionJar.clientJarUrl}.`);
235
+ let effectiveMapping = requestedMapping;
236
+ if ((requestedMapping === "intermediary" || requestedMapping === "yarn") &&
237
+ runtimeNamesUnobfuscated) {
238
+ warnings.push(`Version ${resolvedVersion} is unobfuscated; ${requestedMapping} mappings are not applicable. Using the obfuscated namespace label for the deobfuscated runtime names.`);
239
+ effectiveMapping = "obfuscated";
240
+ }
241
+ if ((effectiveMapping === "intermediary" || effectiveMapping === "yarn") &&
242
+ !runtimeNamesUnobfuscated) {
243
+ throw createError({
244
+ code: ERROR_CODES.MAPPING_NOT_APPLIED,
245
+ message: `Lightweight artifact probe cannot verify ${effectiveMapping} mapping availability without running the full resolver.`,
246
+ details: {
247
+ requestedMapping: effectiveMapping,
248
+ version: resolvedVersion,
249
+ nextAction: "Use a direct validation task for mapping-sensitive checks, or use mapping=obfuscated for the project-summary artifact probe."
250
+ }
251
+ });
252
+ }
253
+ if (effectiveMapping === "mojang" && !runtimeNamesUnobfuscated) {
254
+ // Match validate-mixin's resolve stage: omitted scope defaults to vanilla,
255
+ // avoiding a workspace-wide source-jar scan that validate-mixin would skip.
256
+ const effectiveScope = input.scope ?? "vanilla";
257
+ if (effectiveScope === "vanilla") {
258
+ throw createError({
259
+ code: ERROR_CODES.MAPPING_NOT_APPLIED,
260
+ message: "Lightweight artifact probe cannot verify mojang mapping with scope=vanilla on obfuscated runtime versions.",
261
+ details: {
262
+ requestedMapping: effectiveMapping,
263
+ version: resolvedVersion,
264
+ nextAction: "Retry with scope=merged and projectPath so the probe can use a Loom source jar, or use mapping=obfuscated."
265
+ }
266
+ });
267
+ }
268
+ const versionSourceDiscovery = await svc.discoverVersionSourceJar({
269
+ version: resolvedVersion,
270
+ projectPath: input.projectPath
271
+ });
272
+ if (!versionSourceDiscovery.selectedSourceJarPath) {
273
+ throw createError({
274
+ code: ERROR_CODES.MAPPING_NOT_APPLIED,
275
+ message: "Lightweight artifact probe cannot verify mojang mapping without a source-backed Loom artifact.",
276
+ details: {
277
+ requestedMapping: effectiveMapping,
278
+ version: resolvedVersion,
279
+ searchedPaths: versionSourceDiscovery.searchedPaths,
280
+ candidateArtifacts: versionSourceDiscovery.candidateArtifacts,
281
+ nextAction: "Use mapping=obfuscated for project-summary, or run a direct validation task when full source resolution is required."
282
+ }
283
+ });
284
+ }
285
+ const selectedSourceJarPath = versionSourceDiscovery.selectedSourceJarPath;
286
+ const sourceSignature = artifactSignatureFromFile(selectedSourceJarPath).signature;
287
+ const artifactId = artifactIdForJar("jar", selectedSourceJarPath, sourceSignature);
288
+ warnings.push(`Resolved source-backed artifact from Loom cache candidate: ${selectedSourceJarPath}.`);
289
+ if (versionSourceDiscovery.selectedHasMinecraftNamespace === false) {
290
+ warnings.push(`Source coverage does not include net.minecraft for ${selectedSourceJarPath}; class lookups may fall back to the binary artifact.`);
291
+ }
292
+ if (!hasExactVersionToken(selectedSourceJarPath, value)) {
293
+ warnings.push(`Requested version "${value}" but resolved source jar does not contain exact version string: ${selectedSourceJarPath}`);
294
+ }
295
+ return {
296
+ artifactId,
297
+ mappingApplied: "mojang",
298
+ ...(warnings.length > 0 ? { warnings } : {})
299
+ };
300
+ }
301
+ const binarySignature = artifactSignatureFromFile(versionJar.jarPath).signature;
302
+ const artifactId = artifactIdForJar("jar", versionJar.jarPath, `${binarySignature}:decompile`);
303
+ return {
304
+ artifactId,
305
+ mappingApplied: effectiveMapping,
306
+ ...(warnings.length > 0 ? { warnings } : {})
307
+ };
308
+ }
212
309
  export async function discoverAccessWidenerRuntimeCandidates(_svc, input) {
213
310
  const normalizedProjectPath = normalizeOptionalProjectPath(input.projectPath);
214
311
  const normalizedProjectPathLower = normalizedProjectPath
@@ -323,6 +323,23 @@ export async function ingestIfNeeded(svc, resolved) {
323
323
  touchCacheMetrics(svc, resolved.artifactId, touchedAt);
324
324
  return;
325
325
  }
326
+ const inflight = svc.state.inflightArtifactIngests.get(resolved.artifactId);
327
+ if (inflight) {
328
+ await inflight;
329
+ return;
330
+ }
331
+ const ingestPromise = rebuildMissingArtifactIndex(svc, resolved, reason);
332
+ svc.state.inflightArtifactIngests.set(resolved.artifactId, ingestPromise);
333
+ try {
334
+ await ingestPromise;
335
+ }
336
+ finally {
337
+ if (svc.state.inflightArtifactIngests.get(resolved.artifactId) === ingestPromise) {
338
+ svc.state.inflightArtifactIngests.delete(resolved.artifactId);
339
+ }
340
+ }
341
+ }
342
+ async function rebuildMissingArtifactIndex(svc, resolved, reason) {
326
343
  svc.metrics.recordArtifactCacheMiss();
327
344
  svc.metrics.recordReindex();
328
345
  log("info", "index.rebuild.start", {
@@ -14,6 +14,11 @@ export declare class SourceServiceState {
14
14
  * tiny-remapper run.
15
15
  */
16
16
  readonly inflightRemaps: Map<string, Promise<string>>;
17
+ /**
18
+ * Process-local artifact ingest jobs keyed by artifactId. This collapses
19
+ * concurrent class source/member lookups inside one MCP server process.
20
+ */
21
+ readonly inflightArtifactIngests: Map<string, Promise<void>>;
17
22
  readonly lru: LruList<{
18
23
  totalContentBytes: number;
19
24
  updatedAt: string;
@@ -14,6 +14,11 @@ export class SourceServiceState {
14
14
  * tiny-remapper run.
15
15
  */
16
16
  inflightRemaps = new Map();
17
+ /**
18
+ * Process-local artifact ingest jobs keyed by artifactId. This collapses
19
+ * concurrent class source/member lookups inside one MCP server process.
20
+ */
21
+ inflightArtifactIngests = new Map();
17
22
  lru = new LruList();
18
23
  }
19
24
  //# sourceMappingURL=state.js.map
@@ -1,5 +1,6 @@
1
1
  import type { Config, MappingVariant, ResolvedSourceArtifact, SourceTargetInput } from "./types.js";
2
2
  export type { MappingVariant } from "./types.js";
3
+ export declare function artifactIdForJar(inputKind: string, artifactPath: string, signature: string, suffix?: string, mappingVariant?: MappingVariant): string;
3
4
  export interface ResolveSourceTargetOptions {
4
5
  allowDecompile: boolean;
5
6
  preferBinaryOnly?: boolean;
@@ -133,7 +133,7 @@ async function resolveGradleCacheCoordinateCandidate(coordinate) {
133
133
  function resolveRemoteBinaryCandidate(coordinate, repos) {
134
134
  return buildRemoteBinaryUrls(repos, coordinate);
135
135
  }
136
- function artifactIdForJar(inputKind, artifactPath, signature, suffix, mappingVariant = "pass") {
136
+ export function artifactIdForJar(inputKind, artifactPath, signature, suffix, mappingVariant = "pass") {
137
137
  const parts = [inputKind, artifactPath, signature, suffix ?? "source"];
138
138
  if (mappingVariant === "mojang-remapped") {
139
139
  parts.push("mojang-remapped");
@@ -50,6 +50,22 @@ export type ResolveArtifactOutput = {
50
50
  warnings: string[];
51
51
  sampleEntries?: string[];
52
52
  };
53
+ export type ProbeMinecraftArtifactInput = {
54
+ target: {
55
+ kind: "version";
56
+ value: string;
57
+ };
58
+ mapping?: SourceMapping;
59
+ sourcePriority?: MappingSourcePriority;
60
+ projectPath?: string;
61
+ scope?: ArtifactScope;
62
+ preferProjectVersion?: boolean;
63
+ };
64
+ export type ProbeMinecraftArtifactOutput = {
65
+ artifactId: string;
66
+ mappingApplied: SourceMapping;
67
+ warnings?: string[];
68
+ };
53
69
  export type ArtifactContentsSummary = {
54
70
  sourceKind: "source-jar" | "decompiled-binary";
55
71
  indexedContentKinds: string[];
@@ -508,6 +524,7 @@ export declare class SourceService {
508
524
  workspaceContextCache?: WorkspaceContextCache;
509
525
  });
510
526
  resolveArtifact(input: ResolveArtifactInput): Promise<ResolveArtifactOutput>;
527
+ probeMinecraftArtifact(input: ProbeMinecraftArtifactInput): Promise<ProbeMinecraftArtifactOutput>;
511
528
  synthesizeWorkspaceTarget(input: ResolveArtifactInput, workspace: import("./types.js").WorkspaceTargetInput): ReturnType<typeof workspaceTarget.synthesizeWorkspaceTarget>;
512
529
  synthesizeDependencyTarget(input: ResolveArtifactInput, dep: import("./types.js").DependencyTargetInput): ReturnType<typeof workspaceTarget.synthesizeDependencyTarget>;
513
530
  loadOrDetectWorkspaceContext(projectPath: string): ReturnType<typeof workspaceTarget.loadOrDetectWorkspaceContext>;
@@ -67,6 +67,9 @@ export class SourceService {
67
67
  async resolveArtifact(input) {
68
68
  return artifactResolver.resolveArtifact(this, input);
69
69
  }
70
+ async probeMinecraftArtifact(input) {
71
+ return artifactResolver.probeMinecraftArtifact(this, input);
72
+ }
70
73
  async synthesizeWorkspaceTarget(input, workspace) {
71
74
  return workspaceTarget.synthesizeWorkspaceTarget(this, input, workspace);
72
75
  }
package/docs/README-ja.md CHANGED
@@ -158,6 +158,8 @@ stdio トランスポートは、改行区切り形式と `Content-Length` フ
158
158
  - `trace-symbol-lifecycle` の `symbol` には `Class.method` を指定します。厳密なオーバーロード指定は別フィールドの `descriptor` を使ってください。
159
159
  - ワークスペースのソースカバレッジが部分的な場合でも、バニラクラスを確認できます。`inspect-minecraft task="list-files"` は、その場合に部分的な結果とフォローアップガイダンスを返します。
160
160
  - `analyze-mod` と `validate-project` は、オブジェクト形式の `subject` と正規の `include` グループを要求します。古い文字列形式の `subject` やドメイン名形式の `include` には `ERR_INVALID_INPUT` と、再試行しやすい `suggestedCall` を返します。
161
+ - `validate-mixin` と `validate-project` は、`obfuscated` / `mojang` 検証では `mapping-health` を軽量に保ちます。`intermediary` / `yarn` 名前空間を要求しない限り、完全な Tiny マッピンググラフは読み込みません。
162
+ - `validate-project task="project-summary"` の `tasks["minecraft.artifact.resolved"]` は軽量なアーティファクト probe です。probe 状態を返すためだけに Minecraft のデコンパイルやソースインデックス再構築は行いません。追加の `tasks` フィールドを省きたい場合は `VALIDATE_PROJECT_TASKS_OFF=1` を使います。
161
163
 
162
164
  ### あるバージョンの Minecraft ソースを確認する
163
165
 
@@ -91,6 +91,7 @@ Workspace detection is memoised in a process-resident `WorkspaceContextCache` (1
91
91
  - `validate-mixin` summary-first workflows should combine `includeIssues=false`, `reportMode="compact"`, and `warningMode="aggregated"`.
92
92
  - `validate-mixin` top-level failures expose `failedStage` as a first-class field on the serialized `error` envelope (alongside `code`, `hints`, `suggestedCall`) — one of `"input-validation" | "resolve" | "mapping-health" | "parse" | "target-lookup"`. Branch on that before inspecting `message` to recover automatically (e.g. retry `resolve` failures with `scope="vanilla"`, retry `target-lookup` failures with a different `mapping`). Nested errors that already carry a `failedStage` are preserved so upstream tags (e.g. from `resolve-artifact`) surface unchanged.
93
93
  - `validate-mixin` per-result `quickSummary` appends `Scope fell back from "<requested>" to "<applied>" (<reason>).` when `provenance.scopeFallback` is set and `Mapping health degraded: <degradations>.` when `toolHealth.overallHealthy === false`. Clean validations keep the original one-line summary; the extra notes only attach when the pipeline actually fell back or detected a degraded mapping.
94
+ - `mapping-health` stays lightweight for `obfuscated` and `mojang` requests. It avoids full Tiny mapping graph loads unless the caller requests `intermediary` or `yarn`, where Tiny namespace availability is part of the health check.
94
95
  - `validate-mixin` runs each stage (`resolve` / `mapping-health` / `parse` / `target-lookup`) against an independent soft-deadline. When the `target-lookup` stage exhausts its budget mid-loop, completed targets stay in `targetOutcomes` with `status: "ok"` (and `slowTarget: true` plus `elapsedMs` when the per-target soft cap was exceeded) while remaining targets land as `status: "deferred-budget"`. The summary then carries `targetsDeferredBudget` and `degradedReason: "stage-budget"`, and `validationStatus` is promoted to `"partial"`. If the budget is exhausted before the first iteration, `targetOutcomes` stays empty, `targetsDeferredBudget` is omitted, and `degradedReason: "stage-budget-pre-target"`.
95
96
  - Empty Mixin configs are treated as warning-only discovery results with `summary.total=0` instead of invalid input; malformed JSON still returns `ERR_INVALID_INPUT`.
96
97
 
@@ -148,6 +149,8 @@ Common input fields:
148
149
  - `compact: boolean` (default `true`) — applies the same per-tool projection that the corresponding single tool's `compact: true` mode applies. When `false`, per-entry `result` is byte-identical to the single tool's `compact: false` output.
149
150
  - Per-tool shared inputs (`target`, `mapping`, `projectPath`, `version`, etc.) follow the same shape as the matching single tool.
150
151
 
152
+ Resource behavior: `concurrency` limits per-entry dispatch for one batch call. Within one MCP server process, entries or calls that need the same artifact index or decompiled fallback share the in-flight rebuild by `artifactId`. Separate MCP server processes sharing the same cache are not coordinated by this process-local guard.
153
+
151
154
  Common output:
152
155
 
153
156
  ```json
@@ -272,14 +275,14 @@ These environment variables are read once at worker startup and provide rollback
272
275
  | `workspace.detected` | A `gradle.properties`, `settings.gradle{,.kts}`, or `build.gradle{,.kts}` file exists at `subject.projectPath`. | `evidence: ["gradle.properties", ...]` lists the gradle files that were found. | `missing` when no gradle files exist; `error` when the filesystem read itself failed. |
273
276
  | `gradle.readable` | `gradle.properties` can be read and the workspace's gradle build scripts are enumerable. | `propertiesPath` and `buildScripts[]` (relative paths). | `skipped` when `workspace.detected` is not `ok`; `missing` when no gradle files at all; `error` on parse / read failure. |
274
277
  | `loom.cache.found` | A Loom (Fabric / Quilt) cache directory under the workspace or `GRADLE_USER_HOME` exists. Independent of `workspace.detected` so callers can detect a global Loom cache even on non-gradle workspaces. | `cachePath` of the first matching directory. | `missing` when none of the candidate roots exist; `error` on filesystem failure. |
275
- | `minecraft.artifact.resolved` | `resolve-artifact` with `target: { kind: "version", value: <resolvedVersion> }` succeeds against the workspace context. | `artifactId` and `mappingApplied`. | `skipped` when `workspace.detected` or `gradle.readable` is not `ok`; `error` when `resolve-artifact` throws (carries `error.code` and `error.detail`). |
276
- | `mixins.validated` | At least one `*.mixins.json` file was discovered AND every per-config validation completed without throwing. | `counts: { ok, partial, invalid }` (validation outcomes from `validate-mixin`). | `skipped` when any upstream probe is non-`ok`; `missing` when discovery returned 0 paths; `error` when one or more per-config validations threw. |
277
- | `accessWideners.validated` | At least one Access Widener file was discovered AND every validation completed without throwing. | `counts: { ok, invalid }`. | `skipped` / `missing` / `error` follow the same rules as `mixins.validated`. |
278
- | `accessTransformers.validated` | At least one Access Transformer file was discovered AND every validation completed without throwing. | `counts: { ok, invalid }`. | Same as `mixins.validated`. |
278
+ | `minecraft.artifact.resolved` | A lightweight artifact metadata probe can locate `target: { kind: "version", value: <resolvedVersion> }` against the workspace context. The probe does not decompile Minecraft or rebuild the source index. | `artifactId` and `mappingApplied`. | `skipped` when `workspace.detected` or `gradle.readable` is not `ok`; `error` when the lightweight probe cannot verify the artifact or requested mapping without full resolution (carries `error.code` and `error.detail`). |
279
+ | `mixins.validated` | At least one `*.mixins.json` file was discovered AND every per-config validation completed without throwing. | `counts: { ok, partial, invalid }` (validation outcomes from `validate-mixin`). | `error` when any per-config validation threw (still emits `counts`); `skipped` when discovery was empty AND `workspace.detected` / `gradle.readable` blocked; `missing` when discovery returned 0 paths and upstream probes were `ok`. A failed `minecraft.artifact.resolved` does not flip executed validators to `skipped`. |
280
+ | `accessWideners.validated` | At least one Access Widener file was discovered AND every validation completed without throwing. | `counts: { ok, invalid }`. | Same rules as `mixins.validated`. |
281
+ | `accessTransformers.validated` | At least one Access Transformer file was discovered AND every validation completed without throwing. | `counts: { ok, invalid }`. | Same rules as `mixins.validated`. |
279
282
 
280
- Status precedence (top wins): `skipped` (upstream blocked) > `missing` (no items) > `error` (item-level caught) > `ok`.
283
+ Status precedence (top wins): `error` (item-level caught) > `ok` (validators ran) > `skipped` (upstream env blocked AND no items ran) > `missing` (no items, upstream `ok`).
281
284
 
282
- Output projection: with `detail: "summary"` (or when `include` does not contain `"workspace"`), each `tasks[*]` entry is slimmed to `status`, `error?`, and `warnings?` only — `evidence` / `buildScripts` / `counts` / `propertiesPath` / `cachePath` / `artifactId` / `mapping` / `durationMs` are stripped. With `detail: "full"` (or `"standard"`) AND `include` containing `"workspace"`, every sub-field is preserved. Set `VALIDATE_PROJECT_TASKS_OFF=1` at process start to omit the field entirely (legacy shape).
285
+ Output projection: with `detail: "summary"` (or when `include` does not contain `"workspace"`), each `tasks[*]` entry is slimmed to `status`, `error?`, and `warnings?` only — `evidence` / `buildScripts` / `counts` / `propertiesPath` / `cachePath` / `artifactId` / `mapping` / `durationMs` are stripped. With `detail: "full"` (or `"standard"`) AND `include` containing `"workspace"`, every sub-field is preserved. Set `VALIDATE_PROJECT_TASKS_OFF=1` at process start to omit the field entirely (legacy shape). Use `resolve-artifact` directly when a follow-up source lookup needs a fully indexed artifact.
283
286
  - `validate-access-widener` keeps vanilla validation when `projectPath`, `scope`, and `preferProjectVersion` are omitted. Supplying Loom workspace context switches it into runtime-aware mode, which returns `provenance` and per-entry runtime access evidence without changing the existing summary shape.
284
287
  - `validate-access-transformer` accepts `atNamespace="srg" | "mojang" | "obfuscated"`. When `projectPath` points at a Forge or NeoForge workspace, the tool can infer that namespace automatically and validate against loader/runtime artifacts for `scope="loader"`.
285
288
  - Start with `manage-cache` for cache inventory and safe cleanup. Use `executionMode="preview"` before `executionMode="apply"`.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@adhisang/minecraft-modding-mcp",
3
- "version": "4.1.0",
4
- "description": "MCP server with utilities for Minecraft modding workflows",
3
+ "version": "4.1.1",
4
+ "description": "MCP server for AI-assisted Minecraft modding: inspect decompiled source, resolve Mojang/Yarn/Intermediary mappings, diff versions, analyze Fabric/Forge/NeoForge mod JARs, and validate Mixin, Access Widener, and Access Transformer files.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -36,9 +36,21 @@
36
36
  "validate": "npm run check && npm test && npm run test:coverage && npm run test:perf"
37
37
  },
38
38
  "keywords": [
39
+ "claude",
40
+ "fabric",
41
+ "forge",
39
42
  "mcp",
43
+ "mcp-server",
40
44
  "minecraft",
41
- "modding"
45
+ "minecraft-modding",
46
+ "mixin",
47
+ "modding",
48
+ "model-context-protocol",
49
+ "mojang-mappings",
50
+ "neoforge",
51
+ "nodejs",
52
+ "typescript",
53
+ "yarn-mappings"
42
54
  ],
43
55
  "author": "adhi-jp",
44
56
  "license": "MIT",