@adhisang/minecraft-modding-mcp 1.1.0 → 1.2.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.
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
- import { readFileSync } from "node:fs";
3
- import { join } from "node:path";
2
+ import { readFileSync, writeFileSync } from "node:fs";
3
+ import { isAbsolute, join, resolve as resolvePath } from "node:path";
4
4
  import { createError, ERROR_CODES } from "./errors.js";
5
5
  import { log } from "./logger.js";
6
6
  import { decompileBinaryJar } from "./decompiler/vineflower.js";
@@ -76,11 +76,42 @@ export class ModDecompileService {
76
76
  details: { className, jarPath, availableCount: files.length }
77
77
  });
78
78
  }
79
- const content = readFileSync(join(outputDir, matched), "utf8");
79
+ const fullContent = readFileSync(join(outputDir, matched), "utf8");
80
+ const totalLines = fullContent.split("\n").length;
81
+ let content = fullContent;
82
+ let truncated;
83
+ let charsTruncated;
84
+ let outputFilePath;
85
+ // Apply maxLines truncation
86
+ if (input.maxLines != null && input.maxLines > 0) {
87
+ const lines = content.split("\n");
88
+ if (lines.length > input.maxLines) {
89
+ content = lines.slice(0, input.maxLines).join("\n");
90
+ truncated = true;
91
+ }
92
+ }
93
+ // Apply maxChars truncation
94
+ if (input.maxChars != null && input.maxChars > 0 && content.length > input.maxChars) {
95
+ content = content.slice(0, input.maxChars);
96
+ charsTruncated = true;
97
+ truncated = true;
98
+ }
99
+ // Write to outputFile if specified (after maxLines/maxChars truncation, if any)
100
+ if (input.outputFile) {
101
+ const outPath = isAbsolute(input.outputFile)
102
+ ? input.outputFile
103
+ : resolvePath(input.outputFile);
104
+ writeFileSync(outPath, content, "utf8");
105
+ outputFilePath = outPath;
106
+ content = `[Written to ${outPath}]`;
107
+ }
80
108
  return {
81
109
  className: filePathToClassName(matched),
82
110
  content,
83
- totalLines: content.split("\n").length,
111
+ totalLines,
112
+ truncated,
113
+ charsTruncated,
114
+ outputFilePath,
84
115
  modId: analysis.modId,
85
116
  warnings
86
117
  };
