@adhisang/minecraft-modding-mcp 3.2.0 → 4.0.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 (38) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +25 -18
  3. package/dist/cache-registry.d.ts +1 -1
  4. package/dist/cache-registry.js +10 -2
  5. package/dist/config.d.ts +10 -1
  6. package/dist/config.js +52 -1
  7. package/dist/entry-tools/analyze-symbol-service.d.ts +2 -2
  8. package/dist/entry-tools/analyze-symbol-service.js +13 -2
  9. package/dist/entry-tools/inspect-minecraft-service.d.ts +20 -20
  10. package/dist/entry-tools/inspect-minecraft-service.js +8 -2
  11. package/dist/entry-tools/manage-cache-service.d.ts +4 -4
  12. package/dist/entry-tools/validate-project-service.js +84 -4
  13. package/dist/index.js +99 -33
  14. package/dist/lru-list.d.ts +31 -0
  15. package/dist/lru-list.js +102 -0
  16. package/dist/mapping-pipeline-service.d.ts +10 -1
  17. package/dist/mapping-pipeline-service.js +13 -1
  18. package/dist/mapping-service.d.ts +12 -0
  19. package/dist/mapping-service.js +252 -10
  20. package/dist/mixin-validator.js +22 -7
  21. package/dist/observability.d.ts +18 -1
  22. package/dist/observability.js +44 -1
  23. package/dist/response-utils.d.ts +44 -10
  24. package/dist/response-utils.js +131 -17
  25. package/dist/source-resolver.d.ts +9 -1
  26. package/dist/source-resolver.js +14 -6
  27. package/dist/source-service.d.ts +97 -1
  28. package/dist/source-service.js +922 -113
  29. package/dist/storage/artifacts-repo.d.ts +4 -1
  30. package/dist/storage/artifacts-repo.js +33 -5
  31. package/dist/storage/files-repo.d.ts +0 -2
  32. package/dist/storage/files-repo.js +0 -11
  33. package/dist/storage/migrations.d.ts +1 -1
  34. package/dist/storage/migrations.js +10 -2
  35. package/dist/storage/schema.d.ts +2 -0
  36. package/dist/storage/schema.js +25 -0
  37. package/dist/types.d.ts +3 -0
  38. package/package.json +3 -1
@@ -606,6 +606,7 @@ export class InspectMinecraftService {
606
606
  artifact: resolved.artifact
607
607
  ? {
608
608
  artifactId: resolved.artifact.artifactId,
609
+ artifactAlias: resolved.artifact.artifactAlias,
609
610
  origin: resolved.artifact.origin,
610
611
  mappingApplied: resolved.artifact.mappingApplied,
611
612
  version: resolved.artifact.version,
@@ -961,9 +962,14 @@ export class InspectMinecraftService {
961
962
  }
962
963
  },
963
964
  members: include.includes("members") || detail !== "summary"
964
- ? members.members
965
+ ? {
966
+ ...members.members,
967
+ ...(members.decompiledFallback ? { decompiledFallback: members.decompiledFallback } : {}),
968
+ ...(members.decompiledMemberCounts ? { decompiledMemberCounts: members.decompiledMemberCounts } : {})
969
+ }
965
970
  : {
966
- counts: members.counts
971
+ counts: members.counts,
972
+ ...(members.decompiledMemberCounts ? { decompiledMemberCounts: members.decompiledMemberCounts } : {})
967
973
  }
968
974
  },
