@adhisang/minecraft-modding-mcp 2.1.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/CHANGELOG.md +38 -1
  2. package/README.md +224 -802
  3. package/dist/cache-registry.d.ts +95 -0
  4. package/dist/cache-registry.js +541 -0
  5. package/dist/entry-tools/analyze-mod-service.d.ts +207 -0
  6. package/dist/entry-tools/analyze-mod-service.js +309 -0
  7. package/dist/entry-tools/analyze-symbol-service.d.ts +209 -0
  8. package/dist/entry-tools/analyze-symbol-service.js +359 -0
  9. package/dist/entry-tools/compare-minecraft-service.d.ts +210 -0
  10. package/dist/entry-tools/compare-minecraft-service.js +429 -0
  11. package/dist/entry-tools/entry-tool-schema.d.ts +6 -0
  12. package/dist/entry-tools/entry-tool-schema.js +10 -0
  13. package/dist/entry-tools/inspect-minecraft-service.d.ts +1954 -0
  14. package/dist/entry-tools/inspect-minecraft-service.js +1030 -0
  15. package/dist/entry-tools/manage-cache-service.d.ts +130 -0
  16. package/dist/entry-tools/manage-cache-service.js +264 -0
  17. package/dist/entry-tools/request-normalizers.d.ts +10 -0
  18. package/dist/entry-tools/request-normalizers.js +36 -0
  19. package/dist/entry-tools/response-contract.d.ts +45 -0
  20. package/dist/entry-tools/response-contract.js +99 -0
  21. package/dist/entry-tools/validate-project-service.d.ts +543 -0
  22. package/dist/entry-tools/validate-project-service.js +414 -0
  23. package/dist/index.js +183 -59
  24. package/dist/observability.d.ts +18 -2
  25. package/dist/observability.js +47 -10
  26. package/dist/source-service.d.ts +0 -1
  27. package/dist/source-service.js +44 -54
  28. package/dist/storage/files-repo.d.ts +1 -0
  29. package/dist/storage/files-repo.js +29 -5
  30. package/dist/tool-contract-manifest.d.ts +4 -0
  31. package/dist/tool-contract-manifest.js +139 -0
  32. package/package.json +1 -1