@@ -100,6 +131,7 @@ export class ModDecompileService {
100
131
  catch {
101
132
  analysis = {
102
133
  loader: "unknown",
134
+ jarKind: "binary",
103
135
  classCount: 0
104
136
  };
105
137
  warnings.push("Could not extract mod metadata from JAR.");
@@ -25,4 +25,5 @@ export declare class ModSearchService {
25
25
  private readonly modDecompileService;
26
26
  constructor(modDecompileService: ModDecompileService);
27
27
  searchModSource(input: SearchModSourceInput): Promise<SearchModSourceOutput>;
28
+ private searchSourceJarEntriesDirect;
28
29
  }
@@ -3,6 +3,8 @@ import { join } from "node:path";
3
3
  import { createError, ERROR_CODES } from "./errors.js";
4
4
  import { log } from "./logger.js";
5
5
  import { validateAndNormalizeJarPath } from "./path-resolver.js";
6
+ import { analyzeModJar } from "./mod-analyzer.js";
7
+ import { iterateJavaEntriesAsUtf8 } from "./source-jar-reader.js";
6
8
  const DEFAULT_LIMIT = 50;
7
9
  const MAX_LIMIT = 200;
8
10
  const MAX_QUERY_LENGTH = 200;
@@ -76,6 +78,33 @@ export class ModSearchService {
76
78
  warnings.push(`limit was clamped to ${MAX_LIMIT} from ${requestedLimit}.`);
77
79
  }
78
80
  const startedAt = Date.now();
81
+ const jarAnalysis = await analyzeModJar(jarPath);
82
+ if (jarAnalysis.jarKind === "source") {
83
+ warnings.push("Detected source jar; searching Java entries directly without decompilation.");
84
+ const directResult = await this.searchSourceJarEntriesDirect({
85
+ jarPath,
86
+ query,
87
+ searchType,
88
+ limit,
89
+ regex
90
+ });
91
+ warnings.push(...directResult.warnings);
92
+ log("info", "mod-search.done", {
93
+ jarPath,
94
+ query,
95
+ searchType,
96
+ hitCount: directResult.hits.length,
97
+ durationMs: Date.now() - startedAt
98
+ });
99
+ return {
100
+ query,
101
+ searchType,
102
+ hits: directResult.hits,
103
+ totalHits: directResult.totalHits,
104
+ truncated: directResult.truncated,
105
+ warnings
106
+ };
107
+ }
79
108
  const decompileResult = await this.modDecompileService.decompileModJar({ jarPath });
80
109
  const outputDir = decompileResult.outputDir;
81
110
  warnings.push(...decompileResult.warnings);
@@ -159,6 +188,73 @@ export class ModSearchService {
159
188
  warnings
160
189
  };
161
190
  }
191
+ async searchSourceJarEntriesDirect(input) {
192
+ const warnings = [];
193
+ const hits = [];
194
+ let totalHits = 0;
195
+ let reachedLimit = false;
196
+ for await (const entry of iterateJavaEntriesAsUtf8(input.jarPath)) {
197
+ if (hits.length >= input.limit) {
198
+ reachedLimit = true;
199
+ break;
200
+ }
201
+ const filePath = entry.filePath;
202
+ const className = filePathToClassName(filePath);
203
+ if (input.searchType === "class" || input.searchType === "all") {
204
+ const simpleClassName = className.split(".").pop() ?? className;
205
+ input.regex.lastIndex = 0;
206
+ if (input.regex.test(simpleClassName)) {
207
+ totalHits += 1;
208
+ if (hits.length < input.limit) {
209
+ hits.push({
210
+ type: "class",
211
+ name: className,
212
+ file: filePath
213
+ });
214
+ }
215
+ if (input.searchType === "class") {
216
+ continue;
217
+ }
218
+ }
219
+ }
220
+ if (input.searchType === "method" ||
221
+ input.searchType === "field" ||
222
+ input.searchType === "content" ||
223
+ input.searchType === "all") {
224
+ const lines = entry.content.split("\n");
225
+ for (let i = 0; i < lines.length; i++) {
226
+ if (hits.length >= input.limit) {
227
+ reachedLimit = true;
228
+ break;
229
+ }
230
+ input.regex.lastIndex = 0;
231
+ if (!input.regex.test(lines[i])) {
232
+ continue;
233
+ }
234
+ const lineType = classifyLine(lines[i]);
235
+ if (input.searchType !== "all" && input.searchType !== lineType) {
236
+ continue;
237
+ }
238
+ totalHits += 1;
239
+ if (hits.length < input.limit) {
240
+ hits.push({
241
+ type: lineType,
242
+ name: lineType === "content" ? className : extractSymbolName(lines[i], lineType),
243
+ file: filePath,
244
+ line: i + 1,
245
+ context: extractContext(lines, i)
246
+ });
247
+ }
248
+ }
249
+ }
250
+ }
251
+ return {
252
+ hits,
253
+ totalHits,
254
+ truncated: reachedLimit,
255
+ warnings
256
+ };
257
+ }
162
258
  }
