@adhisang/minecraft-modding-mcp 6.0.0 → 6.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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,25 @@ and this project aims to follow [Semantic Versioning](https://semver.org/spec/v2
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [6.1.0] - 2026-06-28
11
+
12
+ ### Added
13
+
14
+ - `get-class-members` / `batch-class-members` accept a `projection` parameter — `"names"` (member names only), `"signatures"` (name + `javaSignature`, no `jvmDescriptor`), or `"full"` (default; the complete member shape). Use the leaner projections to cut response tokens for "does this member exist?" / signature-only checks. Default `"full"` output is unchanged.
15
+
16
+ ### Changed
17
+
18
+ - `get-class-source` responses for a decompiled artifact now carry a `decompiled-source-signatures-unverified` quality flag and a warning that decompiled method/accessor names may differ from the jar the workspace actually compiles against (e.g. a decompiled `getGameRenderState()` vs the runtime `gameRenderState()`); confirm signatures with `get-class-members` (bytecode-derived) before copying names from the source.
19
+ - Inspecting a Fabric / loader dependency class like vanilla is documented and surfaced: the `target` descriptions, `dependency` target fields, the `get-class-source` / `get-class-members` target-shape error guidance, and `docs/tool-reference.md` now show passing `target: { kind: "dependency", group, name, versionFromProject }` straight to the class tools (no separate lookup tool needed).
20
+
21
+ ### Fixed
22
+
23
+ - `memberPattern` (`get-class-members` / `batch-class-members`) treats `"|"` as OR alternation, so a natural pattern like `"getStateForPlacement|canSurvive|setPlacedBy"` matches any of those members. Previously the pattern was matched as a single literal substring, so a piped pattern searched for a name containing a literal `"|"` and returned zero members on large vanilla classes such as `Block` and `EntityRenderer`. Single-token patterns keep their case-insensitive substring behavior. The parameter description now documents the substring + OR semantics.
24
+
25
+ ### Performance
26
+
27
+ - `validate-project` (`project-summary`) validates discovered mixin configs with bounded concurrency (default 4) instead of one at a time; aggregate counts and warning order are identical to the previous sequential behavior.
28
+
10
29
  ## [6.0.0] - 2026-06-25
11
30
 
12
31
  ### Changed
package/dist/config.js CHANGED
@@ -211,7 +211,7 @@ function aliasJarBase(jarPath) {
211
211
  // to earlier callers. Two distinct artifactIds whose readable tokens collide and
212
212
  // whose first 48 hash bits also collide would trip the schema-v4 alias UNIQUE
213
213
  // constraint at upsert time — a hard error, not silent drift.
214
- // Phase 3.1a: display-only. Phase 3.1b stores it for lookup.
214
+ // Currently display-only; a later change stores it for lookup.
215
215
  export function buildArtifactAlias(input) {
216
216
  const tokens = [];
217
217
  if (input.kind === "version") {
@@ -1,6 +1,7 @@
1
1
  import { type ResponseDetailLevel } from "../response-utils.js";
2
2
  import type { GetClassMembersInput, GetClassMembersOutput, ResolveArtifactInput, ResolveArtifactOutput } from "../source-service.js";
3
3
  import type { ArtifactScope, MappingSourcePriority, ResolveArtifactTargetInput, SourceMapping } from "../types.js";
4
+ import type { MemberProjection } from "../source/class-source/members-builder.js";
4
5
  import { type BatchOutput } from "./batch-runner.js";
5
6
  export type BatchClassMembersDeps = {
6
7
  resolveArtifact: (input: ResolveArtifactInput) => Promise<ResolveArtifactOutput>;
@@ -27,6 +28,7 @@ export type BatchClassMembersInput = {
27
28
  concurrency?: number;
28
29
  failFast?: boolean;
29
30
  detail?: ResponseDetailLevel;
31
+ projection?: MemberProjection;
30
32
  include?: readonly string[];
31
33
  entries: readonly BatchClassMembersEntry[];
32
34
  };
@@ -54,6 +54,7 @@ export class BatchClassMembersService {
54
54
  includeInherited: entry.includeInherited,
55
55
  memberPattern: entry.memberPattern,
56
56
  maxMembers: entry.maxMembers,
57
+ projection: input.projection,
57
58
  includeDescriptors: include.has("descriptors"),
58
59
  mapping: input.mapping,
59
60
  sourcePriority: input.sourcePriority,
@@ -1,4 +1,5 @@
1
1
  import { readFile } from "node:fs/promises";
2
+ import { mapWithConcurrencyLimit } from "../../../concurrency.js";
2
3
  import { buildEntryToolResult, createSummarySubject } from "../../response-contract.js";
3
4
  import { ERROR_CODES, createError } from "../../../errors.js";
4
5
  import { buildEarlyTasksForBlocked, buildFullTaskStatusReport } from "../internal.js";
@@ -199,7 +200,14 @@ export async function handleProjectSummary(deps, input, detail, include, options
199
200
  await safeEmit(options.stageEmitter, "validate-project:mixin-validation", {
200
201
  targetTotal: mixinConfigs.length
201
202
  });
202
- for (const [mixinIndex, configPath] of mixinConfigs.entries()) {
203
+ // Validate mixin configs with bounded concurrency (default 4, matching the
204
+ // batch tools). Each unit returns its own counts/warnings; aggregation happens
205
+ // afterward in input order so totals and warning order are identical to a
206
+ // sequential run. Concurrent validateMixin against one SourceService is already
207
+ // exercised by the batch-class-* tools (shared artifact resolution, idempotent
208
+ // caches, synchronous SQLite), so this adds no new shared-state hazard.
209
+ const MIXIN_VALIDATION_CONCURRENCY = 4;
210
+ const mixinResults = await mapWithConcurrencyLimit(mixinConfigs, MIXIN_VALIDATION_CONCURRENCY, async (configPath, mixinIndex) => {
203
211
  try {
204
212
  await safeEmit(options.stageEmitter, "validate-project:mixin-validation", {
205
213
  targetIndex: mixinIndex + 1,
@@ -232,20 +240,30 @@ export async function handleProjectSummary(deps, input, detail, include, options
232
240
  stageEmitter: wrappedEmitter
233
241
  });
234
242
  const summary = mixinResult.summary;
235
- validMixins += summary?.valid ?? 0;
236
- partialMixins += summary?.partial ?? 0;
237
- invalidMixins += summary?.invalid ?? 0;
238
- if (Array.isArray(mixinResult.warnings)) {
239
- warnings.push(...mixinResult.warnings);
240
- }
243
+ return {
244
+ valid: summary?.valid ?? 0,
245
+ partial: summary?.partial ?? 0,
246
+ invalid: summary?.invalid ?? 0,
247
+ caught: 0,
248
+ warnings: Array.isArray(mixinResult.warnings) ? [...mixinResult.warnings] : []
249
+ };
241
250
  }
242
251
  catch (error) {
243
- invalidMixins += 1;
244
- mixinCaughtErrors += 1;
245
- if (error instanceof Error) {
246
- warnings.push(`${configPath}: ${error.message}`);
247
- }
252
+ return {
253
+ valid: 0,
254
+ partial: 0,
255
+ invalid: 1,
256
+ caught: 1,
257
+ warnings: error instanceof Error ? [`${configPath}: ${error.message}`] : []
258
+ };
248
259
  }
260
+ });
261
+ for (const result of mixinResults) {
262
+ validMixins += result.valid;
263
+ partialMixins += result.partial;
264
+ invalidMixins += result.invalid;
265
+ mixinCaughtErrors += result.caught;
266
+ warnings.push(...result.warnings);
249
267
  }
250
268
  const mixinDurationMs = Date.now() - mixinDurationStart;
251
269
  const awDurationStart = Date.now();
package/dist/index.js CHANGED
@@ -573,6 +573,7 @@ expertTool("get-class-members", "Get fields/methods/constructors for one class f
573
573
  includeInherited: input.includeInherited,
574
574
  memberPattern: input.memberPattern,
575
575
  maxMembers: input.maxMembers,
576
+ projection: input.projection,
576
577
  cursor: input.cursor,
577
578
  projectPath: input.projectPath,
578
579
  gradleUserHome: input.gradleUserHome,
@@ -581,7 +581,7 @@ export class MappingService {
581
581
  const nextCursor = rowsTruncated ? encodeOffsetCursor(consumedRows, rowCursorContext) : undefined;
582
582
  // Map ONLY the requested window. Per-row mapping cost is therefore O(window),
583
583
  // not O(rowCount). warnings/ambiguousRowCount are accordingly page-scoped on
584
- // paginated calls (caveat B1) and byte-identical to the old full-set values when
584
+ // paginated calls and byte-identical to the old full-set values when
585
585
  // no maxRows/cursor narrows the window.
586
586
  const rows = [];
587
587
  let ambiguousRowCount = 0;
@@ -2,6 +2,7 @@ import { createError, ERROR_CODES } from "./errors.js";
2
2
  import { loadConfig } from "./config.js";
3
3
  import { artifactSignatureFromFile, normalizeJarPath } from "./path-resolver.js";
4
4
  import { createJarEntryReader } from "./source-jar-reader.js";
5
+ import { matchesMemberPattern } from "./source/member-pattern.js";
5
6
  const CLASSFILE_MAGIC = 0xcafebabe;
6
7
  const MAX_INHERITANCE_DEPTH = 64;
7
8
  const ACC_PUBLIC = 0x0001;
@@ -603,7 +604,7 @@ export class MinecraftExplorerService {
603
604
  if (access === "public" && !hasPublicVisibility(member.accessFlags)) {
604
605
  return false;
605
606
  }
606
- if (memberPattern && !lower(member.name).includes(memberPattern)) {
607
+ if (memberPattern && !matchesMemberPattern(member.name, memberPattern)) {
607
608
  return false;
608
609
  }
609
610
  return true;
@@ -76,3 +76,29 @@ export declare function projectMembersForWire(slice: {
76
76
  fields: SignatureMember[];
77
77
  methods: SignatureMember[];
78
78
  }, includeInherited: boolean, keepFieldDescriptors?: boolean): WireMembersBlock;
79
+ /**
80
+ * Token-projection level for a member listing, narrowing the per-member fields
81
+ * an agent receives:
82
+ * - `full` (default): the complete {@link WireMember} shape (current behavior).
83
+ * - `signatures`: name + javaSignature (drops jvmDescriptor) — enough to read
84
+ * the API without overload descriptors.
85
+ * - `names`: member name only — enough for an "does this member exist?" check.
86
+ * The block-level hoisted `ownerFqn` and any per-member `ownerFqn` (the
87
+ * multi-owner / includeInherited case) are preserved in every level so members
88
+ * stay attributable.
89
+ */
90
+ export type MemberProjection = "names" | "signatures" | "full";
91
+ export type ProjectedMember = {
92
+ name: string;
93
+ javaSignature?: string;
94
+ jvmDescriptor?: string;
95
+ ownerFqn?: string;
96
+ isSynthetic?: boolean;
97
+ };
98
+ export type ProjectedMembersBlock = {
99
+ ownerFqn?: string;
100
+ constructors: ProjectedMember[];
101
+ fields: ProjectedMember[];
102
+ methods: ProjectedMember[];
103
+ };
104
+ export declare function projectMembersByLevel(block: WireMembersBlock, level: MemberProjection): ProjectedMembersBlock;
@@ -1,3 +1,4 @@
1
+ import { matchesMemberPattern } from "../member-pattern.js";
1
2
  export async function remapAndCountMembers(svc, input) {
2
3
  const remap = async (members, kind) => {
3
4
  if (input.version == null) {
@@ -10,10 +11,10 @@ export async function remapAndCountMembers(svc, input) {
10
11
  let fields = await remap(input.signatureFields, "field");
11
12
  let methods = await remap(input.signatureMethods, "method");
12
13
  if (input.requestedMapping !== input.mappingApplied && input.memberPattern) {
13
- const lowerPattern = input.memberPattern.toLowerCase();
14
- constructors = constructors.filter((m) => m.name.toLowerCase().includes(lowerPattern));
15
- fields = fields.filter((m) => m.name.toLowerCase().includes(lowerPattern));
16
- methods = methods.filter((m) => m.name.toLowerCase().includes(lowerPattern));
14
+ const pattern = input.memberPattern;
15
+ constructors = constructors.filter((m) => matchesMemberPattern(m.name, pattern));
16
+ fields = fields.filter((m) => matchesMemberPattern(m.name, pattern));
17
+ methods = methods.filter((m) => matchesMemberPattern(m.name, pattern));
17
18
  }
18
19
  const counts = {
19
20
  constructors: constructors.length,
@@ -90,4 +91,30 @@ export function projectMembersForWire(slice, includeInherited, keepFieldDescript
90
91
  methods: slice.methods.map((m) => toWire(m, true))
91
92
  };
92
93
  }
94
+ export function projectMembersByLevel(block, level) {
95
+ if (level === "full") {
96
+ return block;
97
+ }
98
+ const reduce = (m) => {
99
+ if (level === "names") {
100
+ return {
101
+ name: m.name,
102
+ ...(m.ownerFqn ? { ownerFqn: m.ownerFqn } : {})
103
+ };
104
+ }
105
+ // "signatures": keep the readable signature, drop the JVM descriptor.
106
+ return {
107
+ name: m.name,
108
+ javaSignature: m.javaSignature,
109
+ ...(m.ownerFqn ? { ownerFqn: m.ownerFqn } : {}),
110
+ ...(m.isSynthetic ? { isSynthetic: true } : {})
111
+ };
112
+ };
113
+ return {
114
+ ...(block.ownerFqn ? { ownerFqn: block.ownerFqn } : {}),
115
+ constructors: block.constructors.map(reduce),
116
+ fields: block.fields.map(reduce),
117
+ methods: block.methods.map(reduce)
118
+ };
119
+ }
93
120
  //# sourceMappingURL=members-builder.js.map
@@ -5,7 +5,8 @@ import { ERROR_CODES, createError, isAppError } from "../errors.js";
5
5
  import * as artifactResolver from "./artifact-resolver.js";
6
6
  import * as classSourceHelpers from "./class-source-helpers.js";
7
7
  import { buildClassSourceSnippet } from "./class-source/snippet-builder.js";
8
- import { remapAndCountMembers, sliceMembersWithLimit, projectMembersForWire } from "./class-source/members-builder.js";
8
+ import { remapAndCountMembers, sliceMembersWithLimit, projectMembersForWire, projectMembersByLevel } from "./class-source/members-builder.js";
9
+ import { matchesMemberPattern } from "./member-pattern.js";
9
10
  import { buildPageContextKey, encodeOffsetCursor, resolveCursorOffset } from "../page-cursor.js";
10
11
  import { dedupeQualityFlags, normalizeMapping, normalizeOptionalString, normalizePathStyle } from "./shared-utils.js";
11
12
  import { isUnobfuscatedVersion } from "../version-service.js";
@@ -239,8 +240,7 @@ export function buildDecompiledFallback(svc, artifactId, lookupClassName, member
239
240
  if (!memberPattern) {
240
241
  return list;
241
242
  }
242
- const lower = memberPattern.toLowerCase();
243
- return list.filter((entry) => entry.name.toLowerCase().includes(lower));
243
+ return list.filter((entry) => matchesMemberPattern(entry.name, memberPattern));
244
244
  };
245
245
  let constructors = filterByPattern(extracted.constructors);
246
246
  let fields = filterByPattern(extracted.fields);
@@ -280,6 +280,25 @@ export function buildDecompiledFallback(svc, artifactId, lookupClassName, member
280
280
  truncated: totalBefore > maxMembers
281
281
  };
282
282
  }
283
+ /**
284
+ * Apply a member projection to the decompiled fallback so it honors the same
285
+ * `projection` contract as the bytecode members block. The fallback only carries
286
+ * names + source line + kind (no signatures), so any non-"full" level reduces to
287
+ * the member name (the array it lives in already conveys the kind). Keeps the
288
+ * full shape for the default "full" level.
289
+ */
290
+ function projectDecompiledFallback(fallback, level) {
291
+ if (level === "full") {
292
+ return fallback;
293
+ }
294
+ const reduce = (member) => ({ name: member.name });
295
+ return {
296
+ constructors: fallback.constructors.map(reduce),
297
+ fields: fallback.fields.map(reduce),
298
+ methods: fallback.methods.map(reduce),
299
+ origin: fallback.origin
300
+ };
301
+ }
283
302
  // The class-like symbol kinds findClass returns. MUST stay in sync with the JS-side
284
303
  // isTypeSymbol checks below; pushed down to SQL so non-type rows are never fetched.
285
304
  const TYPE_SYMBOL_KINDS = ["class", "interface", "enum", "record"];
@@ -652,6 +671,20 @@ export async function getClassSource(svc, input) {
652
671
  }
653
672
  })
654
673
  : undefined;
674
+ // Decompiled source text can use different method/accessor names than the jar
675
+ // the workspace actually compiles against (e.g. `getGameRenderState()` in the
676
+ // decompiled source vs `gameRenderState()` in the runtime jar). Flag it so
677
+ // callers verify names against get-class-members (bytecode-derived) before
678
+ // copying signatures out of this source.
679
+ if (activeOrigin === "decompiled") {
680
+ activeQualityFlags = dedupeQualityFlags([
681
+ ...activeQualityFlags,
682
+ "decompiled-source-signatures-unverified"
683
+ ]);
684
+ warnings.push("Source is decompiled: method/accessor names may differ from the jar the workspace "
685
+ + "compiles against. Confirm signatures with get-class-members (bytecode-derived) "
686
+ + "before copying names from this source.");
687
+ }
655
688
  return {
656
689
  className,
657
690
  mode,
@@ -889,7 +922,8 @@ export async function getClassMembers(svc, input) {
889
922
  // Slim the wire member shape: hoist a shared ownerFqn, drop accessFlags, omit
890
923
  // isSynthetic:false, and drop FIELD jvmDescriptor unless includeDescriptors.
891
924
  // Internal SignatureMember arrays above stay intact.
892
- const projectedMembers = projectMembersForWire({ constructors, fields, methods }, includeInherited, input.includeDescriptors ?? false);
925
+ const projection = input.projection ?? "full";
926
+ const projectedMembers = projectMembersByLevel(projectMembersForWire({ constructors, fields, methods }, includeInherited, input.includeDescriptors ?? false), projection);
893
927
  const truncated = sliced.truncated;
894
928
  const nextCursor = sliced.nextOffset != null ? encodeOffsetCursor(sliced.nextOffset, memberCursorContext) : undefined;
895
929
  const normalizedProvenance = provenance ??
@@ -907,7 +941,7 @@ export async function getClassMembers(svc, input) {
907
941
  const fallbackPattern = namespaceMismatch ? undefined : memberPattern;
908
942
  const sourceFallback = buildDecompiledFallback(svc, artifactId, lookupClassName, fallbackPattern, maxMembers);
909
943
  if (sourceFallback) {
910
- decompiledFallback = sourceFallback.fallback;
944
+ decompiledFallback = projectDecompiledFallback(sourceFallback.fallback, projection);
911
945
  decompiledMemberCounts = sourceFallback.counts;
912
946
  fallbackQualityFlags = dedupeQualityFlags([
913
947
  ...qualityFlags,
@@ -213,7 +213,7 @@ export async function buildRebuiltArtifactData(svc, resolved) {
213
213
  // Decompilation failed BEFORE the artifact was ever upserted, so there is no
214
214
  // queryable artifact. Deliberately omit artifactId from the error: exposing the
215
215
  // would-be id led callers to pass it to find-class/get-class-*, which then failed
216
- // with "Artifact not found. Resolve context first." (B5 state inconsistency).
216
+ // with "Artifact not found. Resolve context first." a state inconsistency.
217
217
  const priorDetails = { ...(caughtError.details ?? {}) };
218
218
  delete priorDetails.artifactId;
219
219
  throw createError({
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Match a class-member name against a `memberPattern` filter.
3
+ *
4
+ * Matching is case-insensitive substring. A `|` splits the pattern into OR
5
+ * alternatives, so `"getStateForPlacement|canSurvive|setPlacedBy"` matches a
6
+ * name that contains ANY of those tokens. Before this, the pattern was matched
7
+ * as a single literal substring, so a piped pattern searched for a name
8
+ * containing a literal `|` and matched nothing — which made member listing
9
+ * return zero on large vanilla classes when callers used the natural OR syntax.
10
+ *
11
+ * Java member names never contain `|`, so treating `|` as OR never collides with
12
+ * a legitimate substring search. Empty or whitespace-only alternatives are
13
+ * dropped; a pattern with no usable alternative matches nothing (a non-empty
14
+ * filter that excludes everything), matching the prior empty-filter behavior.
15
+ */
16
+ export declare function matchesMemberPattern(name: string, pattern: string): boolean;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Match a class-member name against a `memberPattern` filter.
3
+ *
4
+ * Matching is case-insensitive substring. A `|` splits the pattern into OR
5
+ * alternatives, so `"getStateForPlacement|canSurvive|setPlacedBy"` matches a
6
+ * name that contains ANY of those tokens. Before this, the pattern was matched
7
+ * as a single literal substring, so a piped pattern searched for a name
8
+ * containing a literal `|` and matched nothing — which made member listing
9
+ * return zero on large vanilla classes when callers used the natural OR syntax.
10
+ *
11
+ * Java member names never contain `|`, so treating `|` as OR never collides with
12
+ * a legitimate substring search. Empty or whitespace-only alternatives are
13
+ * dropped; a pattern with no usable alternative matches nothing (a non-empty
14
+ * filter that excludes everything), matching the prior empty-filter behavior.
15
+ */
16
+ export function matchesMemberPattern(name, pattern) {
17
+ const alternatives = pattern
18
+ .split("|")
19
+ .map((part) => part.trim().toLowerCase())
20
+ .filter((part) => part.length > 0);
21
+ if (alternatives.length === 0) {
22
+ return false;
23
+ }
24
+ const lowerName = name.toLowerCase();
25
+ return alternatives.some((alt) => lowerName.includes(alt));
26
+ }
27
+ //# sourceMappingURL=member-pattern.js.map
@@ -11,7 +11,7 @@ import * as indexer from "./source/indexer.js";
11
11
  import * as workspaceTarget from "./source/workspace-target.js";
12
12
  import * as validateMixinModule from "./source/validate-mixin.js";
13
13
  import * as artifactResolver from "./source/artifact-resolver.js";
14
- import type { WireMembersBlock } from "./source/class-source/members-builder.js";
14
+ import type { MemberProjection, ProjectedMembersBlock } from "./source/class-source/members-builder.js";
15
15
  import { type StageEmitter } from "./stage-emitter.js";
16
16
  import { WorkspaceMappingService, type WorkspaceCompileMappingOutput } from "./workspace-mapping-service.js";
17
17
  import { type WorkspaceContextCache } from "./workspace-context-cache.js";
@@ -266,11 +266,15 @@ export type GetClassMembersInput = {
266
266
  strictVersion?: boolean;
267
267
  /** When true, keep jvmDescriptor on FIELD members (always kept on methods/constructors). */
268
268
  includeDescriptors?: boolean;
269
+ /** Narrow the per-member fields returned: "names" | "signatures" | "full" (default "full"). */
270
+ projection?: MemberProjection;
269
271
  };
270
272
  export type DecompiledMember = {
271
273
  name: string;
272
- line: number;
273
- kind: "constructor" | "field" | "method";
274
+ /** Source line of the declaration. Omitted under a non-"full" projection. */
275
+ line?: number;
276
+ /** Member kind. Omitted under a non-"full" projection (the array conveys it). */
277
+ kind?: "constructor" | "field" | "method";
274
278
  };
275
279
  export type DecompiledFallback = {
276
280
  constructors: DecompiledMember[];
@@ -281,7 +285,7 @@ export type DecompiledFallback = {
281
285
  export type GetClassMembersStatus = "ok" | "members_unavailable" | "partial";
282
286
  export type GetClassMembersOutput = {
283
287
  className: string;
284
- members: WireMembersBlock;
288
+ members: ProjectedMembersBlock;
285
289
  counts: {
286
290
  constructors: number;
287
291
  fields: number;
@@ -146,7 +146,7 @@ export class SourceService {
146
146
  // Classes/fields surface as `mapping_unavailable` when the graph yields nothing, but
147
147
  // a method query with a non-empty graph returns `not_found` (empty member lookup) —
148
148
  // which previously skipped the runtime fallback and produced false negatives for
149
- // methods that genuinely exist (B1). Treat `not_found` methods as fallback-eligible.
149
+ // methods that genuinely exist. Treat `not_found` methods as fallback-eligible.
150
150
  const fallbackEligible = result.status === "mapping_unavailable" ||
151
151
  (result.status === "not_found" && input.kind === "method");
152
152
  if (!fallbackEligible ||
@@ -672,7 +672,7 @@ export function buildInvalidInputGuidance(tool, normalizedInput) {
672
672
  }
673
673
  if (tool === "get-class-source" || tool === "get-class-members") {
674
674
  return gatedGuidance(tool, [
675
- `${tool}.target must be an object: {"kind":"version|jar|coordinate","value":"..."} or {"kind":"artifact","artifactId":"..."} (same shape as resolve-artifact).`,
675
+ `${tool}.target must be an object: {"kind":"version|jar|coordinate","value":"..."}, {"kind":"workspace"}, {"kind":"dependency","group":"...","name":"...","versionFromProject":true} (inspect a Fabric/loader dependency like vanilla), or {"kind":"artifact","artifactId":"..."} (same shape as resolve-artifact).`,
676
676
  "Bare string targets are not accepted; wrap the value under target.kind/target.value."
677
677
  ], buildSourceLookupSuggestedParams(tool, normalizedInput));
678
678
  }
@@ -211,9 +211,12 @@ export declare const sourceLookupTargetSchema: z.ZodDiscriminatedUnion<"kind", [
211
211
  kind: "artifact";
212
212
  artifactId: string;
213
213
  }>]>;
214
- export declare const RESOLVE_ARTIFACT_TARGET_DESCRIPTION = "Object, not string. e.g. {\"kind\":\"version\",\"value\":\"1.21.10\"}, {\"kind\":\"workspace\"}, {\"kind\":\"dependency\",\"group\":\"g\",\"name\":\"n\"}.";
215
- export declare const SOURCE_LOOKUP_TARGET_DESCRIPTION = "Same shape as resolve-artifact target, plus {\"kind\":\"artifact\",\"artifactId\":\"...\"} to reuse a resolved artifact. Object, not string.";
214
+ export declare const RESOLVE_ARTIFACT_TARGET_DESCRIPTION = "Object, not string. e.g. {\"kind\":\"version\",\"value\":\"1.21.10\"}, {\"kind\":\"workspace\"}, or to inspect a loader/Fabric dependency like vanilla: {\"kind\":\"dependency\",\"group\":\"net.fabricmc.fabric-api\",\"name\":\"fabric-api\",\"versionFromProject\":true} (needs projectPath) or with an explicit \"version\".";
215
+ export declare const SOURCE_LOOKUP_TARGET_DESCRIPTION = "Same shape as resolve-artifact target (incl. {\"kind\":\"dependency\",...} to read a Fabric/loader dependency class like vanilla), plus {\"kind\":\"artifact\",\"artifactId\":\"...\"} to reuse a resolved artifact. Object, not string.";
216
216
  export declare const SOURCE_SCOPE_DESCRIPTION = "vanilla = Mojang client jar only; merged = merged runtime discovery; loader = loader-transformed runtime jars.";
217
+ export declare const MEMBER_PATTERN_DESCRIPTION = "Case-insensitive substring filter on member names. Use \"|\" for OR alternatives, e.g. \"getStateForPlacement|canSurvive|setPlacedBy\" (each token is matched as a substring, not a regex).";
218
+ export declare const memberProjectionSchema: z.ZodEnum<["names", "signatures", "full"]>;
219
+ export declare const MEMBER_PROJECTION_DESCRIPTION = "Per-member field projection: \"names\" (member name only), \"signatures\" (name + javaSignature, no jvmDescriptor), or \"full\" (default; complete member shape). Use \"names\"/\"signatures\" to cut response tokens for existence/signature checks.";
217
220
  export declare const SIGNATURE_MODE_DESCRIPTION = "exact: descriptor required for kind=method; name-only (default): match by owner+name only.";
218
221
  export declare const NAME_MODE_DESCRIPTION = "auto (default): FQCN, or dotless name where allowed; fqcn: require FQCN.";
219
222
  export declare const listVersionsShape: {
@@ -850,6 +853,7 @@ export declare const getClassMembersShape: {
850
853
  includeSynthetic: z.ZodDefault<z.ZodBoolean>;
851
854
  includeInherited: z.ZodDefault<z.ZodBoolean>;
852
855
  memberPattern: z.ZodOptional<z.ZodString>;
856
+ projection: z.ZodOptional<z.ZodEnum<["names", "signatures", "full"]>>;
853
857
  maxMembers: z.ZodOptional<z.ZodNumber>;
854
858
  cursor: z.ZodOptional<z.ZodString>;
855
859
  projectPath: z.ZodOptional<z.ZodString>;
@@ -938,6 +942,7 @@ export declare const getClassMembersSchema: z.ZodObject<{
938
942
  includeSynthetic: z.ZodDefault<z.ZodBoolean>;
939
943
  includeInherited: z.ZodDefault<z.ZodBoolean>;
940
944
  memberPattern: z.ZodOptional<z.ZodString>;
945
+ projection: z.ZodOptional<z.ZodEnum<["names", "signatures", "full"]>>;
941
946
  maxMembers: z.ZodOptional<z.ZodNumber>;
942
947
  cursor: z.ZodOptional<z.ZodString>;
943
948
  projectPath: z.ZodOptional<z.ZodString>;
@@ -992,6 +997,7 @@ export declare const getClassMembersSchema: z.ZodObject<{
992
997
  memberPattern?: string | undefined;
993
998
  include?: ("entries" | "workspace" | "warnings" | "source" | "registry" | "preview" | "files" | "samples" | "timings" | "issues" | "recovery" | "paths" | "members" | "provenance" | "artifact" | "candidates" | "classes" | "timeline" | "descriptors" | "diff" | "matrix" | "health" | "owners" | "cacheEntries")[] | undefined;
994
999
  gradleUserHome?: string | undefined;
1000
+ projection?: "full" | "names" | "signatures" | undefined;
995
1001
  }, {
996
1002
  className: string;
997
1003
  target: {
@@ -1034,6 +1040,7 @@ export declare const getClassMembersSchema: z.ZodObject<{
1034
1040
  include?: ("entries" | "workspace" | "warnings" | "source" | "registry" | "preview" | "files" | "samples" | "timings" | "issues" | "recovery" | "paths" | "members" | "provenance" | "artifact" | "candidates" | "classes" | "timeline" | "descriptors" | "diff" | "matrix" | "health" | "owners" | "cacheEntries")[] | undefined;
1035
1041
  gradleUserHome?: string | undefined;
1036
1042
  includeProvenance?: boolean | undefined;
1043
+ projection?: "full" | "names" | "signatures" | undefined;
1037
1044
  includeDescriptors?: boolean | undefined;
1038
1045
  }>;
1039
1046
  export declare const verifyMixinTargetMemberSchema: z.ZodDiscriminatedUnion<"kind", [z.ZodObject<{
@@ -1811,6 +1818,7 @@ export declare const batchClassMembersShape: {
1811
1818
  concurrency: z.ZodOptional<z.ZodNumber>;
1812
1819
  failFast: z.ZodOptional<z.ZodBoolean>;
1813
1820
  detail: z.ZodDefault<z.ZodEnum<["summary", "standard", "full"]>>;
1821
+ projection: z.ZodOptional<z.ZodEnum<["names", "signatures", "full"]>>;
1814
1822
  include: z.ZodOptional<z.ZodArray<z.ZodEnum<["warnings", "provenance", "candidates", "members", "descriptors", "source", "files", "samples", "diff", "issues", "timeline", "matrix", "entries", "workspace", "health", "recovery", "paths", "owners", "preview", "cacheEntries", "timings", "artifact", "classes", "registry"]>, "many">>;
1815
1823
  entries: z.ZodArray<z.ZodObject<{
1816
1824
  className: z.ZodString;
@@ -1905,6 +1913,7 @@ export declare const batchClassMembersSchema: z.ZodObject<{
1905
1913
  concurrency: z.ZodOptional<z.ZodNumber>;
1906
1914
  failFast: z.ZodOptional<z.ZodBoolean>;
1907
1915
  detail: z.ZodDefault<z.ZodEnum<["summary", "standard", "full"]>>;
1916
+ projection: z.ZodOptional<z.ZodEnum<["names", "signatures", "full"]>>;
1908
1917
  include: z.ZodOptional<z.ZodArray<z.ZodEnum<["warnings", "provenance", "candidates", "members", "descriptors", "source", "files", "samples", "diff", "issues", "timeline", "matrix", "entries", "workspace", "health", "recovery", "paths", "owners", "preview", "cacheEntries", "timings", "artifact", "classes", "registry"]>, "many">>;
1909
1918
  entries: z.ZodArray<z.ZodObject<{
1910
1919
  className: z.ZodString;
@@ -1967,6 +1976,7 @@ export declare const batchClassMembersSchema: z.ZodObject<{
1967
1976
  sourcePriority?: "loom-first" | "maven-first" | undefined;
1968
1977
  include?: ("entries" | "workspace" | "warnings" | "source" | "registry" | "preview" | "files" | "samples" | "timings" | "issues" | "recovery" | "paths" | "members" | "provenance" | "artifact" | "candidates" | "classes" | "timeline" | "descriptors" | "diff" | "matrix" | "health" | "owners" | "cacheEntries")[] | undefined;
1969
1978
  gradleUserHome?: string | undefined;
1979
+ projection?: "full" | "names" | "signatures" | undefined;
1970
1980
  concurrency?: number | undefined;
1971
1981
  failFast?: boolean | undefined;
1972
1982
  }, {
@@ -2008,6 +2018,7 @@ export declare const batchClassMembersSchema: z.ZodObject<{
2008
2018
  detail?: "summary" | "standard" | "full" | undefined;
2009
2019
  include?: ("entries" | "workspace" | "warnings" | "source" | "registry" | "preview" | "files" | "samples" | "timings" | "issues" | "recovery" | "paths" | "members" | "provenance" | "artifact" | "candidates" | "classes" | "timeline" | "descriptors" | "diff" | "matrix" | "health" | "owners" | "cacheEntries")[] | undefined;
2010
2020
  gradleUserHome?: string | undefined;
2021
+ projection?: "full" | "names" | "signatures" | undefined;
2011
2022
  concurrency?: number | undefined;
2012
2023
  failFast?: boolean | undefined;
2013
2024
  }>;
@@ -64,10 +64,10 @@ export const workspaceTargetSchema = z.object({
64
64
  });
65
65
  export const dependencyTargetSchema = z.object({
66
66
  kind: z.literal("dependency"),
67
- group: nonEmptyString,
68
- name: nonEmptyString,
69
- version: z.string().trim().min(1).optional(),
70
- versionFromProject: z.boolean().optional()
67
+ group: nonEmptyString.describe('Maven group, e.g. "net.fabricmc.fabric-api" or "dev.architectury".'),
68
+ name: nonEmptyString.describe('Maven artifact name, e.g. "fabric-api" or "architectury".'),
69
+ version: z.string().trim().min(1).optional().describe("Explicit version; omit and set versionFromProject to resolve it from the workspace."),
70
+ versionFromProject: z.boolean().optional().describe("Resolve the version from the project's gradle.properties / gradle cache (requires projectPath).")
71
71
  });
72
72
  export const resolveArtifactTargetSchema = z.discriminatedUnion("kind", [
73
73
  z.object({ kind: z.literal("version"), value: nonEmptyString }),
@@ -89,11 +89,14 @@ export const sourceLookupTargetSchema = z.discriminatedUnion("kind", [
89
89
  dependencyTargetSchema,
90
90
  z.object({ kind: z.literal("artifact"), artifactId: nonEmptyString })
91
91
  ]);
92
- export const RESOLVE_ARTIFACT_TARGET_DESCRIPTION = 'Object, not string. e.g. {"kind":"version","value":"1.21.10"}, {"kind":"workspace"}, {"kind":"dependency","group":"g","name":"n"}.';
93
- export const SOURCE_LOOKUP_TARGET_DESCRIPTION = 'Same shape as resolve-artifact target, plus {"kind":"artifact","artifactId":"..."} to reuse a resolved artifact. Object, not string.';
92
+ export const RESOLVE_ARTIFACT_TARGET_DESCRIPTION = 'Object, not string. e.g. {"kind":"version","value":"1.21.10"}, {"kind":"workspace"}, or to inspect a loader/Fabric dependency like vanilla: {"kind":"dependency","group":"net.fabricmc.fabric-api","name":"fabric-api","versionFromProject":true} (needs projectPath) or with an explicit "version".';
93
+ export const SOURCE_LOOKUP_TARGET_DESCRIPTION = 'Same shape as resolve-artifact target (incl. {"kind":"dependency",...} to read a Fabric/loader dependency class like vanilla), plus {"kind":"artifact","artifactId":"..."} to reuse a resolved artifact. Object, not string.';
94
94
  export const SOURCE_SCOPE_DESCRIPTION = "vanilla = Mojang client jar only; merged = merged runtime discovery; loader = loader-transformed runtime jars.";
95
95
  // Shared describe() text reused by every symbol-lookup tool so the contract reads
96
96
  // identically on find-mapping and check-symbol-exists (and any future sibling).
97
+ export const MEMBER_PATTERN_DESCRIPTION = 'Case-insensitive substring filter on member names. Use "|" for OR alternatives, e.g. "getStateForPlacement|canSurvive|setPlacedBy" (each token is matched as a substring, not a regex).';
98
+ export const memberProjectionSchema = z.enum(["names", "signatures", "full"]);
99
+ export const MEMBER_PROJECTION_DESCRIPTION = 'Per-member field projection: "names" (member name only), "signatures" (name + javaSignature, no jvmDescriptor), or "full" (default; complete member shape). Use "names"/"signatures" to cut response tokens for existence/signature checks.';
97
100
  export const SIGNATURE_MODE_DESCRIPTION = "exact: descriptor required for kind=method; name-only (default): match by owner+name only.";
98
101
  export const NAME_MODE_DESCRIPTION = "auto (default): FQCN, or dotless name where allowed; fqcn: require FQCN.";
99
102
  export const listVersionsShape = {
@@ -158,7 +161,8 @@ export const getClassMembersShape = {
158
161
  access: memberAccessSchema.default("public"),
159
162
  includeSynthetic: z.boolean().default(false),
160
163
  includeInherited: z.boolean().default(false),
161
- memberPattern: optionalNonEmptyString,
164
+ memberPattern: optionalNonEmptyString.describe(MEMBER_PATTERN_DESCRIPTION),
165
+ projection: memberProjectionSchema.optional().describe(MEMBER_PROJECTION_DESCRIPTION),
162
166
  maxMembers: optionalPositiveInt.describe("default 150, max 5000. Page beyond the first 150 with cursor."),
163
167
  cursor: optionalNonEmptyString.describe("nextCursor from the previous response."),
164
168
  projectPath: optionalNonEmptyString,
@@ -262,7 +266,7 @@ export const batchClassMembersEntrySchema = z.object({
262
266
  access: memberAccessSchema.optional(),
263
267
  includeSynthetic: z.boolean().optional(),
264
268
  includeInherited: z.boolean().optional(),
265
- memberPattern: optionalNonEmptyString,
269
+ memberPattern: optionalNonEmptyString.describe(MEMBER_PATTERN_DESCRIPTION),
266
270
  maxMembers: optionalPositiveInt
267
271
  });
268
272
  export const batchClassMembersShape = {
@@ -278,6 +282,7 @@ export const batchClassMembersShape = {
278
282
  concurrency: z.number().int().min(1).max(8).optional(),
279
283
  failFast: z.boolean().optional(),
280
284
  detail: detailParam("summary"),
285
+ projection: memberProjectionSchema.optional().describe(MEMBER_PROJECTION_DESCRIPTION),
281
286
  include: responseIncludeParam,
282
287
  entries: z.array(batchClassMembersEntrySchema).min(1).max(50)
283
288
  };
@@ -67,6 +67,29 @@ Start here when you are not sure which tool to reach for. In every row, the left
67
67
  | `{ kind: "workspace", scope?, strict? }` | When the caller already passes a `projectPath` and wants the tool to detect Minecraft version, compile mapping, and loader from `gradle.properties` and `build.gradle(.kts)`. | `projectPath` | Synthesised to `{ kind: "version", value: <detected> }`. Scope precedence is `target.scope` → top-level `scope` → loader-derived default (`"merged"` when a loader is detected, `"vanilla"` otherwise). When the version is not detected, raises `ERR_WORKSPACE_VERSION_UNRESOLVED` regardless of `strict`. Detected facts surface on `provenance.workspaceResolution`. |
68
68
  | `{ kind: "dependency", group, name, version?, versionFromProject? }` | When the caller wants to resolve a Maven-coordinate dependency (e.g. `dev.architectury:architectury`) without computing the exact version themselves. | `projectPath` (unless `version` is given) | Synthesised to `{ kind: "coordinate", value: "<group>:<name>:<version>" }`. The dependency JAR is treated as non-vanilla: binary remap is suppressed. When the caller asks for a non-obfuscated mapping the resolver returns the JAR with `mappingApplied: "obfuscated"` and `qualityFlags: ["dependency-mapping-unverified"]`, plus a warning that the caller must validate symbol availability. Resolution metadata appears on `provenance.dependencyResolution`. |
69
69
 
70
+ ### Inspecting a Fabric / loader dependency like vanilla
71
+
72
+ To read members or source of a Fabric API (or any loader/Maven dependency) class
73
+ the same way you read vanilla, pass a `dependency` target straight to
74
+ `get-class-members` / `get-class-source` — no separate lookup tool is needed:
75
+
76
+ ```jsonc
77
+ // members of a Fabric API class, version taken from the workspace
78
+ {
79
+ "tool": "get-class-members",
80
+ "className": "net.fabricmc.fabric.api.event.player.UseEntityCallback",
81
+ "target": { "kind": "dependency", "group": "net.fabricmc.fabric-api", "name": "fabric-api", "versionFromProject": true },
82
+ "projectPath": "/path/to/workspace"
83
+ }
84
+ ```
85
+
86
+ Use an explicit `"version"` instead of `versionFromProject` when you already
87
+ know it. Because dependency JARs are not remapped, members come back in the
88
+ dependency's own namespace with `qualityFlags: ["dependency-mapping-unverified"]`;
89
+ treat the names as the JAR's compiled names. For repeated lookups against the
90
+ same dependency, call `resolve-artifact` once and reuse the returned
91
+ `artifactId` via `target: { kind: "artifact", artifactId }`.
92
+
70
93
  Workspace detection is memoised in a process-resident `WorkspaceContextCache` (16-entry LRU, 5-minute TTL). The cache is observable through `manage-cache` with `cacheKinds: ["workspace"]`, and individual entries can be invalidated via `selector.projectPath`.
71
94
 
72
95
  `target.kind="dependency"` resolution probes four de-duplicated `gradle.properties` keys in order — `name_version`, `camelCaseVersion`, `lastSegment(group)_name_version`, and `camelCase(lastSegment(group)_name)Version` — before falling back to the modules-2 cache layout `~/.gradle/caches/modules-2/files-2.1/<group>/<name>/`. Version tokens that contain path separators, `..`, NUL, control characters, or any character outside `[A-Za-z0-9._+-]` are rejected; in `gradle.properties` the rejection is recorded under `attempts[]` as `gradle.properties:<key>:rejected-unsafe-version` and the next key is tried. Snapshot and dev directories are excluded by default. The modules-2 fallback only resolves when exactly one valid entry remains; multiple entries raise `ERR_DEPENDENCY_VERSION_UNRESOLVED` so a global cache cannot supply a version the workspace did not declare.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhisang/minecraft-modding-mcp",
3
- "version": "6.0.0",
3
+ "version": "6.1.0",
4
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",