@@ -0,0 +1,414 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import fastGlob from "fast-glob";
4
+ import { z } from "zod";
5
+ import { createError, ERROR_CODES } from "../errors.js";
6
+ import { buildIncludeSchema, detailSchema } from "./entry-tool-schema.js";
7
+ import { buildEntryToolResult, createSummarySubject } from "./response-contract.js";
8
+ import { resolveDetail, resolveInclude } from "./request-normalizers.js";
9
+ const nonEmptyString = z.string().trim().min(1);
10
+ const INCLUDE_GROUPS = ["warnings", "issues", "workspace", "recovery"];
11
+ const mixinInputSchema = z.discriminatedUnion("mode", [
12
+ z.object({ mode: z.literal("inline"), source: nonEmptyString }),
13
+ z.object({ mode: z.literal("path"), path: nonEmptyString }),
14
+ z.object({ mode: z.literal("paths"), paths: z.array(nonEmptyString).min(1) }),
15
+ z.object({ mode: z.literal("config"), configPaths: z.array(nonEmptyString).min(1) }),
16
+ z.object({ mode: z.literal("project"), path: nonEmptyString })
17
+ ]);
18
+ const accessWidenerInputSchema = z.discriminatedUnion("mode", [
19
+ z.object({ mode: z.literal("inline"), content: nonEmptyString }),
20
+ z.object({ mode: z.literal("path"), path: nonEmptyString })
21
+ ]);
22
+ const subjectSchema = z.discriminatedUnion("kind", [
23
+ z.object({
24
+ kind: z.literal("workspace"),
25
+ projectPath: nonEmptyString,
26
+ discover: z.array(z.enum(["mixins", "access-wideners"])).optional()
27
+ }),
28
+ z.object({
29
+ kind: z.literal("mixin"),
30
+ input: mixinInputSchema
31
+ }),
32
+ z.object({
33
+ kind: z.literal("access-widener"),
34
+ input: accessWidenerInputSchema
35
+ })
36
+ ]);
37
+ export const validateProjectShape = {
38
+ task: z.enum(["project-summary", "mixin", "access-widener"]),
39
+ subject: subjectSchema,
40
+ version: nonEmptyString.optional(),
41
+ mapping: z.enum(["obfuscated", "mojang", "intermediary", "yarn"]).optional(),
42
+ sourcePriority: z.enum(["loom-first", "maven-first"]).optional(),
43
+ scope: z.enum(["vanilla", "merged", "loader"]).optional(),
44
+ preferProjectVersion: z.boolean().optional(),
45
+ preferProjectMapping: z.boolean().default(false),
46
+ detail: detailSchema.optional(),
47
+ include: buildIncludeSchema(INCLUDE_GROUPS),
48
+ sourceRoots: z.array(nonEmptyString).optional(),
49
+ configPaths: z.array(nonEmptyString).optional(),
50
+ minSeverity: z.enum(["error", "warning", "all"]).default("all"),
51
+ hideUncertain: z.boolean().default(false),
52
+ explain: z.boolean().default(false),
53
+ warningMode: z.enum(["full", "aggregated"]).optional(),
54
+ warningCategoryFilter: z.array(z.enum(["mapping", "configuration", "validation", "resolution", "parse"])).optional(),
55
+ treatInfoAsWarning: z.boolean().default(true),
56
+ includeIssues: z.boolean().default(true)
57
+ };
58
+ export const validateProjectSchema = z.object(validateProjectShape).superRefine((value, ctx) => {
59
+ if (value.task === "project-summary" && value.subject.kind !== "workspace") {
60
+ ctx.addIssue({
61
+ code: z.ZodIssueCode.custom,
62
+ path: ["subject", "kind"],
63
+ message: "task=project-summary requires subject.kind=workspace."
64
+ });
65
+ }
66
+ if (value.task === "mixin" && value.subject.kind !== "mixin") {
67
+ ctx.addIssue({
68
+ code: z.ZodIssueCode.custom,
69
+ path: ["subject", "kind"],
70
+ message: "task=mixin requires subject.kind=mixin."
71
+ });
72
+ }
73
+ if (value.task === "access-widener" && value.subject.kind !== "access-widener") {
74
+ ctx.addIssue({
75
+ code: z.ZodIssueCode.custom,
76
+ path: ["subject", "kind"],
77
+ message: "task=access-widener requires subject.kind=access-widener."
78
+ });
79
+ }
80
+ if (value.configPaths?.length && value.task !== "project-summary") {
81
+ ctx.addIssue({
82
+ code: z.ZodIssueCode.custom,
83
+ path: ["configPaths"],
84
+ message: "configPaths is only supported for task=project-summary workspace discovery."
85
+ });
86
+ }
87
+ });
88
+ export async function discoverWorkspaceMixins(projectPath, configPaths) {
89
+ if (configPaths?.length) {
90
+ return [...configPaths];
91
+ }
92
+ return fastGlob.sync(["**/*.mixins.json"], {
93
+ cwd: projectPath,
94
+ absolute: true,
95
+ onlyFiles: true,
96
+ ignore: ["**/.git/**", "**/build/**", "**/out/**", "**/node_modules/**"]
97
+ });
98
+ }
99
+ export async function discoverWorkspaceAccessWideners(projectPath) {
100
+ const descriptorFiles = fastGlob.sync(["fabric.mod.json", "quilt.mod.json", "**/fabric.mod.json", "**/quilt.mod.json"], {
101
+ cwd: projectPath,
102
+ absolute: true,
103
+ onlyFiles: true,
104
+ ignore: ["**/.git/**", "**/build/**", "**/out/**", "**/node_modules/**"]
105
+ });
106
+ const discovered = new Set();
107
+ for (const descriptorPath of descriptorFiles) {
108
+ try {
109
+ const parsed = JSON.parse(await readFile(descriptorPath, "utf8"));
110
+ const relative = parsed.accessWidener ?? parsed.access_widener;
111
+ if (relative) {
112
+ discovered.add(resolve(descriptorPath, "..", relative));
113
+ }
114
+ }
115
+ catch {
116
+ // ignore malformed descriptors in discovery mode
117
+ }
118
+ }
119
+ return [...discovered].sort((left, right) => left.localeCompare(right));
120
+ }
121
+ export class ValidateProjectService {
122
+ deps;
123
+ constructor(deps) {
124
+ this.deps = deps;
125
+ }
126
+ async execute(input) {
127
+ const detail = resolveDetail(input.detail);
128
+ const include = resolveInclude(input.include);
129
+ switch (input.task) {
130
+ case "mixin": {
131
+ if (input.subject.kind !== "mixin") {
132
+ throw createError({
133
+ code: ERROR_CODES.INVALID_INPUT,
134
+ message: "task=mixin requires subject.kind=mixin."
135
+ });
136
+ }
137
+ const output = await this.deps.validateMixin({
138
+ input: input.subject.input,
139
+ version: input.version,
140
+ mapping: input.mapping,
141
+ sourcePriority: input.sourcePriority,
142
+ scope: input.scope,
143
+ preferProjectVersion: input.preferProjectVersion,
144
+ preferProjectMapping: input.preferProjectMapping,
145
+ sourceRoots: input.sourceRoots,
146
+ minSeverity: input.minSeverity,
147
+ hideUncertain: input.hideUncertain,
148
+ explain: input.explain,
149
+ warningMode: input.warningMode,
150
+ warningCategoryFilter: input.warningCategoryFilter,
151
+ treatInfoAsWarning: input.treatInfoAsWarning,
152
+ includeIssues: input.includeIssues
153
+ });
154
+ const summary = output.summary;
155
+ const invalidCount = summary?.invalid ?? 0;
156
+ const partialCount = summary?.partial ?? 0;
157
+ return {
158
+ ...buildEntryToolResult({
159
+ task: "mixin",
160
+ detail,
161
+ include,
162
+ summary: {
163
+ status: invalidCount > 0 ? "invalid" : partialCount > 0 ? "partial" : "ok",
164
+ headline: `Validated ${summary?.total ?? 0} mixin input(s).`,
165
+ subject: createSummarySubject({
166
+ task: "mixin",
167
+ kind: input.subject.kind,
168
+ input: input.subject.input,
169
+ version: input.version,
170
+ mapping: input.mapping,
171
+ sourcePriority: input.sourcePriority,
172
+ scope: input.scope
173
+ }),
174
+ counts: {
175
+ valid: summary?.valid ?? 0,
176
+ partial: partialCount,
177
+ invalid: invalidCount
178
+ }
179
+ },
180
+ blocks: {
181
+ project: {
182
+ summary
183
+ },
184
+ issues: include.includes("issues") || detail !== "summary" ? output.results : undefined
185
+ },
186
+ alwaysBlocks: ["project"]
187
+ }),
188
+ warnings: Array.isArray(output.warnings) ? output.warnings : []
189
+ };
190
+ }
191
+ case "access-widener": {
192
+ if (input.subject.kind !== "access-widener") {
193
+ throw createError({
194
+ code: ERROR_CODES.INVALID_INPUT,
195
+ message: "task=access-widener requires subject.kind=access-widener."
196
+ });
197
+ }
198
+ const content = input.subject.input.mode === "inline"
199
+ ? input.subject.input.content
200
+ : await readFile(input.subject.input.path, "utf8");
201
+ const output = await this.deps.validateAccessWidener({
202
+ content,
203
+ version: input.version,
204
+ mapping: input.mapping,
205
+ sourcePriority: input.sourcePriority
206
+ });
207
+ return {
208
+ ...buildEntryToolResult({
209
+ task: "access-widener",
210
+ detail,
211
+ include,
212
+ summary: {
213
+ status: output.valid ? "ok" : "invalid",
214
+ headline: output.valid
215
+ ? "Access Widener is valid."
216
+ : "Access Widener contains validation issues.",
217
+ subject: createSummarySubject({
218
+ task: "access-widener",
219
+ kind: input.subject.kind,
220
+ input: input.subject.input,
221
+ version: input.version,
222
+ mapping: input.mapping,
223
+ sourcePriority: input.sourcePriority
224
+ }),
225
+ counts: {
226
+ valid: output.valid ? 1 : 0,
227
+ invalid: output.valid ? 0 : 1
228
+ }
229
+ },
230
+ blocks: {
231
+ project: {
232
+ summary: {
233
+ total: 1,
234
+ valid: output.valid ? 1 : 0,
235
+ invalid: output.valid ? 0 : 1
236
+ }
237
+ },
238
+ issues: include.includes("issues") || detail !== "summary" ? output.issues : undefined
239
+ },
240
+ alwaysBlocks: ["project"]
241
+ }),
242
+ warnings: Array.isArray(output.warnings) ? output.warnings : []
243
+ };
244
+ }
245
+ case "project-summary": {
246
+ if (input.subject.kind !== "workspace") {
247
+ throw createError({
248
+ code: ERROR_CODES.INVALID_INPUT,
249
+ message: "task=project-summary requires subject.kind=workspace."
250
+ });
251
+ }
252
+ if (!input.version && !input.preferProjectVersion) {
253
+ return {
254
+ ...buildEntryToolResult({
255
+ task: "project-summary",
256
+ detail,
257
+ include,
258
+ summary: {
259
+ status: "blocked",
260
+ headline: "project-summary requires version or preferProjectVersion=true.",
261
+ subject: createSummarySubject({
262
+ task: "project-summary",
263
+ kind: input.subject.kind,
264
+ projectPath: input.subject.projectPath,
265
+ discover: input.subject.discover
266
+ }),
267
+ nextActions: [
268
+ {
269
+ tool: "validate-project",
270
+ params: {
271
+ task: "project-summary",
272
+ version: "1.21.10",
273
+ subject: input.subject
274
+ }
275
+ }
276
+ ]
277
+ },
278
+ blocks: {
279
+ workspace: {
280
+ projectPath: input.subject.projectPath
281
+ }
282
+ }
283
+ }),
284
+ warnings: []
285
+ };
286
+ }
287
+ const projectPath = input.subject.projectPath;
288
+ const discover = input.subject.discover ?? ["mixins", "access-wideners"];
289
+ const [mixinConfigs, accessWideners] = await Promise.all([
290
+ discover.includes("mixins")
291
+ ? this.deps.discoverMixins(projectPath, input.configPaths)
292
+ : Promise.resolve([]),
293
+ discover.includes("access-wideners")
294
+ ? this.deps.discoverAccessWideners(projectPath)
295
+ : Promise.resolve([])
296
+ ]);
297
+ const warnings = [];
298
+ let validMixins = 0;
299
+ let partialMixins = 0;
300
+ let invalidMixins = 0;
301
+ for (const configPath of mixinConfigs) {
302
+ try {
303
+ const mixinResult = await this.deps.validateMixin({
304
+ input: {
305
+ mode: "config",
306
+ configPaths: [configPath]
307
+ },
308
+ version: input.version,
309
+ mapping: input.mapping,
310
+ sourcePriority: input.sourcePriority,
311
+ scope: input.scope,
312
+ preferProjectVersion: input.preferProjectVersion,
313
+ preferProjectMapping: input.preferProjectMapping,
314
+ sourceRoots: input.sourceRoots,
315
+ minSeverity: input.minSeverity,
316
+ hideUncertain: input.hideUncertain,
317
+ explain: input.explain,
318
+ warningMode: input.warningMode,
319
+ warningCategoryFilter: input.warningCategoryFilter,
320
+ treatInfoAsWarning: input.treatInfoAsWarning,
321
+ includeIssues: input.includeIssues
322
+ });
323
+ const summary = mixinResult.summary;
324
+ validMixins += summary?.valid ?? 0;
325
+ partialMixins += summary?.partial ?? 0;
326
+ invalidMixins += summary?.invalid ?? 0;
327
+ if (Array.isArray(mixinResult.warnings)) {
328
+ warnings.push(...mixinResult.warnings);
329
+ }
330
+ }
331
+ catch (error) {
332
+ invalidMixins += 1;
333
+ if (error instanceof Error) {
334
+ warnings.push(`${configPath}: ${error.message}`);
335
+ }
336
+ }
337
+ }
338
+ let validAw = 0;
339
+ let invalidAw = 0;
340
+ for (const awPath of accessWideners) {
341
+ try {
342
+ const output = await this.deps.validateAccessWidener({
343
+ content: await readFile(awPath, "utf8"),
344
+ version: input.version,
345
+ mapping: input.mapping,
346
+ sourcePriority: input.sourcePriority
347
+ });
348
+ if (output.valid) {
349
+ validAw += 1;
350
+ }
351
+ else {
352
+ invalidAw += 1;
353
+ }
354
+ if (Array.isArray(output.warnings)) {
355
+ warnings.push(...output.warnings);
356
+ }
357
+ }
358
+ catch (error) {
359
+ invalidAw += 1;
360
+ if (error instanceof Error) {
361
+ warnings.push(error.message);
362
+ }
363
+ }
364
+ }
365
+ const invalidCount = invalidMixins + invalidAw;
366
+ const partialCount = partialMixins;
367
+ const status = invalidCount > 0 ? "invalid" : partialCount > 0 ? "partial" : "ok";
368
+ return {
369
+ ...buildEntryToolResult({
370
+ task: "project-summary",
371
+ detail,
372
+ include,
373
+ summary: {
374
+ status,
375
+ headline: `Validated ${mixinConfigs.length} mixin config(s) and ${accessWideners.length} access widener(s).`,
376
+ subject: createSummarySubject({
377
+ task: "project-summary",
378
+ kind: input.subject.kind,
379
+ projectPath,
380
+ discover: input.subject.discover,
381
+ version: input.version,
382
+ mapping: input.mapping,
383
+ sourcePriority: input.sourcePriority,
384
+ scope: input.scope
385
+ }),
386
+ counts: {
387
+ valid: validMixins + validAw,
388
+ partial: partialCount,
389
+ invalid: invalidCount
390
+ }
391
+ },
392
+ blocks: {
393
+ project: {
394
+ summary: {
395
+ valid: validMixins + validAw,
396
+ partial: partialCount,
397
+ invalid: invalidCount
398
+ }
399
+ },
400
+ workspace: {
401
+ projectPath,
402
+ mixinConfigs,
403
+ accessWideners
404
+ }
405
+ },
406
+ alwaysBlocks: ["project"]
407
+ }),
408
+ warnings
409
+ };
410
+ }
411
+ }
412
+ }
413
+ }
414
+ //# sourceMappingURL=validate-project-service.js.map