163
259
  function extractSymbolName(line, type) {
164
260
  const trimmed = line.trim();
@@ -25,6 +25,7 @@ export interface SearchCursorPayload {
25
25
  export interface SearchHitAccumulator {
26
26
  add(hit: SearchSourceHit): void;
27
27
  setTotalApproxOverride(count: number): void;
28
+ currentCount(): number;
28
29
  finalize(): {
29
30
  page: SearchSourceHit[];
30
31
  nextCursorHit?: SearchSourceHit;
@@ -137,6 +137,9 @@ export function createSearchHitAccumulator(limit, cursor) {
137
137
  setTotalApproxOverride(count) {
138
138
  totalApproxOverride = Math.max(0, Math.trunc(count));
139
139
  },
140
+ currentCount() {
141
+ return heap.length;
142
+ },
140
143
  finalize() {
141
144
  // Sort heap contents by scoreHitOrder (best first)
142
145
  const sorted = heap.slice().sort(scoreHitOrder);
@@ -51,10 +51,6 @@ function resolveLocalCoordinateCandidates(localM2Path, coordinate) {
51
51
  const direct = resolvePath(baseDir, `${base}${classifierSuffix}-sources.jar`);
52
52
  const fallback = resolvePath(baseDir, `${base}-sources.jar`);
53
53
  const candidates = [direct, fallback];
54
- if (parsed.classifier) {
55
- const prefix = `${parsed.artifactId}-${parsed.version}-${parsed.classifier}`;
56
- candidates.push(resolvePath(baseDir, `${base}-${prefix}-sources.jar`));
57
- }
58
54
  const existing = new Set();
59
55
  for (const candidate of candidates) {
60
56
  if (hasExistingJar(candidate)) {
@@ -1,9 +1,9 @@
1
1
  import { type ResponseContext as ExplorerResponseContext, type SignatureMember } from "./minecraft-explorer-service.js";
2
- import { type MixinValidationResult, type AccessWidenerValidationResult } from "./mixin-validator.js";
2
+ import { type MixinValidationResult, type MappingHealthReport, type AccessWidenerValidationResult } from "./mixin-validator.js";
3
3
  import { type ClassApiMatrixInput as MappingClassApiMatrixInput, type ClassApiMatrixOutput as MappingClassApiMatrixOutput, type FindMappingInput as MappingFindMappingInput, type FindMappingOutput as MappingFindMappingOutput, type ResolveMethodMappingExactInput as MappingResolveMethodMappingExactInput, type ResolveMethodMappingExactOutput as MappingResolveMethodMappingExactOutput, type SymbolResolutionOutput as MappingSymbolResolutionOutput, type SymbolExistenceInput as MappingSymbolExistenceInput, type SymbolExistenceOutput as MappingSymbolExistenceOutput } from "./mapping-service.js";
4
4
  import { RuntimeMetrics, type RuntimeMetricSnapshot } from "./observability.js";
5
5
  import { type WorkspaceCompileMappingOutput } from "./workspace-mapping-service.js";
6
- import type { ArtifactProvenance, ArtifactRow, Config, MappingSourcePriority, ResolvedSourceArtifact, SourceMapping, SourceTargetInput } from "./types.js";
6
+ import type { ArtifactProvenance, ArtifactRow, ArtifactScope, Config, MappingSourcePriority, ResolvedSourceArtifact, SourceMapping, SourceTargetInput } from "./types.js";
7
7
  import { type ListVersionsInput, type ListVersionsOutput } from "./version-service.js";
8
8
  import { type GetRegistryDataInput, type GetRegistryDataOutput } from "./registry-service.js";
9
9
  import { type CompareVersionsInput, type CompareVersionsOutput } from "./version-diff-service.js";
@@ -14,6 +14,10 @@ export type ResolveArtifactInput = {
14
14
  mapping?: SourceMapping;
15
15
  sourcePriority?: MappingSourcePriority;
16
16
  allowDecompile?: boolean;
17
+ projectPath?: string;
18
+ scope?: ArtifactScope;
19
+ preferProjectVersion?: boolean;
20
+ strictVersion?: boolean;
17
21
  };
18
22
  export type ResolveArtifactOutput = {
19
23
  artifactId: string;
@@ -30,6 +34,7 @@ export type ResolveArtifactOutput = {
30
34
  qualityFlags: string[];
31
35
  repoUrl?: string;
32
36
  warnings: string[];
37
+ sampleEntries?: string[];
33
38
  };
34
39
  type SymbolKind = "class" | "interface" | "enum" | "record" | "method" | "field";
35
40
  type SearchIntent = "symbol" | "text" | "path";
@@ -75,6 +80,7 @@ export type SearchRelation = {
75
80
  };
76
81
  relation: "calls" | "uses-type" | "imports";
77
82
  };
83
+ export type QueryMode = "auto" | "token" | "literal";
78
84
  export type SearchClassSourceInput = {
79
85
  artifactId: string;
80
86
  query: string;
@@ -82,6 +88,7 @@ export type SearchClassSourceInput = {
82
88
  match?: SearchMatch;
83
89
  scope?: SearchScope;
84
90
  include?: SearchInclude;
91
+ queryMode?: QueryMode;
85
92
  limit?: number;
86
93
  cursor?: string;
87
94
  };
@@ -137,19 +144,28 @@ export type ResolveWorkspaceSymbolInput = {
137
144
  export type ResolveWorkspaceSymbolOutput = MappingSymbolResolutionOutput & {
138
145
  workspaceDetection: WorkspaceCompileMappingOutput;
139
146
  };
147
+ export type SourceMode = "metadata" | "snippet" | "full";
140
148
  export type GetClassSourceInput = {
141
149
  artifactId?: string;
142
150
  target?: SourceTargetInput;
143
151
  className: string;
152
+ mode?: SourceMode;
144
153
  mapping?: SourceMapping;
145
154
  sourcePriority?: MappingSourcePriority;
146
155
  allowDecompile?: boolean;
156
+ projectPath?: string;
157
+ scope?: ArtifactScope;
158
+ preferProjectVersion?: boolean;
159
+ strictVersion?: boolean;
147
160
  startLine?: number;
148
161
  endLine?: number;
149
162
  maxLines?: number;
163
+ maxChars?: number;
164
+ outputFile?: string;
150
165
  };
151
166
  export type GetClassSourceOutput = {
152
167
  className: string;
168
+ mode: SourceMode;
153
169
  sourceText: string;
154
170
  totalLines: number;
155
171
  returnedRange: {
@@ -157,12 +173,30 @@ export type GetClassSourceOutput = {
157
173
  end: number;
158
174
  };
159
175
  truncated: boolean;
176
+ charsTruncated?: boolean;
160
177
  origin: ResolvedSourceArtifact["origin"];
161
178
  artifactId: string;
162
179
  requestedMapping: SourceMapping;
163
180
  mappingApplied: SourceMapping;
164
181
  provenance: ArtifactProvenance;
165
182
  qualityFlags: string[];
183
+ outputFile?: string;
184
+ warnings: string[];
185
+ };
186
+ export type FindClassInput = {
187
+ className: string;
188
+ artifactId: string;
189
+ limit?: number;
190
+ };
191
+ export type FindClassMatch = {
192
+ qualifiedName: string;
193
+ filePath: string;
194
+ line: number;
195
+ symbolKind: string;
196
+ };
197
+ export type FindClassOutput = {
198
+ matches: FindClassMatch[];
199
+ total: number;
166
200
  warnings: string[];
167
201
  };
168
202
  type MemberAccess = "public" | "all";
@@ -178,6 +212,10 @@ export type GetClassMembersInput = {
178
212
  includeInherited?: boolean;
179
213
  memberPattern?: string;
180
214
  maxMembers?: number;
215
+ projectPath?: string;
216
+ scope?: ArtifactScope;
217
+ preferProjectVersion?: boolean;
218
+ strictVersion?: boolean;
181
219
  };
182
220
  export type GetClassMembersOutput = {
183
221
  className: string;
@@ -318,12 +356,56 @@ export type IndexArtifactOutput = {
318
356
  mappingApplied: SourceMapping;
319
357
  };
320
358
  export type ValidateMixinInput = {
321
- source: string;
359
+ source?: string;
360
+ sourcePath?: string;
361
+ sourcePaths?: string[];
362
+ mixinConfigPath?: string | string[];
363
+ sourceRoot?: string;
364
+ sourceRoots?: string[];
322
365
  version: string;
323
366
  mapping?: SourceMapping;
324
367
  sourcePriority?: MappingSourcePriority;
368
+ scope?: ArtifactScope;
369
+ projectPath?: string;
370
+ preferProjectVersion?: boolean;
371
+ minSeverity?: "error" | "warning" | "all";
372
+ hideUncertain?: boolean;
373
+ explain?: boolean;
374
+ warningMode?: "full" | "aggregated";
375
+ preferProjectMapping?: boolean;
376
+ reportMode?: "compact" | "full";
377
+ warningCategoryFilter?: ("mapping" | "configuration" | "validation" | "resolution" | "parse")[];
378
+ treatInfoAsWarning?: boolean;
379
+ };
380
+ export type ValidateMixinBatchResult = {
381
+ sourcePath: string;
382
+ result?: MixinValidationResult;
383
+ error?: string;
384
+ };
385
+ export type ValidateMixinBatchIssueSummaryItem = {
386
+ kind: string;
387
+ confidence: string;
388
+ category: string;
389
+ count: number;
390
+ sampleTargets: string[];
391
+ };
392
+ export type ValidateMixinBatchOutput = {
393
+ results: ValidateMixinBatchResult[];
394
+ summary: {
395
+ total: number;
396
+ valid: number;
397
+ invalid: number;
398
+ /** @deprecated Use processingErrors instead */
399
+ errors: number;
400
+ processingErrors: number;
401
+ totalValidationErrors: number;
402
+ totalValidationWarnings: number;
403
+ confidenceScore?: number;
404
+ };
405
+ issueSummary?: ValidateMixinBatchIssueSummaryItem[];
406
+ toolHealth?: MappingHealthReport;
325
407
  };
326
- export type ValidateMixinOutput = MixinValidationResult;
408
+ export type ValidateMixinOutput = MixinValidationResult | ValidateMixinBatchOutput;
327
409
  export type ValidateAccessWidenerInput = {
328
410
  content: string;
329
411
  version: string;
@@ -348,6 +430,8 @@ export declare class SourceService {
348
430
  private readonly modDecompileService;
349
431
  private readonly modSearchService;
350
432
  constructor(explicitConfig?: Config, metrics?: RuntimeMetrics);
433
+ private discoverVersionSourceJar;
434
+ private buildVersionSourceRecoveryCommand;
351
435
  resolveArtifact(input: ResolveArtifactInput): Promise<ResolveArtifactOutput>;
352
436
  searchClassSource(input: SearchClassSourceInput): Promise<SearchClassSourceOutput>;
353
437
  getArtifactFile(input: GetArtifactFileInput): Promise<GetArtifactFileOutput>;
@@ -365,9 +449,11 @@ export declare class SourceService {
365
449
  resolveWorkspaceSymbol(input: ResolveWorkspaceSymbolInput): Promise<ResolveWorkspaceSymbolOutput>;
366
450
  traceSymbolLifecycle(input: TraceSymbolLifecycleInput): Promise<TraceSymbolLifecycleOutput>;
367
451
  diffClassSignatures(input: DiffClassSignaturesInput): Promise<DiffClassSignaturesOutput>;
452
+ findClass(input: FindClassInput): FindClassOutput;
368
453
  getClassSource(input: GetClassSourceInput): Promise<GetClassSourceOutput>;
369
454
  getClassMembers(input: GetClassMembersInput): Promise<GetClassMembersOutput>;
370
455
  validateMixin(input: ValidateMixinInput): Promise<ValidateMixinOutput>;
456
+ private validateMixinBatch;
371
457
  validateAccessWidener(input: ValidateAccessWidenerInput): Promise<ValidateAccessWidenerOutput>;
372
458
  getRuntimeMetrics(): RuntimeMetricSnapshot;
373
459
  indexArtifact(input: IndexArtifactInput): Promise<IndexArtifactOutput>;
@@ -380,6 +466,7 @@ export declare class SourceService {
380
466
  private loadScopedFilePaths;
381
467
  private indexedCandidateLimit;
382
468
  private indexedCandidateLimitForMatch;
469
+ private extractClassMetadata;
383
470
  private resolveClassFilePath;
384
471
  private findNearestSymbolForLine;
385
472
  private findNearestSymbolFromList;