@adhisang/minecraft-modding-mcp 3.1.1 → 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 (61) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +37 -18
  3. package/dist/access-transformer-parser.d.ts +17 -0
  4. package/dist/access-transformer-parser.js +97 -0
  5. package/dist/cache-registry.d.ts +1 -1
  6. package/dist/cache-registry.js +10 -2
  7. package/dist/concurrency.d.ts +1 -0
  8. package/dist/concurrency.js +24 -0
  9. package/dist/config.d.ts +10 -1
  10. package/dist/config.js +52 -1
  11. package/dist/decompiler/vineflower.js +22 -21
  12. package/dist/entry-tools/analyze-mod-service.d.ts +4 -4
  13. package/dist/entry-tools/analyze-symbol-service.d.ts +22 -22
  14. package/dist/entry-tools/analyze-symbol-service.js +13 -2
  15. package/dist/entry-tools/inspect-minecraft-service.d.ts +168 -168
  16. package/dist/entry-tools/inspect-minecraft-service.js +8 -2
  17. package/dist/entry-tools/manage-cache-service.d.ts +4 -4
  18. package/dist/entry-tools/validate-project-service.d.ts +153 -16
  19. package/dist/entry-tools/validate-project-service.js +442 -25
  20. package/dist/gradle-paths.d.ts +4 -0
  21. package/dist/gradle-paths.js +57 -0
  22. package/dist/index.js +148 -30
  23. package/dist/lru-list.d.ts +31 -0
  24. package/dist/lru-list.js +102 -0
  25. package/dist/mapping-pipeline-service.d.ts +12 -1
  26. package/dist/mapping-pipeline-service.js +28 -1
  27. package/dist/mapping-service.d.ts +16 -0
  28. package/dist/mapping-service.js +405 -68
  29. package/dist/minecraft-explorer-service.d.ts +13 -0
  30. package/dist/minecraft-explorer-service.js +8 -4
  31. package/dist/mixin-validator.d.ts +33 -2
  32. package/dist/mixin-validator.js +218 -17
  33. package/dist/mod-analyzer.d.ts +1 -0
  34. package/dist/mod-analyzer.js +17 -1
  35. package/dist/mod-decompile-service.js +4 -4
  36. package/dist/mod-remap-service.js +1 -54
  37. package/dist/mod-search-service.d.ts +1 -0
  38. package/dist/mod-search-service.js +84 -51
  39. package/dist/observability.d.ts +18 -1
  40. package/dist/observability.js +44 -1
  41. package/dist/response-utils.d.ts +69 -0
  42. package/dist/response-utils.js +227 -0
  43. package/dist/source-jar-reader.d.ts +16 -0
  44. package/dist/source-jar-reader.js +103 -1
  45. package/dist/source-resolver.d.ts +9 -1
  46. package/dist/source-resolver.js +23 -16
  47. package/dist/source-service.d.ts +119 -3
  48. package/dist/source-service.js +1836 -218
  49. package/dist/storage/artifacts-repo.d.ts +4 -1
  50. package/dist/storage/artifacts-repo.js +33 -5
  51. package/dist/storage/files-repo.d.ts +0 -2
  52. package/dist/storage/files-repo.js +0 -11
  53. package/dist/storage/migrations.d.ts +1 -1
  54. package/dist/storage/migrations.js +10 -2
  55. package/dist/storage/schema.d.ts +2 -0
  56. package/dist/storage/schema.js +25 -0
  57. package/dist/tool-contract-manifest.js +8 -6
  58. package/dist/types.d.ts +20 -0
  59. package/dist/workspace-mapping-service.d.ts +13 -0
  60. package/dist/workspace-mapping-service.js +146 -14
  61. package/package.json +3 -1
@@ -3,6 +3,7 @@ import type { ArtifactProvenance, ArtifactRow, SourceMapping, SourceOrigin } fro
3
3
  type SqliteDatabase = InstanceType<typeof Database>;
