@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 +14 -0
- package/README.md +4 -0
- package/dist/entry-tools/validate-project/cases/project-summary.d.ts +6 -1
- package/dist/entry-tools/validate-project/cases/project-summary.js +75 -6
- package/dist/entry-tools/validate-project/internal.d.ts +23 -16
- package/dist/entry-tools/validate-project/internal.js +28 -12
- package/dist/entry-tools/validate-project-service.d.ts +5 -1
- package/dist/entry-tools/validate-project-service.js +2 -2
- package/dist/index.js +6 -3
- package/dist/mapping-service.d.ts +5 -1
- package/dist/mapping-service.js +11 -6
- package/dist/source/artifact-resolver.d.ts +2 -1
- package/dist/source/artifact-resolver.js +98 -1
- package/dist/source/indexer.js +17 -0
- package/dist/source/state.d.ts +5 -0
- package/dist/source/state.js +5 -0
- package/dist/source-resolver.d.ts +1 -0
- package/dist/source-resolver.js +1 -1
- package/dist/source-service.d.ts +17 -0
- package/dist/source-service.js +3 -0
- package/docs/README-ja.md +2 -0
- package/docs/tool-reference.md +9 -6
- package/package.json +15 -3
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
78
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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;
|
package/dist/mapping-service.js
CHANGED
|
@@ -926,11 +926,16 @@ export class MappingService {
|
|
|
926
926
|
}
|
|
927
927
|
/**
|
|
928
928
|
* Probe the mapping graph health for a given version.
|
|
929
|
-
*
|
|
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:
|
|
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:
|
|
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
|
package/dist/source/indexer.js
CHANGED
|
@@ -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", {
|
package/dist/source/state.d.ts
CHANGED
|
@@ -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;
|
package/dist/source/state.js
CHANGED
|
@@ -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;
|
package/dist/source-resolver.js
CHANGED
|
@@ -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");
|
package/dist/source-service.d.ts
CHANGED
|
@@ -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>;
|
package/dist/source-service.js
CHANGED
|
@@ -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
|
|
package/docs/tool-reference.md
CHANGED
|
@@ -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` |
|
|
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`). | `
|
|
277
|
-
| `accessWideners.validated` | At least one Access Widener file was discovered AND every validation completed without throwing. | `counts: { ok, invalid }`. |
|
|
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): `
|
|
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.
|
|
4
|
-
"description": "MCP server
|
|
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",
|