@adhisang/minecraft-modding-mcp 3.1.0 → 3.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.
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { createError, ERROR_CODES } from "../errors.js";
2
+ import { createError, ERROR_CODES, isAppError } from "../errors.js";
3
3
  import { buildIncludeSchema, detailSchema, positiveIntSchema } from "./entry-tool-schema.js";
4
4
  import { buildEntryToolResult, buildEntryToolMeta, createNextAction, createSummarySubject, createTruncationMeta } from "./response-contract.js";
5
5
  import { capArray, nextActionsOrUndefined, resolveDetail, resolveInclude } from "./request-normalizers.js";
@@ -124,6 +124,32 @@ export const inspectMinecraftSchema = z.object(inspectMinecraftShape).superRefin
124
124
  });
125
125
  }
126
126
  });
127
+ function hasPartialVanillaCoverage(artifact) {
128
+ return artifact?.qualityFlags.includes("partial-source-no-net-minecraft") === true
129
+ || artifact?.artifactContents.sourceCoverage === "partial";
130
+ }
131
+ function looksLikeClassQuery(query) {
132
+ const trimmed = query.trim();
133
+ if (!/^[A-Za-z_$][A-Za-z0-9_$.]*$/.test(trimmed)) {
134
+ return false;
135
+ }
136
+ const simpleName = trimmed.split(".").at(-1) ?? trimmed;
137
+ return /^[A-Z_$]/.test(simpleName) || /^class_\d+(?:\$class_\d+)*$/.test(simpleName);
138
+ }
139
+ function classNameToFilePath(className) {
140
+ const topLevelClassName = className.split("$")[0] ?? className;
141
+ return `${topLevelClassName.replace(/\./g, "/")}.java`;
142
+ }
143
+ function isVanillaNamespacePath(filePath) {
144
+ return filePath.startsWith("net/minecraft/") || filePath.startsWith("com/mojang/");
145
+ }
146
+ function hitTargetsVanillaNamespace(hit) {
147
+ if (isVanillaNamespacePath(hit.filePath)) {
148
+ return true;
149
+ }
150
+ const qualifiedName = hit.symbol?.qualifiedName;
151
+ return qualifiedName?.startsWith("net.minecraft.") === true || qualifiedName?.startsWith("com.mojang.") === true;
152
+ }
127
153
  export class InspectMinecraftService {
128
154
  deps;
129
155
  constructor(deps) {
@@ -200,11 +226,11 @@ export class InspectMinecraftService {
200
226
  strictVersion: subject.strictVersion
201
227
  };
202
228
  }
203
- async resolveClassArtifactReference(subject, classSubject) {
229
+ async resolveClassArtifactReference(subject, classSubject, task) {
204
230
  if (subject.kind === "workspace") {
205
231
  return this.resolveWorkspaceArtifactReference(subject, classSubject.artifact);
206
232
  }
207
- return this.resolveArtifactReference(classSubject);
233
+ return this.resolveArtifactReference(classSubject, task);
208
234
  }
209
235
  async resolveWorkspaceArtifactReference(subject, artifactRef) {
210
236
  if (!artifactRef) {
@@ -280,15 +306,130 @@ export class InspectMinecraftService {
280
306
  }
281
307
  return subject;
282
308
  }
283
- async resolveArtifactReference(subject) {
309
+ async exampleVersionForSubject(subject) {
310
+ if ("projectPath" in subject && typeof subject.projectPath === "string") {
311
+ const detectedVersion = await this.deps.detectProjectMinecraftVersion(subject.projectPath);
312
+ if (detectedVersion) {
313
+ return detectedVersion;
314
+ }
315
+ }
316
+ return "<version>";
317
+ }
318
+ async buildArtifactContextSuggestedCall(task, subject) {
319
+ return {
320
+ tool: "inspect-minecraft",
321
+ params: {
322
+ task,
323
+ subject: {
324
+ ...subject,
325
+ artifact: {
326
+ type: "resolve-target",
327
+ target: {
328
+ kind: "version",
329
+ value: await this.exampleVersionForSubject(subject)
330
+ }
331
+ }
332
+ }
333
+ }
334
+ };
335
+ }
336
+ taskForSubject(subject) {
337
+ return this.resolveTask(undefined, subject);
338
+ }
339
+ invalidTaskSubjectError(task, subject) {
340
+ if (task === "class-source" && subject.kind === "version") {
341
+ throw createError({
342
+ code: ERROR_CODES.INVALID_INPUT,
343
+ message: "class-source requires a class subject; version subjects resolve artifacts, not class names.",
344
+ details: {
345
+ nextAction: "Retry class-source with subject.kind=class and attach artifact context, or use task=artifact to inspect the version first.",
346
+ suggestedCall: {
347
+ tool: "inspect-minecraft",
348
+ params: {
349
+ task: "class-source",
350
+ subject: {
351
+ kind: "class",
352
+ className: "net.minecraft.world.item.Item",
353
+ artifact: {
354
+ type: "resolve-target",
355
+ target: {
356
+ kind: "version",
357
+ value: subject.version
358
+ }
359
+ }
360
+ }
361
+ }
362
+ }
363
+ }
364
+ });
365
+ }
366
+ const suggestedTask = this.taskForSubject(subject);
367
+ throw createError({
368
+ code: ERROR_CODES.INVALID_INPUT,
369
+ message: `${task} is not compatible with subject.kind="${subject.kind}".`,
370
+ details: {
371
+ nextAction: suggestedTask === "artifact"
372
+ ? `Retry with task=artifact for this ${subject.kind} subject, or reshape the subject so it supplies the input that ${task} needs.`
373
+ : `Retry with task=${suggestedTask} for this subject, or reshape the subject so it supplies the input that ${task} needs.`,
374
+ suggestedCall: {
375
+ tool: "inspect-minecraft",
376
+ params: {
377
+ task: suggestedTask,
378
+ subject
379
+ }
380
+ }
381
+ }
382
+ });
383
+ }
384
+ async resolveBinaryBackedClass(className, input) {
385
+ if (!this.deps.checkSymbolExists || !input.version) {
386
+ return undefined;
387
+ }
388
+ let lookup;
389
+ try {
390
+ lookup = await this.deps.checkSymbolExists({
391
+ version: input.version,
392
+ kind: "class",
393
+ name: className,
394
+ sourceMapping: input.mapping ?? "obfuscated",
395
+ nameMode: className.includes(".") ? "fqcn" : "auto",
396
+ maxCandidates: 10
397
+ });
398
+ }
399
+ catch (caughtError) {
400
+ if (isAppError(caughtError)) {
401
+ return undefined;
402
+ }
403
+ throw caughtError;
404
+ }
405
+ const resolvedClassName = lookup.resolvedSymbol?.name;
406
+ if (!resolvedClassName) {
407
+ return undefined;
408
+ }
409
+ return {
410
+ className: resolvedClassName,
411
+ warnings: lookup.warnings
412
+ };
413
+ }
414
+ async resolveArtifactReference(subject, task) {
284
415
  if (subject.kind === "artifact") {
285
416
  return this.resolveArtifactRef(subject.artifact, subject);
286
417
  }
287
418
  if (subject.kind === "class" || subject.kind === "file" || subject.kind === "search") {
288
419
  if (!subject.artifact) {
420
+ const suggestedTask = task
421
+ ?? (subject.kind === "class"
422
+ ? "class-overview"
423
+ : subject.kind === "search"
424
+ ? "search"
425
+ : "file");
289
426
  throw createError({
290
427
  code: ERROR_CODES.INVALID_INPUT,
291
- message: `${subject.kind} subject requires artifact context.`
428
+ message: `${subject.kind} subject requires artifact context.`,
429
+ details: {
430
+ nextAction: "Add subject.artifact or use subject.kind=workspace so inspect-minecraft can resolve the artifact first.",
431
+ suggestedCall: await this.buildArtifactContextSuggestedCall(suggestedTask, subject)
432
+ }
292
433
  });
293
434
  }
294
435
  return this.resolveArtifactRef(subject.artifact, subject);
@@ -479,14 +620,11 @@ export class InspectMinecraftService {
479
620
  }
480
621
  async handleClassOverview(subject, detail, include) {
481
622
  if (subject.kind !== "class" && !(subject.kind === "workspace" && subject.focus?.kind === "class")) {
482
- throw createError({
483
- code: ERROR_CODES.INVALID_INPUT,
484
- message: "class-overview requires a class or workspace focus subject."
485
- });
623
+ this.invalidTaskSubjectError("class-overview", subject);
486
624
  }
487
625
  const classSubject = this.buildClassSubject(subject);
488
626
  const className = classSubject.className;
489
- const artifact = await this.resolveClassArtifactReference(subject, classSubject);
627
+ const artifact = await this.resolveClassArtifactReference(subject, classSubject, "class-overview");
490
628
  if (!artifact.artifactId) {
491
629
  const summary = {
492
630
  status: "blocked",
@@ -519,6 +657,69 @@ export class InspectMinecraftService {
519
657
  limit: 10
520
658
  });
521
659
  if (matches.total === 0) {
660
+ const partialSourceFallback = subject.kind === "workspace" && hasPartialVanillaCoverage(artifact.artifact)
661
+ ? await this.resolveBinaryBackedClass(className, {
662
+ version: artifact.version,
663
+ mapping: classSubject.mapping
664
+ })
665
+ : undefined;
666
+ if (partialSourceFallback) {
667
+ const metadata = await this.deps.getClassSource({
668
+ className: partialSourceFallback.className,
669
+ artifactId: artifact.artifactId,
670
+ mapping: classSubject.mapping,
671
+ scope: classSubject.scope,
672
+ projectPath: classSubject.projectPath,
673
+ preferProjectVersion: classSubject.preferProjectVersion,
674
+ strictVersion: classSubject.strictVersion,
675
+ mode: "metadata"
676
+ });
677
+ const summary = {
678
+ status: "ok",
679
+ headline: `Resolved class overview for ${partialSourceFallback.className}.`,
680
+ subject: createSummarySubject({
681
+ task: "class-overview",
682
+ requested: subject,
683
+ className: partialSourceFallback.className,
684
+ artifactId: metadata.artifactId
685
+ }),
686
+ counts: {
687
+ totalLines: metadata.totalLines
688
+ },
689
+ notes: [
690
+ "Source coverage was partial, so inspect-minecraft confirmed the vanilla class through binary-backed symbol lookup."
691
+ ]
692
+ };
693
+ return {
694
+ ...buildEntryToolResult({
695
+ task: "class-overview",
696
+ summary,
697
+ detail,
698
+ include,
699
+ blocks: {
700
+ subject: {
701
+ requested: subject,
702
+ resolved: {
703
+ artifactId: metadata.artifactId,
704
+ className: partialSourceFallback.className
705
+ }
706
+ },
707
+ class: {
708
+ className: partialSourceFallback.className,
709
+ totalLines: metadata.totalLines,
710
+ returnedNamespace: metadata.returnedNamespace
711
+ }
712
+ },
713
+ alwaysBlocks: ["subject"]
714
+ }),
715
+ warnings: [
716
+ ...artifact.warnings,
717
+ ...matches.warnings,
718
+ ...partialSourceFallback.warnings,
719
+ ...metadata.warnings
720
+ ]
721
+ };
722
+ }
522
723
  const summary = {
523
724
  status: "not_found",
524
725
  headline: `No class match was found for ${className}.`,
@@ -663,14 +864,11 @@ export class InspectMinecraftService {
663
864
  }
664
865
  async handleClassSource(subject, detail, include) {
665
866
  if (subject.kind !== "class" && !(subject.kind === "workspace" && subject.focus?.kind === "class")) {
666
- throw createError({
667
- code: ERROR_CODES.INVALID_INPUT,
668
- message: "class-source requires a class or workspace focus subject."
669
- });
867
+ this.invalidTaskSubjectError("class-source", subject);
670
868
  }
671
869
  const classSubject = this.buildClassSubject(subject);
672
870
  const className = classSubject.className;
673
- const artifactContext = await this.resolveClassArtifactReference(subject, classSubject);
871
+ const artifactContext = await this.resolveClassArtifactReference(subject, classSubject, "class-source");
674
872
  const source = await this.deps.getClassSource({
675
873
  className,
676
874
  artifactId: artifactContext.artifactId || undefined,
@@ -723,13 +921,10 @@ export class InspectMinecraftService {
723
921
  }
724
922
  async handleClassMembers(subject, detail, include, limit) {
725
923
  if (subject.kind !== "class" && !(subject.kind === "workspace" && subject.focus?.kind === "class")) {
726
- throw createError({
727
- code: ERROR_CODES.INVALID_INPUT,
728
- message: "class-members requires a class or workspace focus subject."
729
- });
924
+ this.invalidTaskSubjectError("class-members", subject);
730
925
  }
731
926
  const classSubject = this.buildClassSubject(subject);
732
- const artifact = await this.resolveClassArtifactReference(subject, classSubject);
927
+ const artifact = await this.resolveClassArtifactReference(subject, classSubject, "class-members");
733
928
  const members = await this.deps.getClassMembers({
734
929
  className: classSubject.className,
735
930
  artifactId: artifact.artifactId || undefined,
@@ -798,16 +993,13 @@ export class InspectMinecraftService {
798
993
  }
799
994
  async handleSearch(subject, detail, include, limit, cursor) {
800
995
  if (subject.kind !== "search" && !(subject.kind === "workspace" && subject.focus?.kind === "search")) {
801
- throw createError({
802
- code: ERROR_CODES.INVALID_INPUT,
803
- message: "search requires a search or workspace focus subject."
804
- });
996
+ this.invalidTaskSubjectError("search", subject);
805
997
  }
806
998
  const searchSubject = subject.kind === "search" ? subject : this.requireWorkspaceSearchFocus(subject);
807
999
  const requestedSubject = this.summarizeRequestedSubject(subject);
808
1000
  const queryMode = searchSubject.queryMode ?? "auto";
809
1001
  const artifact = subject.kind === "search"
810
- ? await this.resolveArtifactReference(subject)
1002
+ ? await this.resolveArtifactReference(subject, "search")
811
1003
  : await this.resolveWorkspaceArtifactReference(subject, searchSubject.artifact);
812
1004
  const search = await this.deps.searchClassSource({
813
1005
  artifactId: artifact.artifactId,
@@ -825,8 +1017,38 @@ export class InspectMinecraftService {
825
1017
  }
826
1018
  : undefined
827
1019
  });
828
- const sampledHits = capArray(search.hits, 5);
829
- const isAutoSeparatorMiss = search.hits.length === 0 &&
1020
+ const needsBinaryBackedClassHit = subject.kind === "workspace" &&
1021
+ hasPartialVanillaCoverage(artifact.artifact) &&
1022
+ looksLikeClassQuery(searchSubject.query) &&
1023
+ !search.hits.some((hit) => hitTargetsVanillaNamespace(hit));
1024
+ const binaryBackedClassHit = needsBinaryBackedClassHit
1025
+ ? await this.resolveBinaryBackedClass(searchSubject.query, {
1026
+ version: artifact.version,
1027
+ mapping: subject.mapping
1028
+ })
1029
+ : undefined;
1030
+ const binaryBackedHitRecord = binaryBackedClassHit == null
1031
+ ? undefined
1032
+ : {
1033
+ filePath: classNameToFilePath(binaryBackedClassHit.className),
1034
+ score: 100,
1035
+ matchedIn: "symbol",
1036
+ reasonCodes: ["binary-class-lookup"],
1037
+ symbol: {
1038
+ symbolKind: "class",
1039
+ symbolName: binaryBackedClassHit.className.split(".").at(-1) ?? binaryBackedClassHit.className,
1040
+ qualifiedName: binaryBackedClassHit.className,
1041
+ line: 1
1042
+ }
1043
+ };
1044
+ const effectiveHits = binaryBackedHitRecord == null
1045
+ ? search.hits
1046
+ : [
1047
+ binaryBackedHitRecord,
1048
+ ...search.hits.filter((hit) => hit.filePath !== binaryBackedHitRecord.filePath)
1049
+ ];
1050
+ const sampledHits = capArray(effectiveHits, 5);
1051
+ const isAutoSeparatorMiss = effectiveHits.length === 0 &&
830
1052
  queryMode === "auto" &&
831
1053
  /[._$]/.test(searchSubject.query);
832
1054
  const literalRetrySubject = subject.kind === "search"
@@ -843,9 +1065,9 @@ export class InspectMinecraftService {
843
1065
  }
844
1066
  };
845
1067
  const summary = {
846
- status: search.hits.length > 0 ? "ok" : "not_found",
847
- headline: search.hits.length > 0
848
- ? `Found ${search.hits.length} source hits for ${searchSubject.query}.`
1068
+ status: effectiveHits.length > 0 ? "ok" : "not_found",
1069
+ headline: effectiveHits.length > 0
1070
+ ? `Found ${effectiveHits.length} source hits for ${searchSubject.query}.`
849
1071
  : `No source hits were found for ${searchSubject.query}.`,
850
1072
  subject: createSummarySubject({
851
1073
  task: "search",
@@ -854,16 +1076,16 @@ export class InspectMinecraftService {
854
1076
  artifactId: artifact.artifactId
855
1077
  }),
856
1078
  counts: {
857
- hits: search.hits.length
1079
+ hits: effectiveHits.length
858
1080
  },
859
1081
  nextActions: nextActionsOrUndefined([
860
- ...(search.hits.length > 0
1082
+ ...(effectiveHits.length > 0
861
1083
  ? [
862
1084
  createNextAction("inspect-minecraft", {
863
1085
  task: "file",
864
1086
  subject: {
865
1087
  kind: "file",
866
- filePath: search.hits[0].filePath,
1088
+ filePath: effectiveHits[0].filePath,
867
1089
  artifact: {
868
1090
  type: "resolved-id",
869
1091
  artifactId: artifact.artifactId
@@ -888,7 +1110,13 @@ export class InspectMinecraftService {
888
1110
  "Separator query returned no indexed hits. Retry with queryMode=\"literal\" for an explicit full substring scan."
889
1111
  ]
890
1112
  }
891
- : {})
1113
+ : binaryBackedClassHit
1114
+ ? {
1115
+ notes: [
1116
+ "Source coverage was partial, so inspect-minecraft returned a binary-backed class match for the vanilla symbol."
1117
+ ]
1118
+ }
1119
+ : {})
892
1120
  };
893
1121
  return {
894
1122
  ...buildEntryToolResult({
@@ -905,25 +1133,22 @@ export class InspectMinecraftService {
905
1133
  },
906
1134
  search: {
907
1135
  query: searchSubject.query,
908
- hits: detail === "summary" ? sampledHits.items : search.hits,
1136
+ hits: detail === "summary" ? sampledHits.items : effectiveHits,
909
1137
  nextCursor: search.nextCursor
910
1138
  }
911
1139
  },
912
1140
  alwaysBlocks: ["subject"]
913
1141
  }),
914
- warnings: [...artifact.warnings]
1142
+ warnings: [...artifact.warnings, ...(binaryBackedClassHit?.warnings ?? [])]
915
1143
  };
916
1144
  }
917
1145
  async handleFile(subject, detail, include) {
918
1146
  if (subject.kind !== "file" && !(subject.kind === "workspace" && subject.focus?.kind === "file")) {
919
- throw createError({
920
- code: ERROR_CODES.INVALID_INPUT,
921
- message: "file task requires a file or workspace focus subject."
922
- });
1147
+ this.invalidTaskSubjectError("file", subject);
923
1148
  }
924
1149
  const fileSubject = subject.kind === "file" ? subject : this.requireWorkspaceFileFocus(subject);
925
1150
  const artifact = subject.kind === "file"
926
- ? await this.resolveArtifactReference(subject)
1151
+ ? await this.resolveArtifactReference(subject, "file")
927
1152
  : await this.resolveWorkspaceArtifactReference(subject, fileSubject.artifact);
928
1153
  const file = await this.deps.getArtifactFile({
929
1154
  artifactId: artifact.artifactId,
@@ -969,25 +1194,16 @@ export class InspectMinecraftService {
969
1194
  };
970
1195
  }
971
1196
  async handleListFiles(subject, detail, include, limit, cursor) {
972
- const artifact = await this.resolveArtifactReference(subject);
1197
+ const artifact = await this.resolveArtifactReference(subject, "list-files");
973
1198
  const files = await this.deps.listArtifactFiles({
974
1199
  artifactId: artifact.artifactId,
975
1200
  limit,
976
1201
  cursor
977
1202
  });
978
1203
  const sampled = capArray(files.items, 10);
979
- const summary = {
980
- status: "ok",
981
- headline: `Listed ${files.items.length} files for ${artifact.artifactId}.`,
982
- subject: createSummarySubject({
983
- task: "list-files",
984
- requested: subject,
985
- artifactId: artifact.artifactId
986
- }),
987
- counts: {
988
- files: files.items.length
989
- },
990
- nextActions: nextActionsOrUndefined(files.items.length > 0
1204
+ const partialCoverage = subject.kind === "workspace" && hasPartialVanillaCoverage(artifact.artifact);
1205
+ const nextActions = [
1206
+ ...(files.items.length > 0
991
1207
  ? [
992
1208
  createNextAction("inspect-minecraft", {
993
1209
  task: "file",
@@ -1001,7 +1217,46 @@ export class InspectMinecraftService {
1001
1217
  }
1002
1218
  })
1003
1219
  ]
1220
+ : []),
1221
+ ...(partialCoverage
1222
+ ? [
1223
+ createNextAction("inspect-minecraft", {
1224
+ task: "class-source",
1225
+ subject: {
1226
+ kind: "workspace",
1227
+ projectPath: subject.projectPath,
1228
+ mapping: subject.mapping,
1229
+ scope: subject.scope,
1230
+ preferProjectVersion: subject.preferProjectVersion,
1231
+ strictVersion: subject.strictVersion,
1232
+ focus: {
1233
+ kind: "class",
1234
+ className: "net.minecraft.world.item.Item"
1235
+ }
1236
+ }
1237
+ })
1238
+ ]
1004
1239
  : [])
1240
+ ];
1241
+ const summary = {
1242
+ status: partialCoverage ? "partial" : "ok",
1243
+ headline: `Listed ${files.items.length} files for ${artifact.artifactId}.`,
1244
+ subject: createSummarySubject({
1245
+ task: "list-files",
1246
+ requested: subject,
1247
+ artifactId: artifact.artifactId
1248
+ }),
1249
+ counts: {
1250
+ files: files.items.length
1251
+ },
1252
+ nextActions: nextActionsOrUndefined(nextActions),
1253
+ ...(partialCoverage
1254
+ ? {
1255
+ notes: [
1256
+ "This listing is partial because the resolved source artifact does not contain net.minecraft entries."
1257
+ ]
1258
+ }
1259
+ : {})
1005
1260
  };
1006
1261
  return {
1007
1262
  ...buildEntryToolResult({
@@ -1018,7 +1273,15 @@ export class InspectMinecraftService {
1018
1273
  },
1019
1274
  files: {
1020
1275
  items: detail === "summary" ? sampled.items : files.items,
1021
- nextCursor: files.nextCursor
1276
+ nextCursor: files.nextCursor,
1277
+ ...(partialCoverage
1278
+ ? {
1279
+ coverage: {
1280
+ sourceCoverage: "partial",
1281
+ missingNamespaces: ["net.minecraft"]
1282
+ }
1283
+ }
1284
+ : {})
1022
1285
  }
1023
1286
  },
1024
1287
  alwaysBlocks: ["subject"]