4
4
  interface UpsertArtifactInput {
5
5
  artifactId: string;
6
+ alias?: string;
6
7
  origin: SourceOrigin;
7
8
  coordinate?: string;
8
9
  version?: string;
@@ -27,6 +28,7 @@ export declare class ArtifactsRepo {
27
28
  private readonly upsertStmt;
28
29
  private readonly getStmt;
29
30
  private readonly touchStmt;
31
+ private readonly setAliasStmt;
30
32
  private readonly deleteStmt;
31
33
  private readonly listStmt;
32
34
  private readonly countStmt;
@@ -34,8 +36,9 @@ export declare class ArtifactsRepo {
34
36
  private readonly listLruWithContentBytesStmt;
35
37
  constructor(db: SqliteDatabase);
36
38
  upsertArtifact(input: UpsertArtifactInput): void;
37
- getArtifact(artifactId: string): ArtifactRow | undefined;
39
+ getArtifact(artifactIdOrAlias: string): ArtifactRow | undefined;
38
40
  touchArtifact(artifactId: string, timestamp: string): void;
41
+ setAlias(artifactId: string, alias: string): void;
39
42
  deleteArtifact(artifactId: string): void;
40
43
  listArtifactsByLru(limit: number): ArtifactRow[];
41
44
  countArtifacts(): number;
@@ -35,6 +35,7 @@ function parseQualityFlags(value) {
35
35
  function toArtifactRow(record) {
36
36
  return {
37
37
  artifactId: record.artifact_id,
38
+ alias: record.alias ?? undefined,
38
39
  origin: record.origin,
39
40
  coordinate: record.coordinate ?? undefined,
40
41
  version: record.version ?? undefined,
@@ -59,6 +60,7 @@ export class ArtifactsRepo {
59
60
  upsertStmt;
60
61
  getStmt;
61
62
  touchStmt;
63
+ setAliasStmt;
62
64
  deleteStmt;
63
65
  listStmt;
64
66
  countStmt;
@@ -68,11 +70,12 @@ export class ArtifactsRepo {
68
70
  this.db = db;
69
71
  this.upsertStmt = this.db.prepare(`
70
72
  INSERT INTO artifacts (
71
- artifact_id, origin, coordinate, version, binary_jar_path, source_jar_path, repo_url, requested_mapping, mapping_applied, provenance_json, quality_flags_json, artifact_signature, is_decompiled, created_at, updated_at
73
+ artifact_id, alias, origin, coordinate, version, binary_jar_path, source_jar_path, repo_url, requested_mapping, mapping_applied, provenance_json, quality_flags_json, artifact_signature, is_decompiled, created_at, updated_at
72
74
  ) VALUES (
73
- @artifact_id, @origin, @coordinate, @version, @binary_jar_path, @source_jar_path, @repo_url, @requested_mapping, @mapping_applied, @provenance_json, @quality_flags_json, @artifact_signature, @is_decompiled, @created_at, @updated_at
75
+ @artifact_id, @alias, @origin, @coordinate, @version, @binary_jar_path, @source_jar_path, @repo_url, @requested_mapping, @mapping_applied, @provenance_json, @quality_flags_json, @artifact_signature, @is_decompiled, @created_at, @updated_at
74
76
  )
75
77
  ON CONFLICT(artifact_id) DO UPDATE SET
78
+ alias = excluded.alias,
76
79
  origin = excluded.origin,
77
80
  coordinate = excluded.coordinate,
78
81
  version = excluded.version,
@@ -87,9 +90,12 @@ export class ArtifactsRepo {
87
90
  is_decompiled = excluded.is_decompiled,
88
91
  updated_at = excluded.updated_at
89
92
  `);
93
+ // artifact_id is a 64-char SHA hex; alias is `<type>-<...>-<6charhex>` containing
94
+ // dashes/letters. The two namespaces cannot collide, so OR-matching is unambiguous.
90
95
  this.getStmt = this.db.prepare(`
91
96
  SELECT
92
97
  artifact_id,
98
+ alias,
93
99
  origin,
94
100
  coordinate,
95
101
  version,
@@ -105,17 +111,28 @@ export class ArtifactsRepo {
105
111
  created_at,
106
112
  updated_at
107
113
  FROM artifacts
108
- WHERE artifact_id = ?
114
+ WHERE artifact_id = ? OR alias = ?
115
+ LIMIT 1
109
116
  `);
110
117
  this.touchStmt = this.db.prepare(`
111
118
  UPDATE artifacts
112
119
  SET updated_at = ?
113
120
  WHERE artifact_id = ?
121
+ `);
122
+ // Persists alias on cache-hit / migrated-row paths where upsertArtifact would
123
+ // otherwise be skipped. Conditional WHERE keeps it idempotent and avoids a
124
+ // pointless write when alias is already correct.
125
+ this.setAliasStmt = this.db.prepare(`
126
+ UPDATE artifacts
127
+ SET alias = ?
128
+ WHERE artifact_id = ?
129
+ AND (alias IS NULL OR alias <> ?)
114
130
  `);
115
131
  this.deleteStmt = this.db.prepare(`DELETE FROM artifacts WHERE artifact_id = ?`);
116
132
  this.listStmt = this.db.prepare(`
117
133
  SELECT
118
134
  artifact_id,
135
+ alias,
119
136
  origin,
120
137
  coordinate,
121
138
  version,
@@ -157,6 +174,7 @@ export class ArtifactsRepo {
157
174
  upsertArtifact(input) {
158
175
  this.upsertStmt.run({
159
176
  artifact_id: input.artifactId,
177
+ alias: input.alias ?? null,
160
178
  origin: input.origin,
161
179
  coordinate: input.coordinate ?? null,
162
180
  version: input.version ?? null,
@@ -173,8 +191,11 @@ export class ArtifactsRepo {
173
191
  updated_at: input.timestamp
174
192
  });
175
193
  }
176
- getArtifact(artifactId) {
177
- const row = this.getStmt.get([artifactId]);
194
+ // Accepts either an artifact_id (64-char SHA hex) or a human-readable alias
195
+ // (e.g. `mc-1.21.10-mojang-merged-5ad2e7`). The two key namespaces cannot
196
+ // collide because aliases always contain `-` and a non-hex prefix.
197
+ getArtifact(artifactIdOrAlias) {
198
+ const row = this.getStmt.get([artifactIdOrAlias, artifactIdOrAlias]);
178
199
  if (!row) {
179
200
  return undefined;
180
201
  }
@@ -183,6 +204,13 @@ export class ArtifactsRepo {
183
204
  touchArtifact(artifactId, timestamp) {
184
205
  this.touchStmt.run([timestamp, artifactId]);
185
206
  }
207
+ // Backfills or rotates the alias for an existing row. Used by warm-cache
208
+ // resolveArtifact paths where upsertArtifact is skipped, so a freshly
209
+ // computed alias still reaches the DB and stays in sync with the value the
210
+ // caller just received in the response.
211
+ setAlias(artifactId, alias) {
212
+ this.setAliasStmt.run([alias, artifactId, alias]);
213
+ }
186
214
  deleteArtifact(artifactId) {
187
215
  this.deleteStmt.run([artifactId]);
188
216
  }
@@ -43,8 +43,6 @@ export declare class FilesRepo {
43
43
  private readonly db;
44
44
  private readonly deleteStmt;
45
45
  private readonly insertFilesStmt;
46
- private readonly insertFtsStmt;
47
- private readonly deleteFtsStmt;
48
46
  private readonly getContentStmt;
49
47
  private readonly listStmt;
50
48
  private readonly listRowsStmt;
@@ -125,8 +125,6 @@ export class FilesRepo {
125
125
  db;
126
126
  deleteStmt;
127
127
  insertFilesStmt;
128
- insertFtsStmt;
129
- deleteFtsStmt;
130
128
  getContentStmt;
131
129
  listStmt;
132
130
  listRowsStmt;
@@ -138,17 +136,10 @@ export class FilesRepo {
138
136
  this.db = db;
139
137
  this.deleteStmt = this.db.prepare(`
140
138
  DELETE FROM files WHERE artifact_id = ?
141
- `);
142
- this.deleteFtsStmt = this.db.prepare(`
143
- DELETE FROM files_fts WHERE artifact_id = ?
144
139
  `);
145
140
  this.insertFilesStmt = this.db.prepare(`
146
141
  INSERT INTO files (artifact_id, file_path, content, content_bytes, content_hash)
147
142
  VALUES (?, ?, ?, ?, ?)
148
- `);
149
- this.insertFtsStmt = this.db.prepare(`
150
- INSERT INTO files_fts (artifact_id, file_path, content)
151
- VALUES (?, ?, ?)
152
143
  `);
153
144
  this.getContentStmt = this.db.prepare(`
154
145
  SELECT artifact_id, file_path, content, content_bytes, content_hash
@@ -187,7 +178,6 @@ export class FilesRepo {
187
178
  }
188
179
  clearFilesForArtifact(artifactId) {
189
180
  this.deleteStmt.run([artifactId]);
190
- this.deleteFtsStmt.run([artifactId]);
191
181
  }
192
182
  insertFilesForArtifact(artifactId, files) {
193
183
  for (const file of files) {
@@ -199,7 +189,6 @@ export class FilesRepo {
199
189
  file.contentBytes,
200
190
  contentHash
201
191
  ]);
202
- this.insertFtsStmt.run([artifactId, file.filePath, file.content]);
203
192
  }
204
193
  }
205
194
  replaceFilesForArtifact(artifactId, files) {
@@ -6,6 +6,6 @@ type MigrationRunner = {
6
6
  };
7
7
  transaction<T>(fn: () => T): () => T;
8
8
  };
9
- export declare const LATEST_SCHEMA_VERSION = 2;
9
+ export declare const LATEST_SCHEMA_VERSION = 4;
10
10
  export declare function runMigrations(db: MigrationRunner): number;
11
11
  export {};
@@ -1,6 +1,6 @@
1
1
  import { createError, ERROR_CODES } from "../errors.js";
2
- import { SCHEMA_V1_STATEMENTS, SCHEMA_V2_STATEMENTS } from "./schema.js";
3
- export const LATEST_SCHEMA_VERSION = 2;
2
+ import { SCHEMA_V1_STATEMENTS, SCHEMA_V2_STATEMENTS, SCHEMA_V3_STATEMENTS, SCHEMA_V4_STATEMENTS } from "./schema.js";
3
+ export const LATEST_SCHEMA_VERSION = 4;
4
4
  const migrations = [
5
5
  {
6
6
  version: 1,
@@ -9,6 +9,14 @@ const migrations = [
9
9
  {
10
10
  version: 2,
11
11
  statements: SCHEMA_V2_STATEMENTS
12
+ },
13
+ {
14
+ version: 3,
15
+ statements: SCHEMA_V3_STATEMENTS
16
+ },
17
+ {
18
+ version: 4,
19
+ statements: SCHEMA_V4_STATEMENTS
12
20
  }
13
21
  ];
14
22
  function selectSchemaVersion(tx) {
@@ -1,2 +1,4 @@
1
1
  export declare const SCHEMA_V1_STATEMENTS: string[];
2
2
  export declare const SCHEMA_V2_STATEMENTS: string[];
3
+ export declare const SCHEMA_V4_STATEMENTS: string[];
4
+ export declare const SCHEMA_V3_STATEMENTS: string[];
@@ -164,4 +164,29 @@ export const SCHEMA_V2_STATEMENTS = [
164
164
  `DELETE FROM artifact_index_meta`,
165
165
  `DELETE FROM artifacts`
166
166
  ];
167
+ export const SCHEMA_V4_STATEMENTS = [
168
+ `ALTER TABLE artifacts ADD COLUMN alias TEXT`,
169
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_artifacts_alias ON artifacts(alias) WHERE alias IS NOT NULL`
170
+ ];
171
+ export const SCHEMA_V3_STATEMENTS = [
172
+ `DROP TABLE IF EXISTS files_fts`,
173
+ `CREATE VIRTUAL TABLE files_fts USING fts5(
174
+ artifact_id UNINDEXED,
175
+ file_path,
176
+ content,
177
+ content='files',
178
+ content_rowid='rowid',
179
+ tokenize = 'unicode61 separators ''._$'''
180
+ )`,
181
+ `CREATE TRIGGER IF NOT EXISTS trg_fts_insert AFTER INSERT ON files BEGIN
182
+ INSERT INTO files_fts(rowid, artifact_id, file_path, content)
183
+ VALUES (NEW.rowid, NEW.artifact_id, NEW.file_path, NEW.content);
184
+ END`,
185
+ // No UPDATE trigger: FilesRepo uses delete+insert, never updates rows in place.
186
+ `CREATE TRIGGER IF NOT EXISTS trg_fts_delete BEFORE DELETE ON files BEGIN
187
+ INSERT INTO files_fts(files_fts, rowid, artifact_id, file_path, content)
188
+ VALUES ('delete', OLD.rowid, OLD.artifact_id, OLD.file_path, OLD.content);
189
+ END`,
190
+ `INSERT INTO files_fts(files_fts) VALUES('rebuild')`
191
+ ];
167
192
  //# sourceMappingURL=schema.js.map
@@ -15,7 +15,7 @@ const SECTION_ROWS = {
15
15
  "| `analyze-symbol` | Handle symbol existence checks, namespace mapping, lifecycle tracing, workspace symbol resolution, and API overviews |",
16
16
  "| `compare-minecraft` | Compare version pairs, class diffs, registry diffs, and migration-oriented summaries |",
17
17
  "| `analyze-mod` | Summarize mod metadata, decompile and search mod code, inspect class source, and preview or apply remaps |",
18
- "| `validate-project` | Summarize workspaces and run direct Mixin or Access Widener validation |",
18
+ "| `validate-project` | Summarize workspaces and run direct Mixin, Access Widener, or Access Transformer validation |",
19
19
  "| `manage-cache` | List, verify, and preview or apply cache cleanup and rebuild operations |"
20
20
  ],
21
21
  ja: [
@@ -23,7 +23,7 @@ const SECTION_ROWS = {
23
23
  "| `analyze-symbol` | シンボル存在確認、名前空間変換、ライフサイクル追跡、ワークスペースシンボル解決、API 概要をまとめて扱う |",
24
24
  "| `compare-minecraft` | バージョン差分、クラス差分、レジストリ差分、移行向け概要を比較する |",
25
25
  "| `analyze-mod` | Mod メタデータの要約、Mod コードのデコンパイル / 検索、クラスソース確認、リマップのプレビュー / 実行を扱う |",
26
- "| `validate-project` | ワークスペース要約と、Mixin / Access Widener の直接検証を行う |",
26
+ "| `validate-project` | ワークスペース要約と、Mixin / Access Widener / Access Transformer の直接検証を行う |",
27
27
  "| `manage-cache` | キャッシュの一覧、検証、クリーンアップ / 再構築のプレビュー / 実行を行う |"
28
28
  ]
29
29
  },
@@ -93,14 +93,14 @@ const SECTION_ROWS = {
93
93
  },
94
94
  "mod-analysis": {
95
95
  en: [
96
- "| `analyze-mod-jar` | Extract mod metadata, dependencies, entrypoints, and mixin config info from a JAR |",
96
+ "| `analyze-mod-jar` | Extract mod metadata, dependencies, entrypoints, mixin config info, and packaged access transformer paths from a JAR |",
97
97
  "| `decompile-mod-jar` | Decompile a mod JAR and optionally return one class source |",
98
98
  "| `get-mod-class-source` | Read one class source from the decompiled mod cache |",
99
99
  "| `search-mod-source` | Search decompiled mod source by class, method, field, or content |",
100
100
  "| `remap-mod-jar` | Remap a Fabric or Quilt mod JAR to `yarn` or `mojang` names |"
101
101
  ],
102
102
  ja: [
103
- "| `analyze-mod-jar` | JAR から Mod メタデータ、依存関係、エントリポイント、Mixin 設定情報を抽出する |",
103
+ "| `analyze-mod-jar` | JAR から Mod メタデータ、依存関係、エントリポイント、Mixin 設定情報、同梱 Access Transformer パスを抽出する |",
104
104
  "| `decompile-mod-jar` | Mod JAR をデコンパイルし、必要に応じて 1 つのクラスソースを返す |",
105
105
  "| `get-mod-class-source` | デコンパイル済み Mod キャッシュから 1 つのクラスソースを読み取る |",
106
106
  "| `search-mod-source` | デコンパイル済み Mod ソースを class、method、field、content で検索する |",
@@ -110,11 +110,13 @@ const SECTION_ROWS = {
110
110
  "validation": {
111
111
  en: [
112
112
  "| `validate-mixin` | Validate Mixin source against a target Minecraft version |",
113
- "| `validate-access-widener` | Validate Access Widener content against a target Minecraft version |"
113
+ "| `validate-access-widener` | Validate Access Widener content against a target Minecraft version, optionally using runtime-aware Loom artifacts |",
114
+ "| `validate-access-transformer` | Validate Access Transformer content against a target Minecraft version, optionally using Forge/NeoForge runtime artifacts |"
114
115
  ],
115
116
  ja: [
116
117
  "| `validate-mixin` | 対象 Minecraft バージョンに対して Mixin ソースを検証する |",
117
- "| `validate-access-widener` | 対象 Minecraft バージョンに対して Access Widener の内容を検証する |"
118
+ "| `validate-access-widener` | 対象 Minecraft バージョンに対して Access Widener の内容を検証し、必要に応じて Loom runtime artifact も使う |",
119
+ "| `validate-access-transformer` | 対象 Minecraft バージョンに対して Access Transformer の内容を検証し、必要に応じて Forge / NeoForge runtime artifact も使う |"
118
120
  ]
119
121
  },
120
122
  "registry-diagnostics": {
package/dist/types.d.ts CHANGED
@@ -1,14 +1,18 @@
1
1
  export type SourceOrigin = "local-jar" | "local-m2" | "remote-repo" | "decompiled";
2
2
  export type SourceMapping = "obfuscated" | "mojang" | "intermediary" | "yarn";
3
+ export type AccessTransformerNamespace = "srg" | "mojang" | "obfuscated";
4
+ export type RuntimeValidationNamespace = SourceMapping | AccessTransformerNamespace;
3
5
  export type MappingSourcePriority = "loom-first" | "maven-first";
4
6
  export type ArtifactTargetKind = "version" | "jar" | "coordinate";
5
7
  export type ArtifactScope = "vanilla" | "merged" | "loader";
8
+ export type MappingVariant = "pass" | "mojang-remapped";
6
9
  export interface SourceTargetInput {
7
10
  kind: ArtifactTargetKind;
8
11
  value: string;
9
12
  }
10
13
  export interface ResolvedSourceArtifact {
11
14
  artifactId: string;
15
+ artifactAlias?: string;
12
16
  artifactSignature: string;
13
17
  origin: SourceOrigin;
14
18
  binaryJarPath?: string;
@@ -37,6 +41,21 @@ export interface ArtifactProvenance {
37
41
  };
38
42
  transformChain: string[];
39
43
  }
44
+ export interface RuntimeValidationProvenance<TMapping extends RuntimeValidationNamespace = RuntimeValidationNamespace> {
45
+ version: string;
46
+ jarPath: string;
47
+ requestedScope?: ArtifactScope;
48
+ appliedScope?: ArtifactScope;
49
+ requestedMapping: TMapping;
50
+ mappingApplied: TMapping;
51
+ origin: SourceOrigin | "loom-cache" | "version-jar";
52
+ resolutionNotes?: string[];
53
+ scopeFallback?: {
54
+ requested: string;
55
+ applied: string;
56
+ reason: string;
57
+ };
58
+ }
40
59
  export interface ErrorEnvelope {
41
60
  code: string;
42
61
  message: string;
@@ -85,6 +104,7 @@ export interface SourceSearchHit {
85
104
  }
86
105
  export interface ArtifactRow {
87
106
  artifactId: string;
107
+ alias: string | undefined;
88
108
  origin: SourceOrigin;
89
109
  coordinate: string | undefined;
90
110
  version: string | undefined;
@@ -13,7 +13,20 @@ export type WorkspaceCompileMappingOutput = {
13
13
  evidence: WorkspaceMappingEvidence[];
14
14
  warnings: string[];
15
15
  };
16
+ export type WorkspaceProjectLoader = "fabric" | "quilt" | "forge" | "neoforge" | "unknown";
17
+ export type WorkspaceLoaderEvidence = {
18
+ filePath: string;
19
+ loader: WorkspaceProjectLoader;
20
+ reason: string;
21
+ };
22
+ export type WorkspaceProjectLoaderOutput = {
23
+ resolved: boolean;
24
+ loader?: WorkspaceProjectLoader;
25
+ evidence: WorkspaceLoaderEvidence[];
26
+ warnings: string[];
27
+ };
16
28
  export declare class WorkspaceMappingService {
17
29
  detectCompileMapping(input: WorkspaceCompileMappingInput): Promise<WorkspaceCompileMappingOutput>;
18
30
  detectProjectMinecraftVersion(projectPath: string): Promise<string | undefined>;
31
+ detectProjectLoader(projectPath: string): Promise<WorkspaceProjectLoaderOutput>;
19
32
  }
@@ -1,7 +1,9 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { resolve } from "node:path";
3
3
  import fastGlob from "fast-glob";
4
+ import { mapWithConcurrencyLimit } from "./concurrency.js";
4
5
  import { createError, ERROR_CODES } from "./errors.js";
6
+ const WORKSPACE_FILE_READ_CONCURRENCY = 4;
5
7
  function detectMappingsFromContent(content) {
6
8
  const detections = [];
7
9
  if (/officialMojangMappings\s*\(/i.test(content)) {
@@ -36,6 +38,46 @@ function detectMappingsFromContent(content) {
36
38
  }
37
39
  return detections;
38
40
  }
41
+ function detectLoadersFromContent(content) {
42
+ const detections = [];
43
+ if (/\bid\s*(?:\(\s*)?["']net\.neoforged\.moddev["']\s*\)?/i.test(content)) {
44
+ detections.push({
45
+ loader: "neoforge",
46
+ reason: "net.neoforged.moddev plugin"
47
+ });
48
+ }
49
+ if (/\bid\s*(?:\(\s*)?["']net\.minecraftforge\.gradle["']\s*\)?/i.test(content)) {
50
+ detections.push({
51
+ loader: "forge",
52
+ reason: "net.minecraftforge.gradle plugin"
53
+ });
54
+ }
55
+ if (/\bid\s*(?:\(\s*)?["']org\.quiltmc\.loom["']\s*\)?/i.test(content)) {
56
+ detections.push({
57
+ loader: "quilt",
58
+ reason: "org.quiltmc.loom plugin"
59
+ });
60
+ }
61
+ if (/\bid\s*(?:\(\s*)?["']fabric-loom["']\s*\)?/i.test(content) || /\bid\s*(?:\(\s*)?["']dev\.architectury\.loom["']\s*\)?/i.test(content)) {
62
+ detections.push({
63
+ loader: "fabric",
64
+ reason: "fabric/dev.architectury loom plugin"
65
+ });
66
+ }
67
+ if (/\bminecraft\s*\{[\s\S]*?\baccessTransformer\b/i.test(content)) {
68
+ detections.push({
69
+ loader: "forge",
70
+ reason: "minecraft { accessTransformer ... } block"
71
+ });
72
+ }
73
+ if (/\bneoForge\s*\{[\s\S]*?\baccessTransformers\b/i.test(content)) {
74
+ detections.push({
75
+ loader: "neoforge",
76
+ reason: "neoForge { accessTransformers ... } block"
77
+ });
78
+ }
79
+ return detections;
80
+ }
39
81
  export class WorkspaceMappingService {
40
82
  async detectCompileMapping(input) {
41
83
  const projectPath = input.projectPath?.trim();
@@ -49,30 +91,26 @@ export class WorkspaceMappingService {
49
91
  });
50
92
  }
51
93
  const root = resolve(projectPath);
52
- const files = fastGlob.sync(["build.gradle", "build.gradle.kts", "**/build.gradle", "**/build.gradle.kts"], {
94
+ const files = (await fastGlob.glob(["build.gradle", "build.gradle.kts", "**/build.gradle", "**/build.gradle.kts"], {
53
95
  cwd: root,
54
96
  absolute: true,
55
97
  onlyFiles: true,
56
98
  ignore: ["**/.git/**", "**/.gradle/**", "**/build/**", "**/out/**", "**/node_modules/**"]
57
- });
58
- const evidence = [];
59
- for (const filePath of files.sort((left, right) => left.localeCompare(right))) {
99
+ })).sort((left, right) => left.localeCompare(right));
100
+ const evidence = (await mapWithConcurrencyLimit(files, WORKSPACE_FILE_READ_CONCURRENCY, async (filePath) => {
60
101
  let content;
61
102
  try {
62
103
  content = await readFile(filePath, "utf8");
63
104
  }
64
105
  catch {
65
- continue;
106
+ return [];
66
107
  }
67
- const detections = detectMappingsFromContent(content);
68
- for (const detection of detections) {
69
- evidence.push({
70
- filePath,
71
- mapping: detection.mapping,
72
- reason: detection.reason
73
- });
74
- }
75
- }
108
+ return detectMappingsFromContent(content).map((detection) => ({
109
+ filePath,
110
+ mapping: detection.mapping,
111
+ reason: detection.reason
112
+ }));
113
+ })).flat();
76
114
  if (evidence.length === 0) {
77
115
  return {
78
116
  resolved: false,
@@ -121,5 +159,99 @@ export class WorkspaceMappingService {
121
159
  }
122
160
  return undefined;
123
161
  }
162
+ async detectProjectLoader(projectPath) {
163
+ const root = resolve(projectPath);
164
+ const buildFiles = (await fastGlob.glob(["build.gradle", "build.gradle.kts", "**/build.gradle", "**/build.gradle.kts"], {
165
+ cwd: root,
166
+ absolute: true,
167
+ onlyFiles: true,
168
+ ignore: ["**/.git/**", "**/.gradle/**", "**/build/**", "**/out/**", "**/node_modules/**"]
169
+ })).sort((left, right) => left.localeCompare(right));
170
+ const descriptorFiles = (await fastGlob.glob([
171
+ "fabric.mod.json",
172
+ "quilt.mod.json",
173
+ "META-INF/mods.toml",
174
+ "META-INF/neoforge.mods.toml",
175
+ "**/fabric.mod.json",
176
+ "**/quilt.mod.json",
177
+ "**/META-INF/mods.toml",
178
+ "**/META-INF/neoforge.mods.toml"
179
+ ], {
180
+ cwd: root,
181
+ absolute: true,
182
+ onlyFiles: true,
183
+ ignore: ["**/.git/**", "**/.gradle/**", "**/build/**", "**/out/**", "**/node_modules/**"]
184
+ })).sort((left, right) => left.localeCompare(right));
185
+ const evidence = (await mapWithConcurrencyLimit(buildFiles, WORKSPACE_FILE_READ_CONCURRENCY, async (filePath) => {
186
+ let content;
187
+ try {
188
+ content = await readFile(filePath, "utf8");
189
+ }
190
+ catch {
191
+ return [];
192
+ }
193
+ return detectLoadersFromContent(content).map((detection) => ({
194
+ filePath,
195
+ loader: detection.loader,
196
+ reason: detection.reason
197
+ }));
198
+ })).flat();
199
+ for (const descriptorPath of descriptorFiles) {
200
+ const normalized = descriptorPath.replaceAll("\\", "/");
201
+ if (normalized.endsWith("fabric.mod.json")) {
202
+ evidence.push({
203
+ filePath: descriptorPath,
204
+ loader: "fabric",
205
+ reason: "fabric.mod.json"
206
+ });
207
+ }
208
+ else if (normalized.endsWith("quilt.mod.json")) {
209
+ evidence.push({
210
+ filePath: descriptorPath,
211
+ loader: "quilt",
212
+ reason: "quilt.mod.json"
213
+ });
214
+ }
215
+ else if (normalized.endsWith("META-INF/neoforge.mods.toml")) {
216
+ evidence.push({
217
+ filePath: descriptorPath,
218
+ loader: "neoforge",
219
+ reason: "META-INF/neoforge.mods.toml"
220
+ });
221
+ }
222
+ else if (normalized.endsWith("META-INF/mods.toml")) {
223
+ evidence.push({
224
+ filePath: descriptorPath,
225
+ loader: "forge",
226
+ reason: "META-INF/mods.toml"
227
+ });
228
+ }
229
+ }
230
+ if (evidence.length === 0) {
231
+ return {
232
+ resolved: false,
233
+ evidence,
234
+ warnings: ["No workspace loader declaration was detected from build.gradle(.kts) files or mod descriptors."]
235
+ };
236
+ }
237
+ const loaderSet = new Set(evidence
238
+ .map((entry) => entry.loader)
239
+ .filter((loader) => loader !== "unknown"));
240
+ if (loaderSet.size !== 1) {
241
+ return {
242
+ resolved: false,
243
+ evidence,
244
+ warnings: [
245
+ `Multiple or ambiguous workspace loaders were detected: ${[...loaderSet].join(", ") || "unknown"}.`
246
+ ]
247
+ };
248
+ }
249
+ return {
250
+ resolved: true,
251
+ loader: [...loaderSet][0],
252
+ evidence,
253
+ warnings: []
254
+ };
255
+ }
124
256
  }
125
257
  //# sourceMappingURL=workspace-mapping-service.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhisang/minecraft-modding-mcp",
3
- "version": "3.1.1",
3
+ "version": "4.0.0",
4
4
  "description": "MCP server with utilities for Minecraft modding workflows",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -23,6 +23,8 @@
23
23
  "start": "node dist/cli.js",
24
24
  "check": "tsc --noEmit -p tsconfig.json",
25
25
  "test": "node --test --import tsx tests/*.test.ts",
26
+ "test:file": "node --test --import tsx",
27
+ "test:grep": "node --test --import tsx --test-name-pattern",
26
28
  "test:coverage": "node --test --import tsx --experimental-test-coverage --test-coverage-lines=80 --test-coverage-branches=70 --test-coverage-functions=80 tests/*.test.ts",
27
29
  "test:coverage:lcov": "node --input-type=module -e \"import { mkdirSync } from 'node:fs'; mkdirSync('coverage', { recursive: true });\" && node --test --import tsx --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=coverage/lcov.info --test-coverage-lines=80 --test-coverage-branches=70 --test-coverage-functions=80 tests/*.test.ts",
28
30
  "test:perf": "node --test --test-concurrency=1 --import tsx tests/perf/*.perf.ts",