969
975
  alwaysBlocks: ["subject"]
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  import { type CacheRegistry } from "../cache-registry.js";
3
3
  export declare const manageCacheShape: {
4
4
  action: z.ZodEnum<["summary", "list", "inspect", "delete", "prune", "rebuild", "verify"]>;
5
- cacheKinds: z.ZodOptional<z.ZodArray<z.ZodEnum<["artifact-index", "downloads", "mapping", "registry", "decompiled-source", "mod-remap"]>, "many">>;
5
+ cacheKinds: z.ZodOptional<z.ZodArray<z.ZodEnum<["artifact-index", "downloads", "mapping", "registry", "decompiled-source", "mod-remap", "binary-remap"]>, "many">>;
6
6
  selector: z.ZodOptional<z.ZodObject<{
7
7
  artifactId: z.ZodOptional<z.ZodString>;
8
8
  version: z.ZodOptional<z.ZodString>;
@@ -42,7 +42,7 @@ export declare const manageCacheShape: {
42
42
  };
43
43
  export declare const manageCacheSchema: z.ZodObject<{
44
44
  action: z.ZodEnum<["summary", "list", "inspect", "delete", "prune", "rebuild", "verify"]>;
45
- cacheKinds: z.ZodOptional<z.ZodArray<z.ZodEnum<["artifact-index", "downloads", "mapping", "registry", "decompiled-source", "mod-remap"]>, "many">>;
45
+ cacheKinds: z.ZodOptional<z.ZodArray<z.ZodEnum<["artifact-index", "downloads", "mapping", "registry", "decompiled-source", "mod-remap", "binary-remap"]>, "many">>;
46
46
  selector: z.ZodOptional<z.ZodObject<{
47
47
  artifactId: z.ZodOptional<z.ZodString>;
48
48
  version: z.ZodOptional<z.ZodString>;
@@ -86,7 +86,7 @@ export declare const manageCacheSchema: z.ZodObject<{
86
86
  cursor?: string | undefined;
87
87
  detail?: "full" | "summary" | "standard" | undefined;
88
88
  include?: string[] | undefined;
89
- cacheKinds?: ("artifact-index" | "downloads" | "mapping" | "registry" | "decompiled-source" | "mod-remap")[] | undefined;
89
+ cacheKinds?: ("artifact-index" | "downloads" | "mapping" | "registry" | "decompiled-source" | "mod-remap" | "binary-remap")[] | undefined;
90
90
  selector?: {
91
91
  mapping?: string | undefined;
92
92
  jarPath?: string | undefined;
@@ -105,7 +105,7 @@ export declare const manageCacheSchema: z.ZodObject<{
105
105
  detail?: "full" | "summary" | "standard" | undefined;
106
106
  include?: string[] | undefined;
107
107
  executionMode?: "preview" | "apply" | undefined;
108
- cacheKinds?: ("artifact-index" | "downloads" | "mapping" | "registry" | "decompiled-source" | "mod-remap")[] | undefined;
108
+ cacheKinds?: ("artifact-index" | "downloads" | "mapping" | "registry" | "decompiled-source" | "mod-remap" | "binary-remap")[] | undefined;
109
109
  selector?: {
110
110
  mapping?: string | undefined;
111
111
  jarPath?: string | undefined;
@@ -269,7 +269,32 @@ export class ValidateProjectService {
269
269
  if (input.subject.kind !== "mixin") {
270
270
  throw createError({
271
271
  code: ERROR_CODES.INVALID_INPUT,
272
- message: "task=mixin requires subject.kind=mixin."
272
+ message: "task=mixin requires subject.kind=mixin.",
273
+ details: {
274
+ task: input.task,
275
+ subjectKind: input.subject.kind,
276
+ failedStage: "input-validation",
277
+ nextAction: "Set subject.kind to \"mixin\" for task=\"mixin\"."
278
+ }
279
+ });
280
+ }
281
+ if (!input.version) {
282
+ throw createError({
283
+ code: ERROR_CODES.INVALID_INPUT,
284
+ message: "task=mixin requires version.",
285
+ details: {
286
+ task: "mixin",
287
+ failedStage: "input-validation",
288
+ nextAction: "Pass version explicitly (e.g. \"1.21.10\"). task=\"project-summary\" supports preferProjectVersion for auto-detection from gradle.properties, but direct task=\"mixin\" requires an explicit version.",
289
+ suggestedCall: {
290
+ tool: "validate-project",
291
+ params: {
292
+ task: "mixin",
293
+ subject: input.subject,
294
+ version: "1.21.10"
295
+ }
296
+ }
297
+ }
273
298
  });
274
299
  }
275
300
  const output = await this.deps.validateMixin({
@@ -330,7 +355,32 @@ export class ValidateProjectService {
330
355
  if (input.subject.kind !== "access-widener") {
331
356
  throw createError({
332
357
  code: ERROR_CODES.INVALID_INPUT,
333
- message: "task=access-widener requires subject.kind=access-widener."
358
+ message: "task=access-widener requires subject.kind=access-widener.",
359
+ details: {
360
+ task: input.task,
361
+ subjectKind: input.subject.kind,
362
+ failedStage: "input-validation",
363
+ nextAction: "Set subject.kind to \"access-widener\" for task=\"access-widener\"."
364
+ }
365
+ });
366
+ }
367
+ if (!input.version) {
368
+ throw createError({
369
+ code: ERROR_CODES.INVALID_INPUT,
370
+ message: "task=access-widener requires version.",
371
+ details: {
372
+ task: "access-widener",
373
+ failedStage: "input-validation",
374
+ nextAction: "Pass version explicitly (e.g. \"1.21.10\"). Access Widener validation resolves class names against a specific Minecraft version.",
375
+ suggestedCall: {
376
+ tool: "validate-project",
377
+ params: {
378
+ task: "access-widener",
379
+ subject: input.subject,
380
+ version: "1.21.10"
381
+ }
382
+ }
383
+ }
334
384
  });
335
385
  }
336
386
  const content = input.subject.input.mode === "inline"
@@ -386,7 +436,32 @@ export class ValidateProjectService {
386
436
  if (input.subject.kind !== "access-transformer") {
387
437
  throw createError({
388
438
  code: ERROR_CODES.INVALID_INPUT,
389
- message: "task=access-transformer requires subject.kind=access-transformer."
439
+ message: "task=access-transformer requires subject.kind=access-transformer.",
440
+ details: {
441
+ task: input.task,
442
+ subjectKind: input.subject.kind,
443
+ failedStage: "input-validation",
444
+ nextAction: "Set subject.kind to \"access-transformer\" for task=\"access-transformer\"."
445
+ }
446
+ });
447
+ }
448
+ if (!input.version) {
449
+ throw createError({
450
+ code: ERROR_CODES.INVALID_INPUT,
451
+ message: "task=access-transformer requires version.",
452
+ details: {
453
+ task: "access-transformer",
454
+ failedStage: "input-validation",
455
+ nextAction: "Pass version explicitly (e.g. \"1.21.10\"). Access Transformer validation resolves class names against a specific Minecraft version.",
456
+ suggestedCall: {
457
+ tool: "validate-project",
458
+ params: {
459
+ task: "access-transformer",
460
+ subject: input.subject,
461
+ version: "1.21.10"
462
+ }
463
+ }
464
+ }
390
465
  });
391
466
  }
392
467
  const content = input.subject.input.mode === "inline"
@@ -395,7 +470,12 @@ export class ValidateProjectService {
395
470
  if (!this.deps.validateAccessTransformer) {
396
471
  throw createError({
397
472
  code: ERROR_CODES.CONTEXT_UNRESOLVED,
398
- message: "Access Transformer validation is not configured."
473
+ message: "Access Transformer validation is not configured.",
474
+ details: {
475
+ task: "access-transformer",
476
+ failedStage: "dependency-resolution",
477
+ nextAction: "The current runtime was built without an Access Transformer validator. Rebuild the MCP server with validateAccessTransformer configured, or use task=\"access-widener\" if the workspace uses Fabric AccessWideners."
478
+ }
399
479
  });
400
480
  }
401
481
  const output = await this.deps.validateAccessTransformer({
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import { ZodError, z } from "zod";
4
4
  import { CompatStdioServerTransport } from "./compat-stdio-transport.js";
5
5
  import { objectResult } from "./mcp-helpers.js";
6
6
  import { prepareToolInput } from "./tool-input.js";
7
- import { isCompactEnabled, COMPACT_MAPPING_TOOL_NAMES, compactResponse, compactArtifactResponse, compactMappingResponse } from "./response-utils.js";
7
+ import { isCompactEnabled, COMPACT_MAPPING_TOOL_NAMES, COMPACT_SOURCE_TOOL_NAMES, COMPACT_MEMBERS_TOOL_NAMES, COMPACT_LIGHT_TOOL_NAMES, TOOL_PRESERVE_PAYLOAD_KEYS, compactResponse, compactArtifactResponse, compactMappingResponse, compactSourceResponse, compactMembersResponse, compactLightResponse } from "./response-utils.js";
8
8
  import { loadConfig } from "./config.js";
9
9
  import { createError, ERROR_CODES, isAppError } from "./errors.js";
10
10
  import { log } from "./logger.js";
@@ -60,6 +60,19 @@ const heavyToolExecutionGate = new ToolExecutionGate({ maxConcurrent: 1, maxQueu
60
60
  const nonEmptyString = z.string().trim().min(1);
61
61
  const optionalNonEmptyString = z.string().trim().min(1).optional();
62
62
  const optionalPositiveInt = z.number().int().positive().optional();
63
+ // Optional descriptor: "" and whitespace-only strings are normalized to undefined so that
64
+ // tools with signatureMode="name-only" can accept "caller omitted descriptor" inputs whether
65
+ // the caller passed an empty string or omitted the field entirely. Malformed descriptors are
66
+ // still rejected downstream by normalizeMethodDescriptor in mapping-service.
67
+ const optionalDescriptorString = z
68
+ .string()
69
+ .optional()
70
+ .transform((value) => {
71
+ if (value === undefined)
72
+ return undefined;
73
+ const trimmed = value.trim();
74
+ return trimmed.length === 0 ? undefined : trimmed;
75
+ });
63
76
  const sourceMappingSchema = z.enum(SOURCE_MAPPINGS);
64
77
  const mappingSourcePrioritySchema = z.enum(SOURCE_PRIORITIES);
65
78
  const targetKindSchema = z.enum(TARGET_KINDS);
@@ -131,8 +144,9 @@ const resolveArtifactShape = {
131
144
  scope: artifactScopeSchema.optional().describe(SOURCE_SCOPE_DESCRIPTION),
132
145
  preferProjectVersion: z.boolean().optional().describe("When true, detect MC version from gradle.properties and override target.value"),
133
146
  strictVersion: z.boolean().optional().describe("When true, reject version-approximated results instead of returning them. Default false."),
134
- compact: z.boolean().default(false).describe("When true, return minimal fields (artifactId, origin, isDecompiled, version, requestedMapping, mappingApplied, qualityFlags). "
135
- + "Omit provenance, artifactContents, sampleEntries, adjacentSourceCandidates, binaryJarPath, coordinate, repoUrl, resolvedSourceJarPath.")
147
+ compact: z.boolean().default(true).describe("Return minimal fields (artifactId, origin, isDecompiled, version, requestedMapping, mappingApplied, qualityFlags). "
148
+ + "Omit provenance, artifactContents, sampleEntries, adjacentSourceCandidates, binaryJarPath, coordinate, repoUrl, resolvedSourceJarPath. "
149
+ + "Enabled by default; set to false for full output.")
136
150
  };
137
151
  const resolveArtifactSchema = z.object(resolveArtifactShape);
138
152
  const getClassSourceShape = {
@@ -150,7 +164,8 @@ const getClassSourceShape = {
150
164
  endLine: optionalPositiveInt,
151
165
  maxLines: optionalPositiveInt,
152
166
  maxChars: optionalPositiveInt.describe("Hard character limit on sourceText; truncates if exceeded"),
153
- outputFile: optionalNonEmptyString.describe("Write source to this file path and return metadata-only response")
167
+ outputFile: optionalNonEmptyString.describe("Write source to this file path and return metadata-only response"),
168
+ compact: z.boolean().default(false).describe("When true, strip debug metadata (provenance, artifactContents, qualityFlags) and empty fields from the response. Default false.")
154
169
  };
155
170
  const getClassSourceSchema = z
156
171
  .object(getClassSourceShape)
@@ -179,7 +194,8 @@ const getClassMembersShape = {
179
194
  projectPath: optionalNonEmptyString,
180
195
  scope: artifactScopeSchema.optional().describe(SOURCE_SCOPE_DESCRIPTION),
181
196
  preferProjectVersion: z.boolean().optional().describe("When true, detect MC version from gradle.properties and override version"),
182
- strictVersion: z.boolean().optional().describe("When true, reject version-approximated results instead of returning them. Default false.")
197
+ strictVersion: z.boolean().optional().describe("When true, reject version-approximated results instead of returning them. Default false."),
198
+ compact: z.boolean().default(false).describe("When true, strip debug metadata (provenance, artifactContents, qualityFlags, context) and empty fields from the response. Default false.")
183
199
  };
184
200
  const getClassMembersSchema = z.object(getClassMembersShape);
185
201
  const searchClassSourceShape = {
@@ -192,7 +208,10 @@ const searchClassSourceShape = {
192
208
  symbolKind: searchSymbolKindSchema.optional().describe("class | interface | enum | record | method | field"),
193
209
  queryMode: z.enum(["auto", "token", "literal"]).default("auto").describe("auto: indexed search, including separator queries like foo.bar; token: indexed-only; literal: explicit substring scan only"),
194
210
  limit: optionalPositiveInt.default(20),
195
- cursor: optionalNonEmptyString
211
+ cursor: optionalNonEmptyString,
212
+ queryNamespace: sourceMappingSchema.optional().describe("Namespace of the query. When set and intent='symbol' with a fully-qualified class name, the query is translated through find-mapping before searching the artifact namespace. Ignored for text/path intents (warning surfaced)."),
213
+ sourcePriority: mappingSourcePrioritySchema.optional().describe("loom-first | maven-first. Used only when queryNamespace triggers translation."),
214
+ compact: z.boolean().default(false).describe("When true, strip the artifactContents summary and empty fields from the response. Default false.")
196
215
  };
197
216
  const searchClassSourceSchema = z.object(searchClassSourceShape).superRefine((value, ctx) => {
198
217
  if (value.symbolKind && value.intent && value.intent !== "symbol") {
@@ -213,12 +232,13 @@ const listArtifactFilesShape = {
213
232
  artifactId: nonEmptyString,
214
233
  prefix: optionalNonEmptyString,
215
234
  limit: optionalPositiveInt,
216
- cursor: optionalNonEmptyString
235
+ cursor: optionalNonEmptyString,
236
+ compact: z.boolean().default(false).describe("When true, strip the artifactContents summary and empty fields from the response. Default false.")
217
237
  };
218
238
  const listArtifactFilesSchema = z.object(listArtifactFilesShape);
219
239
  const traceSymbolLifecycleShape = {
220
240
  symbol: nonEmptyString.describe("fully.qualified.Class.method"),
221
- descriptor: optionalNonEmptyString.describe('optional JVM descriptor, e.g. "(I)V"'),
241
+ descriptor: optionalDescriptorString.describe('optional JVM descriptor, e.g. "(I)V". Empty strings are treated as omitted.'),
222
242
  fromVersion: optionalNonEmptyString,
223
243
  toVersion: optionalNonEmptyString,
224
244
  mapping: sourceMappingSchema.optional().describe("obfuscated | mojang | intermediary | yarn (default obfuscated)"),
@@ -242,10 +262,12 @@ const findMappingShape = {
242
262
  kind: workspaceSymbolKindSchema.describe("class | field | method"),
243
263
  name: nonEmptyString,
244
264
  owner: optionalNonEmptyString,
245
- descriptor: optionalNonEmptyString,
265
+ descriptor: optionalDescriptorString.describe("JVM descriptor. Optional when signatureMode='name-only' (default). Empty strings are treated as omitted."),
246
266
  sourceMapping: sourceMappingSchema.describe("obfuscated | mojang | intermediary | yarn"),
247
267
  targetMapping: sourceMappingSchema.describe("obfuscated | mojang | intermediary | yarn"),
248
268
  sourcePriority: mappingSourcePrioritySchema.optional().describe("loom-first | maven-first"),
269
+ signatureMode: z.enum(["exact", "name-only"]).default("name-only")
270
+ .describe("exact: descriptor required for kind=method; name-only (default): match by owner+name only"),
249
271
  disambiguation: z
250
272
  .object({
251
273
  ownerHint: optionalNonEmptyString,
@@ -253,9 +275,10 @@ const findMappingShape = {
253
275
  })
254
276
  .partial()
255
277
  .optional(),
256
- maxCandidates: optionalPositiveInt.default(200).describe("Limit returned candidates (max 200)"),
257
- compact: z.boolean().default(false).describe("When true, omit top-level empty arrays, null/undefined values, and empty objects from the response. "
258
- + "Also omit redundant candidates array for single full-confidence exact-match resolutions.")
278
+ maxCandidates: optionalPositiveInt.default(5).describe("Limit returned candidates (default 5, max 200). Raise when you need the full candidate list."),
279
+ compact: z.boolean().default(true).describe("Omit top-level empty arrays, null/undefined values, and empty objects from the response. "
280
+ + "Also omit redundant candidates array for single full-confidence exact-match resolutions. "
281
+ + "Enabled by default; set to false for full output.")
259
282
  };
260
283
  const findMappingSchema = z.object(findMappingShape).superRefine((value, ctx) => {
261
284
  if (value.kind === "class") {
@@ -306,10 +329,10 @@ const findMappingSchema = z.object(findMappingShape).superRefine((value, ctx) =>
306
329
  }
307
330
  return;
308
331
  }
309
- if (!value.descriptor) {
332
+ if (!value.descriptor && value.signatureMode !== "name-only") {
310
333
  ctx.addIssue({
311
334
  code: z.ZodIssueCode.custom,
312
- message: "descriptor is required when kind=method.",
335
+ message: "descriptor is required when kind=method (use signatureMode='name-only' to match by name only).",
313
336
  path: ["descriptor"]
314
337
  });
315
338
  }
@@ -322,9 +345,10 @@ const resolveMethodMappingExactShape = {
322
345
  sourceMapping: sourceMappingSchema.describe("obfuscated | mojang | intermediary | yarn"),
323
346
  targetMapping: sourceMappingSchema.describe("obfuscated | mojang | intermediary | yarn"),
324
347
  sourcePriority: mappingSourcePrioritySchema.optional().describe("loom-first | maven-first"),
325
- maxCandidates: optionalPositiveInt.default(200).describe("Limit returned candidates (max 200)"),
326
- compact: z.boolean().default(false).describe("When true, omit top-level empty arrays, null/undefined values, and empty objects from the response. "
327
- + "Also omit redundant candidates array for single full-confidence exact-match resolutions.")
348
+ maxCandidates: optionalPositiveInt.default(5).describe("Limit returned candidates (default 5, max 200). Raise when you need the full candidate list."),
349
+ compact: z.boolean().default(true).describe("Omit top-level empty arrays, null/undefined values, and empty objects from the response. "
350
+ + "Also omit redundant candidates array for single full-confidence exact-match resolutions. "
351
+ + "Enabled by default; set to false for full output.")
328
352
  };
329
353
  const resolveMethodMappingExactSchema = z
330
354
  .object(resolveMethodMappingExactShape)
@@ -372,12 +396,13 @@ const resolveWorkspaceSymbolShape = {
372
396
  kind: workspaceSymbolKindSchema.describe("class | field | method"),
373
397
  name: nonEmptyString,
374
398
  owner: optionalNonEmptyString,
375
- descriptor: optionalNonEmptyString,
399
+ descriptor: optionalDescriptorString.describe("JVM descriptor. Empty strings are treated as omitted."),
376
400
  sourceMapping: sourceMappingSchema.describe("obfuscated | mojang | intermediary | yarn"),
377
401
  sourcePriority: mappingSourcePrioritySchema.optional().describe("loom-first | maven-first"),
378
- maxCandidates: optionalPositiveInt.default(200).describe("Limit returned candidates for field/method lookups (max 200)"),
379
- compact: z.boolean().default(false).describe("When true, omit top-level empty arrays, null/undefined values, and empty objects from the response. "
380
- + "Also omit redundant candidates array for single full-confidence exact-match resolutions.")
402
+ maxCandidates: optionalPositiveInt.default(5).describe("Limit returned candidates for field/method lookups (default 5, max 200). Raise when you need the full candidate list."),
403
+ compact: z.boolean().default(true).describe("Omit top-level empty arrays, null/undefined values, and empty objects from the response. "
404
+ + "Also omit redundant candidates array for single full-confidence exact-match resolutions. "
405
+ + "Enabled by default; set to false for full output.")
381
406
  };
382
407
  const resolveWorkspaceSymbolSchema = z
383
408
  .object(resolveWorkspaceSymbolShape)
@@ -443,15 +468,16 @@ const checkSymbolExistsShape = {
443
468
  kind: workspaceSymbolKindSchema.describe("class | field | method"),
444
469
  owner: optionalNonEmptyString,
445
470
  name: nonEmptyString,
446
- descriptor: optionalNonEmptyString.describe("required for kind=method unless signatureMode=name-only"),
471
+ descriptor: optionalDescriptorString.describe("required for kind=method unless signatureMode=name-only. Empty strings are treated as omitted."),
447
472
  sourceMapping: sourceMappingSchema.describe("obfuscated | mojang | intermediary | yarn"),
448
473
  sourcePriority: mappingSourcePrioritySchema.optional().describe("loom-first | maven-first"),
449
474
  nameMode: classNameModeSchema.default("fqcn").describe("fqcn | auto"),
450
475
  signatureMode: z.enum(["exact", "name-only"]).default("exact")
451
476
  .describe("exact: require descriptor for methods; name-only: match by owner+name only"),
452
- maxCandidates: optionalPositiveInt.default(200).describe("Limit returned candidates (max 200)"),
453
- compact: z.boolean().default(false).describe("When true, omit top-level empty arrays, null/undefined values, and empty objects from the response. "
454
- + "Also omit redundant candidates array for single full-confidence exact-match resolutions.")
477
+ maxCandidates: optionalPositiveInt.default(5).describe("Limit returned candidates (default 5, max 200). Raise when you need the full candidate list."),
478
+ compact: z.boolean().default(true).describe("Omit top-level empty arrays, null/undefined values, and empty objects from the response. "
479
+ + "Also omit redundant candidates array for single full-confidence exact-match resolutions. "
480
+ + "Enabled by default; set to false for full output.")
455
481
  };
456
482
  const checkSymbolExistsSchema = z.object(checkSymbolExistsShape).superRefine((value, ctx) => {
457
483
  if (value.kind === "class") {
@@ -886,6 +912,16 @@ function statusForErrorCode(code) {
886
912
  }
887
913
  return 500;
888
914
  }
915
+ function extractFailedStageFromDetails(details) {
916
+ if (typeof details !== "object" || details == null) {
917
+ return undefined;
918
+ }
919
+ const maybeStage = details.failedStage;
920
+ if (typeof maybeStage === "string" && maybeStage.trim().length > 0) {
921
+ return maybeStage;
922
+ }
923
+ return undefined;
924
+ }
889
925
  function extractFieldErrorsFromDetails(details) {
890
926
  if (typeof details !== "object" || details == null) {
891
927
  return undefined;
@@ -1447,11 +1483,18 @@ function mapErrorToProblem(caughtError, requestId, context) {
1447
1483
  instance: requestId,
1448
1484
  fieldErrors: toFieldErrorsFromZod(caughtError),
1449
1485
  hints: guidance?.hints ?? ["Check fieldErrors and submit a valid tool argument payload."],
1450
- ...(guidance?.suggestedCall ? { suggestedCall: guidance.suggestedCall } : {})
1486
+ ...(guidance?.suggestedCall ? { suggestedCall: guidance.suggestedCall } : {}),
1487
+ ...(context?.tool === "validate-mixin" ? { failedStage: "input-validation" } : {})
1451
1488
  };
1452
1489
  }
1453
1490
  if (isAppError(caughtError)) {
1454
1491
  const suggestedCall = toSuggestedCall(caughtError.details);
1492
+ let failedStage = extractFailedStageFromDetails(caughtError.details);
1493
+ if (!failedStage
1494
+ && context?.tool === "validate-mixin"
1495
+ && caughtError.code === ERROR_CODES.INVALID_INPUT) {
1496
+ failedStage = "input-validation";
1497
+ }
1455
1498
  return {
1456
1499
  type: `https://minecraft-modding-mcp.dev/problems/${caughtError.code.toLowerCase()}`,
1457
1500
  title: "Tool execution error",
@@ -1461,7 +1504,8 @@ function mapErrorToProblem(caughtError, requestId, context) {
1461
1504
  instance: requestId,
1462
1505
  fieldErrors: extractFieldErrorsFromDetails(caughtError.details),
1463
1506
  hints: toHints(caughtError.details),
1464
- ...(suggestedCall ? { suggestedCall } : {})
1507
+ ...(suggestedCall ? { suggestedCall } : {}),
1508
+ ...(failedStage ? { failedStage } : {})
1465
1509
  };
1466
1510
  }
1467
1511
  return {
@@ -1540,7 +1584,16 @@ async function runTool(tool, rawInput, schema, action) {
1540
1584
  if (COMPACT_MAPPING_TOOL_NAMES.has(tool)) {
1541
1585
  projectedResult = compactMappingResponse(projectedResult);
1542
1586
  }
1543
- projectedResult = compactResponse(projectedResult);
1587
+ if (COMPACT_SOURCE_TOOL_NAMES.has(tool)) {
1588
+ projectedResult = compactSourceResponse(projectedResult);
1589
+ }
1590
+ if (COMPACT_MEMBERS_TOOL_NAMES.has(tool)) {
1591
+ projectedResult = compactMembersResponse(projectedResult);
1592
+ }
1593
+ if (COMPACT_LIGHT_TOOL_NAMES.has(tool)) {
1594
+ projectedResult = compactLightResponse(projectedResult);
1595
+ }
1596
+ projectedResult = compactResponse(projectedResult, TOOL_PRESERVE_PAYLOAD_KEYS[tool]);
1544
1597
  }
1545
1598
  const entryMeta = ENTRY_TOOL_NAMES.has(tool)
1546
1599
  ? buildEntryToolMeta({
@@ -1558,6 +1611,8 @@ async function runTool(tool, rawInput, schema, action) {
1558
1611
  : undefined
1559
1612
  })
1560
1613
  : undefined;
1614
+ const durationMs = Date.now() - startedAt;
1615
+ sourceService.recordToolCall(tool, durationMs);
1561
1616
  return objectResult({
1562
1617
  result: projectedResult,
1563
1618
  meta: {
@@ -1565,7 +1620,7 @@ async function runTool(tool, rawInput, schema, action) {
1565
1620
  ...resultMeta,
1566
1621
  requestId,
1567
1622
  tool,
1568
- durationMs: Date.now() - startedAt,
1623
+ durationMs,
1569
1624
  warnings
1570
1625
  }
1571
1626
  });
@@ -1605,12 +1660,14 @@ async function runTool(tool, rawInput, schema, action) {
1605
1660
  reason: caughtError instanceof Error ? caughtError.message : String(caughtError)
1606
1661
  });
1607
1662
  }
1663
+ const errorDurationMs = Date.now() - startedAt;
1664
+ sourceService.recordToolCall(tool, errorDurationMs);
1608
1665
  return objectResult({
1609
1666
  error: problem,
1610
1667
  meta: {
1611
1668
  requestId,
1612
1669
  tool,
1613
- durationMs: Date.now() - startedAt,
1670
+ durationMs: errorDurationMs,
1614
1671
  warnings: []
1615
1672
  }
1616
1673
  }, { isError: true });
@@ -1634,7 +1691,8 @@ server.tool("resolve-artifact", "Resolve source artifact from a target object ({
1634
1691
  projectPath: input.projectPath,
1635
1692
  scope: input.scope,
1636
1693
  preferProjectVersion: input.preferProjectVersion,
1637
- strictVersion: input.strictVersion
1694
+ strictVersion: input.strictVersion,
1695
+ compact: input.compact
1638
1696
  })));
1639
1697
  const findClassShape = {
1640
1698
  className: nonEmptyString.describe("Simple name (e.g. Blocks) or fully-qualified name (e.g. net.minecraft.world.level.block.Blocks)"),
@@ -1704,7 +1762,9 @@ server.tool("search-class-source", "Search indexed class source files for one ar
1704
1762
  scope: scope,
1705
1763
  queryMode: input.queryMode,
1706
1764
  limit: input.limit,
1707
- cursor: input.cursor
1765
+ cursor: input.cursor,
1766
+ queryNamespace: input.queryNamespace,
1767
+ sourcePriority: input.sourcePriority
1708
1768
  });
1709
1769
  }));
1710
1770
  server.tool("get-artifact-file", "Get full source file content by artifactId and file path.", getArtifactFileShape, { readOnlyHint: true }, async (args) => runTool("get-artifact-file", args, getArtifactFileSchema, async (input) => sourceService.getArtifactFile({
@@ -1712,7 +1772,12 @@ server.tool("get-artifact-file", "Get full source file content by artifactId and
1712
1772
  filePath: input.filePath,
1713
1773
  maxBytes: input.maxBytes
1714
1774
  })));
1715
- server.tool("list-artifact-files", "List source file paths in an artifact with optional prefix filter and cursor-based pagination.", listArtifactFilesShape, { readOnlyHint: true }, async (args) => runTool("list-artifact-files", args, listArtifactFilesSchema, async (input) => sourceService.listArtifactFiles(input)));
1775
+ server.tool("list-artifact-files", "List source file paths in an artifact with optional prefix filter and cursor-based pagination.", listArtifactFilesShape, { readOnlyHint: true }, async (args) => runTool("list-artifact-files", args, listArtifactFilesSchema, async (input) => sourceService.listArtifactFiles({
1776
+ artifactId: input.artifactId,
1777
+ prefix: input.prefix,
1778
+ limit: input.limit,
1779
+ cursor: input.cursor
1780
+ })));
1716
1781
  server.tool("trace-symbol-lifecycle", "Trace which Minecraft versions contain a specific class method and report first/last seen versions.", traceSymbolLifecycleShape, { readOnlyHint: true }, async (args) => runTool("trace-symbol-lifecycle", args, traceSymbolLifecycleSchema, async (input) => sourceService.traceSymbolLifecycle({
1717
1782
  symbol: input.symbol,
1718
1783
  descriptor: input.descriptor,
@@ -1741,6 +1806,7 @@ server.tool("find-mapping", "Find symbol mapping candidates between namespaces u
1741
1806
  sourceMapping: input.sourceMapping,
1742
1807
  targetMapping: input.targetMapping,
1743
1808
  sourcePriority: input.sourcePriority,
1809
+ signatureMode: input.signatureMode,
1744
1810
  disambiguation: input.disambiguation,
1745
1811
  maxCandidates: input.maxCandidates
1746
1812
  })));
@@ -0,0 +1,31 @@
1
+ /**
2
+ * O(1) doubly-linked list + Map LRU.
3
+ * head = oldest, tail = newest.
4
+ */
5
+ export declare class LruList<T> {
6
+ private map;
7
+ private head;
8
+ private tail;
9
+ get size(): number;
10
+ /** O(1) — move to tail (most recent), return value reference. */
11
+ touch(key: string): T | undefined;
12
+ /** O(1) — insert or update + move to tail. Returns previous value if existed. */
13
+ upsert(key: string, value: T): T | undefined;
14
+ /** O(1) — remove by key, return removed value. */
15
+ remove(key: string): T | undefined;
16
+ /** O(1) — peek at oldest (head) without removing. */
17
+ peekOldest(): {
18
+ key: string;
19
+ value: T;
20
+ } | undefined;
21
+ /** O(1) — clear all entries. */
22
+ clear(): void;
23
+ /** O(n) — iterate oldest → newest. */
24
+ toArray(): Array<{
25
+ key: string;
26
+ value: T;
27
+ }>;
28
+ private unlink;
29
+ private appendToTail;
30
+ private moveToTail;
31
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * O(1) doubly-linked list + Map LRU.
3
+ * head = oldest, tail = newest.
4
+ */
5
+ export class LruList {
6
+ map = new Map();
7
+ head = null;
8
+ tail = null;
9
+ get size() {
10
+ return this.map.size;
11
+ }
12
+ /** O(1) — move to tail (most recent), return value reference. */
13
+ touch(key) {
14
+ const node = this.map.get(key);
15
+ if (!node) {
16
+ return undefined;
17
+ }
18
+ this.moveToTail(node);
19
+ return node.value;
20
+ }
21
+ /** O(1) — insert or update + move to tail. Returns previous value if existed. */
22
+ upsert(key, value) {
23
+ const existing = this.map.get(key);
24
+ if (existing) {
25
+ const prev = existing.value;
26
+ existing.value = value;
27
+ this.moveToTail(existing);
28
+ return prev;
29
+ }
30
+ const node = { key, value, prev: null, next: null };
31
+ this.map.set(key, node);
32
+ this.appendToTail(node);
33
+ return undefined;
34
+ }
35
+ /** O(1) — remove by key, return removed value. */
36
+ remove(key) {
37
+ const node = this.map.get(key);
38
+ if (!node) {
39
+ return undefined;
40
+ }
41
+ this.unlink(node);
42
+ this.map.delete(key);
43
+ return node.value;
44
+ }
45
+ /** O(1) — peek at oldest (head) without removing. */
46
+ peekOldest() {
47
+ if (!this.head) {
48
+ return undefined;
49
+ }
50
+ return { key: this.head.key, value: this.head.value };
51
+ }
52
+ /** O(1) — clear all entries. */
53
+ clear() {
54
+ this.map.clear();
55
+ this.head = null;
56
+ this.tail = null;
57
+ }
58
+ /** O(n) — iterate oldest → newest. */
59
+ toArray() {
60
+ const result = [];
61
+ let current = this.head;
62
+ while (current) {
63
+ result.push({ key: current.key, value: current.value });
64
+ current = current.next;
65
+ }
66
+ return result;
67
+ }
68
+ unlink(node) {
69
+ if (node.prev) {
70
+ node.prev.next = node.next;
71
+ }
72
+ else {
73
+ this.head = node.next;
74
+ }
75
+ if (node.next) {
76
+ node.next.prev = node.prev;
77
+ }
78
+ else {
79
+ this.tail = node.prev;
80
+ }
81
+ node.prev = null;
82
+ node.next = null;
83
+ }
84
+ appendToTail(node) {
85
+ if (!this.tail) {
86
+ this.head = node;
87
+ this.tail = node;
88
+ return;
89
+ }
90
+ node.prev = this.tail;
91
+ this.tail.next = node;
92
+ this.tail = node;
93
+ }
94
+ moveToTail(node) {
95
+ if (node === this.tail) {
96
+ return;
97
+ }
98
+ this.unlink(node);
99
+ this.appendToTail(node);
100
+ }
101
+ }
102
+ //# sourceMappingURL=lru-list.js.map
@@ -4,6 +4,14 @@ export interface MappingPipelineInput {
4
4
  target: SourceTargetInput;
5
5
  resolved: ResolvedSourceArtifact;
6
6
  runtimeNamesUnobfuscated?: boolean;
7
+ /**
8
+ * When true and the mojang request lands on a binary-only artifact, the pipeline
9
+ * authorizes a downstream tiny-remap (obfuscated -> mojang) followed by decompile
10
+ * instead of throwing MAPPING_NOT_APPLIED. The caller is responsible for
11
+ * pre-resolving tiny-remapper jar, mojang tiny mappings, and verifying mapping
12
+ * health before setting this flag.
13
+ */
14
+ allowBinaryRemap?: boolean;
7
15
  }
8
16
  export interface MappingPipelineResult {
9
17
  mappingApplied: SourceMapping;
@@ -15,6 +23,7 @@ export interface MappingPipelineResult {
15
23
  * Current implementation enforces explicit guarantees:
16
24
  * - obfuscated: always pass-through
17
25
  * - mojang: requires source-backed artifacts on legacy obfuscated versions,
18
- * but unobfuscated runtime jars can pass through directly
26
+ * but unobfuscated runtime jars can pass through directly,
27
+ * or binary-only artifacts may be remapped + decompiled when allowBinaryRemap=true
19
28
  */
20
29
  export declare function applyMappingPipeline(input: MappingPipelineInput): MappingPipelineResult;