@adhisang/minecraft-modding-mcp 1.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 (106) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +765 -0
  4. package/dist/access-widener-parser.d.ts +24 -0
  5. package/dist/access-widener-parser.js +77 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.js +4 -0
  8. package/dist/config.d.ts +27 -0
  9. package/dist/config.js +178 -0
  10. package/dist/decompiler/vineflower.d.ts +15 -0
  11. package/dist/decompiler/vineflower.js +185 -0
  12. package/dist/errors.d.ts +50 -0
  13. package/dist/errors.js +49 -0
  14. package/dist/hash.d.ts +1 -0
  15. package/dist/hash.js +12 -0
  16. package/dist/index.d.ts +7 -0
  17. package/dist/index.js +1447 -0
  18. package/dist/java-process.d.ts +16 -0
  19. package/dist/java-process.js +120 -0
  20. package/dist/logger.d.ts +3 -0
  21. package/dist/logger.js +21 -0
  22. package/dist/mapping-pipeline-service.d.ts +18 -0
  23. package/dist/mapping-pipeline-service.js +60 -0
  24. package/dist/mapping-service.d.ts +161 -0
  25. package/dist/mapping-service.js +1706 -0
  26. package/dist/maven-resolver.d.ts +22 -0
  27. package/dist/maven-resolver.js +122 -0
  28. package/dist/minecraft-explorer-service.d.ts +43 -0
  29. package/dist/minecraft-explorer-service.js +562 -0
  30. package/dist/mixin-parser.d.ts +34 -0
  31. package/dist/mixin-parser.js +194 -0
  32. package/dist/mixin-validator.d.ts +59 -0
  33. package/dist/mixin-validator.js +274 -0
  34. package/dist/mod-analyzer.d.ts +23 -0
  35. package/dist/mod-analyzer.js +346 -0
  36. package/dist/mod-decompile-service.d.ts +39 -0
  37. package/dist/mod-decompile-service.js +136 -0
  38. package/dist/mod-remap-service.d.ts +17 -0
  39. package/dist/mod-remap-service.js +186 -0
  40. package/dist/mod-search-service.d.ts +28 -0
  41. package/dist/mod-search-service.js +174 -0
  42. package/dist/mojang-tiny-mapping-service.d.ts +13 -0
  43. package/dist/mojang-tiny-mapping-service.js +351 -0
  44. package/dist/nbt/java-nbt-codec.d.ts +3 -0
  45. package/dist/nbt/java-nbt-codec.js +385 -0
  46. package/dist/nbt/json-patch.d.ts +3 -0
  47. package/dist/nbt/json-patch.js +352 -0
  48. package/dist/nbt/pipeline.d.ts +39 -0
  49. package/dist/nbt/pipeline.js +173 -0
  50. package/dist/nbt/typed-json.d.ts +10 -0
  51. package/dist/nbt/typed-json.js +205 -0
  52. package/dist/nbt/types.d.ts +66 -0
  53. package/dist/nbt/types.js +2 -0
  54. package/dist/observability.d.ts +88 -0
  55. package/dist/observability.js +165 -0
  56. package/dist/path-converter.d.ts +12 -0
  57. package/dist/path-converter.js +161 -0
  58. package/dist/path-resolver.d.ts +19 -0
  59. package/dist/path-resolver.js +78 -0
  60. package/dist/registry-service.d.ts +29 -0
  61. package/dist/registry-service.js +214 -0
  62. package/dist/repo-downloader.d.ts +15 -0
  63. package/dist/repo-downloader.js +111 -0
  64. package/dist/resources.d.ts +3 -0
  65. package/dist/resources.js +154 -0
  66. package/dist/search-hit-accumulator.d.ts +38 -0
  67. package/dist/search-hit-accumulator.js +153 -0
  68. package/dist/source-jar-reader.d.ts +13 -0
  69. package/dist/source-jar-reader.js +216 -0
  70. package/dist/source-resolver.d.ts +14 -0
  71. package/dist/source-resolver.js +274 -0
  72. package/dist/source-service.d.ts +404 -0
  73. package/dist/source-service.js +2881 -0
  74. package/dist/storage/artifacts-repo.d.ts +45 -0
  75. package/dist/storage/artifacts-repo.js +209 -0
  76. package/dist/storage/db.d.ts +14 -0
  77. package/dist/storage/db.js +132 -0
  78. package/dist/storage/files-repo.d.ts +78 -0
  79. package/dist/storage/files-repo.js +437 -0
  80. package/dist/storage/index-meta-repo.d.ts +35 -0
  81. package/dist/storage/index-meta-repo.js +97 -0
  82. package/dist/storage/migrations.d.ts +11 -0
  83. package/dist/storage/migrations.js +71 -0
  84. package/dist/storage/schema.d.ts +1 -0
  85. package/dist/storage/schema.js +160 -0
  86. package/dist/storage/sqlite.d.ts +20 -0
  87. package/dist/storage/sqlite.js +111 -0
  88. package/dist/storage/symbols-repo.d.ts +63 -0
  89. package/dist/storage/symbols-repo.js +401 -0
  90. package/dist/symbols/symbol-extractor.d.ts +7 -0
  91. package/dist/symbols/symbol-extractor.js +64 -0
  92. package/dist/tiny-remapper-resolver.d.ts +1 -0
  93. package/dist/tiny-remapper-resolver.js +62 -0
  94. package/dist/tiny-remapper-service.d.ts +16 -0
  95. package/dist/tiny-remapper-service.js +73 -0
  96. package/dist/types.d.ts +120 -0
  97. package/dist/types.js +2 -0
  98. package/dist/version-diff-service.d.ts +41 -0
  99. package/dist/version-diff-service.js +222 -0
  100. package/dist/version-service.d.ts +70 -0
  101. package/dist/version-service.js +411 -0
  102. package/dist/vineflower-resolver.d.ts +1 -0
  103. package/dist/vineflower-resolver.js +62 -0
  104. package/dist/workspace-mapping-service.d.ts +18 -0
  105. package/dist/workspace-mapping-service.js +89 -0
  106. package/package.json +61 -0
@@ -0,0 +1,411 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { createError, ERROR_CODES } from "./errors.js";
5
+ import { computeFileSha1 } from "./hash.js";
6
+ import { defaultDownloadPath, downloadToCache } from "./repo-downloader.js";
7
+ const DEFAULT_VERSION_MANIFEST_URL = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json";
8
+ const MANIFEST_CACHE_TTL_MS = 60 * 60 * 1000;
9
+ const VERSION_DETAIL_CACHE_TTL_MS = 60 * 60 * 1000;
10
+ function clampLimit(limit, fallback, max) {
11
+ if (!Number.isFinite(limit) || limit == null) {
12
+ return fallback;
13
+ }
14
+ return Math.max(1, Math.min(max, Math.trunc(limit)));
15
+ }
16
+ function isVersionManifest(value) {
17
+ return typeof value === "object" && value !== null;
18
+ }
19
+ function ensureVersionDetail(value, version) {
20
+ if (typeof value !== "object" || value == null) {
21
+ throw createError({
22
+ code: ERROR_CODES.VERSION_NOT_FOUND,
23
+ message: `Version metadata for "${version}" is invalid.`,
24
+ details: { version }
25
+ });
26
+ }
27
+ return value;
28
+ }
29
+ export class VersionService {
30
+ config;
31
+ fetchFn;
32
+ manifestUrl;
33
+ manifestCache;
34
+ versionDetailCache = new Map();
35
+ resolveLocks = new Map();
36
+ constructor(config, fetchFn = globalThis.fetch) {
37
+ this.config = config;
38
+ this.fetchFn = fetchFn;
39
+ this.manifestUrl = process.env.MCP_VERSION_MANIFEST_URL ?? DEFAULT_VERSION_MANIFEST_URL;
40
+ }
41
+ async listVersions(input = {}) {
42
+ const manifest = await this.fetchManifest();
43
+ const includeSnapshots = input.includeSnapshots ?? false;
44
+ const limit = clampLimit(input.limit, 20, 200);
45
+ const versions = manifest.versions ?? [];
46
+ const releases = versions
47
+ .filter((entry) => entry.type === "release")
48
+ .map((entry) => ({ id: entry.id, unobfuscated: isUnobfuscatedVersion(entry.id) }))
49
+ .slice(0, limit);
50
+ const snapshots = versions
51
+ .filter((entry) => entry.type === "snapshot")
52
+ .map((entry) => ({ id: entry.id, unobfuscated: isUnobfuscatedVersion(entry.id) }))
53
+ .slice(0, limit);
54
+ const cached = (await this.loadCacheIndex()).entries.map((entry) => entry.version);
55
+ return {
56
+ latest: {
57
+ release: manifest.latest?.release,
58
+ snapshot: manifest.latest?.snapshot
59
+ },
60
+ releases,
61
+ snapshots: includeSnapshots ? snapshots : undefined,
62
+ cached: Array.from(new Set(cached)).sort((a, b) => a.localeCompare(b)),
63
+ totalAvailable: versions.length
64
+ };
65
+ }
66
+ async listVersionIds(input = {}) {
67
+ const manifest = await this.fetchManifest();
68
+ const includeSnapshots = input.includeSnapshots ?? false;
69
+ return (manifest.versions ?? [])
70
+ .filter((entry) => entry.type === "release" || (includeSnapshots && entry.type === "snapshot"))
71
+ .map((entry) => entry.id);
72
+ }
73
+ async resolveVersionJar(version) {
74
+ const normalizedVersion = version.trim();
75
+ if (!normalizedVersion) {
76
+ throw createError({
77
+ code: ERROR_CODES.INVALID_INPUT,
78
+ message: "version must be non-empty."
79
+ });
80
+ }
81
+ const existingLock = this.resolveLocks.get(normalizedVersion);
82
+ if (existingLock) {
83
+ return existingLock;
84
+ }
85
+ const resolvePromise = this.resolveVersionJarInternal(normalizedVersion);
86
+ this.resolveLocks.set(normalizedVersion, resolvePromise);
87
+ try {
88
+ return await resolvePromise;
89
+ }
90
+ finally {
91
+ this.resolveLocks.delete(normalizedVersion);
92
+ }
93
+ }
94
+ async resolveVersionMappings(version) {
95
+ const normalizedVersion = version.trim();
96
+ if (!normalizedVersion) {
97
+ throw createError({
98
+ code: ERROR_CODES.INVALID_INPUT,
99
+ message: "version must be non-empty."
100
+ });
101
+ }
102
+ const manifest = await this.fetchManifest();
103
+ const versionEntry = (manifest.versions ?? []).find((entry) => entry.id === normalizedVersion);
104
+ if (!versionEntry) {
105
+ throw createError({
106
+ code: ERROR_CODES.VERSION_NOT_FOUND,
107
+ message: `Minecraft version "${normalizedVersion}" was not found in version manifest.`,
108
+ details: { version: normalizedVersion }
109
+ });
110
+ }
111
+ const details = await this.fetchVersionDetails(versionEntry.url, normalizedVersion);
112
+ const clientMappingsUrl = details.downloads?.client_mappings?.url;
113
+ return {
114
+ version: normalizedVersion,
115
+ versionManifestUrl: this.manifestUrl,
116
+ versionDetailUrl: versionEntry.url,
117
+ clientMappingsUrl,
118
+ serverMappingsUrl: details.downloads?.server_mappings?.url,
119
+ mappingsUrl: clientMappingsUrl
120
+ };
121
+ }
122
+ async resolveServerJar(version) {
123
+ const normalizedVersion = version.trim();
124
+ if (!normalizedVersion) {
125
+ throw createError({
126
+ code: ERROR_CODES.INVALID_INPUT,
127
+ message: "version must be non-empty."
128
+ });
129
+ }
130
+ const manifest = await this.fetchManifest();
131
+ const versionEntry = (manifest.versions ?? []).find((entry) => entry.id === normalizedVersion);
132
+ if (!versionEntry) {
133
+ throw createError({
134
+ code: ERROR_CODES.VERSION_NOT_FOUND,
135
+ message: `Minecraft version "${normalizedVersion}" was not found in version manifest.`,
136
+ details: { version: normalizedVersion }
137
+ });
138
+ }
139
+ const details = await this.fetchVersionDetails(versionEntry.url, normalizedVersion);
140
+ const serverJarUrl = details.downloads?.server?.url;
141
+ if (!serverJarUrl) {
142
+ throw createError({
143
+ code: ERROR_CODES.VERSION_NOT_FOUND,
144
+ message: `Minecraft version "${normalizedVersion}" does not expose a server download URL.`,
145
+ details: { version: normalizedVersion }
146
+ });
147
+ }
148
+ const destinationPath = defaultDownloadPath(this.config.cacheDir, serverJarUrl);
149
+ if (existsSync(destinationPath)) {
150
+ return {
151
+ version: normalizedVersion,
152
+ jarPath: destinationPath,
153
+ source: "downloaded",
154
+ serverJarUrl
155
+ };
156
+ }
157
+ const downloaded = await downloadToCache(serverJarUrl, destinationPath, {
158
+ retries: this.config.fetchRetries,
159
+ timeoutMs: this.config.fetchTimeoutMs,
160
+ fetchFn: this.fetchFn
161
+ });
162
+ if (!downloaded.ok || !downloaded.path) {
163
+ throw createError({
164
+ code: ERROR_CODES.REPO_FETCH_FAILED,
165
+ message: `Failed to download Minecraft server jar for version "${normalizedVersion}".`,
166
+ details: {
167
+ version: normalizedVersion,
168
+ url: serverJarUrl,
169
+ statusCode: downloaded.statusCode
170
+ }
171
+ });
172
+ }
173
+ const expectedSha1 = details.downloads?.server?.sha1;
174
+ if (expectedSha1) {
175
+ const actualSha1 = await computeFileSha1(downloaded.path);
176
+ if (actualSha1 !== expectedSha1) {
177
+ await unlink(downloaded.path).catch(() => { });
178
+ throw createError({
179
+ code: ERROR_CODES.REPO_FETCH_FAILED,
180
+ message: `SHA-1 mismatch for downloaded server jar of version "${normalizedVersion}".`,
181
+ details: {
182
+ version: normalizedVersion,
183
+ url: serverJarUrl,
184
+ expected: expectedSha1,
185
+ actual: actualSha1
186
+ }
187
+ });
188
+ }
189
+ }
190
+ return {
191
+ version: normalizedVersion,
192
+ jarPath: downloaded.path,
193
+ source: "downloaded",
194
+ serverJarUrl
195
+ };
196
+ }
197
+ async fetchManifest() {
198
+ const now = Date.now();
199
+ if (this.manifestCache && this.manifestCache.expiresAt > now) {
200
+ return this.manifestCache.value;
201
+ }
202
+ const manifest = await this.fetchJson(this.manifestUrl);
203
+ if (!isVersionManifest(manifest)) {
204
+ throw createError({
205
+ code: ERROR_CODES.REPO_FETCH_FAILED,
206
+ message: "Minecraft version manifest response is invalid.",
207
+ details: { manifestUrl: this.manifestUrl }
208
+ });
209
+ }
210
+ this.manifestCache = {
211
+ value: manifest,
212
+ expiresAt: now + MANIFEST_CACHE_TTL_MS
213
+ };
214
+ return manifest;
215
+ }
216
+ async resolveVersionJarInternal(normalizedVersion) {
217
+ const cachedIndex = await this.loadCacheIndex();
218
+ const cachedEntry = cachedIndex.entries.find((entry) => entry.version === normalizedVersion);
219
+ if (cachedEntry && existsSync(cachedEntry.jarPath)) {
220
+ return {
221
+ version: normalizedVersion,
222
+ jarPath: cachedEntry.jarPath,
223
+ source: "downloaded",
224
+ clientJarUrl: "cache:index"
225
+ };
226
+ }
227
+ const manifest = await this.fetchManifest();
228
+ const versionEntry = (manifest.versions ?? []).find((entry) => entry.id === normalizedVersion);
229
+ if (!versionEntry) {
230
+ throw createError({
231
+ code: ERROR_CODES.VERSION_NOT_FOUND,
232
+ message: `Minecraft version "${normalizedVersion}" was not found in version manifest.`,
233
+ details: { version: normalizedVersion }
234
+ });
235
+ }
236
+ const details = await this.fetchVersionDetails(versionEntry.url, normalizedVersion);
237
+ const clientJarUrl = details.downloads?.client?.url;
238
+ if (!clientJarUrl) {
239
+ throw createError({
240
+ code: ERROR_CODES.VERSION_NOT_FOUND,
241
+ message: `Minecraft version "${normalizedVersion}" does not expose a client download URL.`,
242
+ details: { version: normalizedVersion }
243
+ });
244
+ }
245
+ const destinationPath = defaultDownloadPath(this.config.cacheDir, clientJarUrl);
246
+ if (existsSync(destinationPath)) {
247
+ await this.recordCacheEntry({
248
+ version: normalizedVersion,
249
+ jarPath: destinationPath,
250
+ downloadedAt: new Date().toISOString()
251
+ });
252
+ return {
253
+ version: normalizedVersion,
254
+ jarPath: destinationPath,
255
+ source: "downloaded",
256
+ clientJarUrl
257
+ };
258
+ }
259
+ const downloaded = await downloadToCache(clientJarUrl, destinationPath, {
260
+ retries: this.config.fetchRetries,
261
+ timeoutMs: this.config.fetchTimeoutMs,
262
+ fetchFn: this.fetchFn
263
+ });
264
+ if (!downloaded.ok || !downloaded.path) {
265
+ throw createError({
266
+ code: ERROR_CODES.REPO_FETCH_FAILED,
267
+ message: `Failed to download Minecraft client jar for version "${normalizedVersion}".`,
268
+ details: {
269
+ version: normalizedVersion,
270
+ url: clientJarUrl,
271
+ statusCode: downloaded.statusCode
272
+ }
273
+ });
274
+ }
275
+ const expectedSha1 = details.downloads?.client?.sha1;
276
+ if (expectedSha1) {
277
+ const actualSha1 = await computeFileSha1(downloaded.path);
278
+ if (actualSha1 !== expectedSha1) {
279
+ await unlink(downloaded.path).catch(() => { });
280
+ throw createError({
281
+ code: ERROR_CODES.REPO_FETCH_FAILED,
282
+ message: `SHA-1 mismatch for downloaded jar of version "${normalizedVersion}".`,
283
+ details: {
284
+ version: normalizedVersion,
285
+ url: clientJarUrl,
286
+ expected: expectedSha1,
287
+ actual: actualSha1
288
+ }
289
+ });
290
+ }
291
+ }
292
+ await this.recordCacheEntry({
293
+ version: normalizedVersion,
294
+ jarPath: downloaded.path,
295
+ downloadedAt: new Date().toISOString()
296
+ });
297
+ return {
298
+ version: normalizedVersion,
299
+ jarPath: downloaded.path,
300
+ source: "downloaded",
301
+ clientJarUrl
302
+ };
303
+ }
304
+ async fetchVersionDetails(versionUrl, version) {
305
+ const now = Date.now();
306
+ const cached = this.versionDetailCache.get(versionUrl);
307
+ if (cached && cached.expiresAt > now) {
308
+ this.versionDetailCache.delete(versionUrl);
309
+ this.versionDetailCache.set(versionUrl, cached);
310
+ return cached.value;
311
+ }
312
+ const detailRaw = await this.fetchJson(versionUrl);
313
+ const details = ensureVersionDetail(detailRaw, version);
314
+ this.versionDetailCache.set(versionUrl, {
315
+ value: details,
316
+ expiresAt: now + VERSION_DETAIL_CACHE_TTL_MS
317
+ });
318
+ this.trimVersionDetailCache();
319
+ return details;
320
+ }
321
+ async fetchJson(url) {
322
+ const response = await this.fetchFn(url);
323
+ if (!response.ok) {
324
+ throw createError({
325
+ code: ERROR_CODES.REPO_FETCH_FAILED,
326
+ message: `Request failed for "${url}" with status ${response.status}.`,
327
+ details: {
328
+ url,
329
+ statusCode: response.status
330
+ }
331
+ });
332
+ }
333
+ try {
334
+ return await response.json();
335
+ }
336
+ catch {
337
+ throw createError({
338
+ code: ERROR_CODES.REPO_FETCH_FAILED,
339
+ message: `Response from "${url}" is not valid JSON.`,
340
+ details: { url }
341
+ });
342
+ }
343
+ }
344
+ cacheIndexPath() {
345
+ return join(this.config.cacheDir, "versions", "index.json");
346
+ }
347
+ async loadCacheIndex() {
348
+ const indexPath = this.cacheIndexPath();
349
+ try {
350
+ const raw = await readFile(indexPath, "utf8");
351
+ const parsed = JSON.parse(raw);
352
+ if (!Array.isArray(parsed.entries)) {
353
+ return { entries: [] };
354
+ }
355
+ return {
356
+ entries: parsed.entries.filter((entry) => typeof entry === "object" &&
357
+ entry != null &&
358
+ typeof entry.version === "string" &&
359
+ typeof entry.jarPath === "string" &&
360
+ typeof entry.downloadedAt === "string")
361
+ };
362
+ }
363
+ catch {
364
+ return { entries: [] };
365
+ }
366
+ }
367
+ async recordCacheEntry(entry) {
368
+ const indexPath = this.cacheIndexPath();
369
+ const existing = await this.loadCacheIndex();
370
+ const deduped = existing.entries.filter((candidate) => candidate.version !== entry.version);
371
+ deduped.push(entry);
372
+ deduped.sort((left, right) => right.downloadedAt.localeCompare(left.downloadedAt));
373
+ await mkdir(dirname(indexPath), { recursive: true });
374
+ await writeFile(indexPath, `${JSON.stringify({
375
+ entries: deduped
376
+ }, null, 2)}\n`, "utf8");
377
+ }
378
+ trimVersionDetailCache() {
379
+ const maxEntries = Math.max(1, this.config.maxVersionDetailCache ?? 256);
380
+ while (this.versionDetailCache.size > maxEntries) {
381
+ const oldest = this.versionDetailCache.keys().next().value;
382
+ if (!oldest) {
383
+ return;
384
+ }
385
+ this.versionDetailCache.delete(oldest);
386
+ }
387
+ }
388
+ }
389
+ /**
390
+ * MC 26.1+ uses new YY.N version format and ships unobfuscated source.
391
+ * Legacy 1.x.y versions remain obfuscated.
392
+ * Snapshots: "26w01a" (year >= 26) → unobfuscated, "24w01a" → obfuscated.
393
+ */
394
+ export function isUnobfuscatedVersion(version) {
395
+ if (!version)
396
+ return false;
397
+ // Snapshot format: YYwNNa (e.g. "26w01a")
398
+ const snapshotMatch = version.match(/^(\d{2})w\d{2}[a-z]$/);
399
+ if (snapshotMatch) {
400
+ return Number(snapshotMatch[1]) >= 26;
401
+ }
402
+ // New format: YY.N or YY.N.P, optionally with -preN/-rcN suffix.
403
+ // Examples: "26.1", "27.3.1", "26.1-pre1", "26.1-rc1"
404
+ const newFormatMatch = version.match(/^(\d{2,})\.\d+(?:\.\d+)?(?:-(?:pre|rc)\d+)?$/);
405
+ if (newFormatMatch) {
406
+ return Number(newFormatMatch[1]) >= 26;
407
+ }
408
+ // Legacy 1.x.y → obfuscated; unknown → false (safe default)
409
+ return false;
410
+ }
411
+ //# sourceMappingURL=version-service.js.map
@@ -0,0 +1 @@
1
+ export declare function resolveVineflowerJar(cacheDir: string, overridePath: string | undefined, fetchFn?: typeof fetch): Promise<string>;
@@ -0,0 +1,62 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { createError, ERROR_CODES } from "./errors.js";
4
+ import { log } from "./logger.js";
5
+ import { downloadToCache } from "./repo-downloader.js";
6
+ const DEFAULT_VERSION = "1.11.2";
7
+ const DOWNLOAD_TIMEOUT_MS = 120_000;
8
+ const inflightDownloads = new Map();
9
+ function vineflowerCachePath(cacheDir, version) {
10
+ return join(cacheDir, "resources", `vineflower-${version}.jar`);
11
+ }
12
+ function vineflowerDownloadUrl(version) {
13
+ return `https://github.com/Vineflower/vineflower/releases/download/${version}/vineflower-${version}.jar`;
14
+ }
15
+ export async function resolveVineflowerJar(cacheDir, overridePath, fetchFn) {
16
+ // 1. Environment / config override
17
+ if (overridePath) {
18
+ return overridePath;
19
+ }
20
+ const version = process.env.MCP_VINEFLOWER_VERSION ?? DEFAULT_VERSION;
21
+ const cached = vineflowerCachePath(cacheDir, version);
22
+ // 2. Already cached
23
+ if (existsSync(cached)) {
24
+ return cached;
25
+ }
26
+ // 3. Download with per-target lock
27
+ const inflight = inflightDownloads.get(cached);
28
+ if (inflight) {
29
+ return inflight;
30
+ }
31
+ const downloadPromise = downloadVineflower(version, cached, fetchFn);
32
+ inflightDownloads.set(cached, downloadPromise);
33
+ try {
34
+ return await downloadPromise;
35
+ }
36
+ finally {
37
+ inflightDownloads.delete(cached);
38
+ }
39
+ }
40
+ async function downloadVineflower(version, destination, fetchFn) {
41
+ const url = vineflowerDownloadUrl(version);
42
+ log("info", "vineflower.download.start", { version, url });
43
+ const result = await downloadToCache(url, destination, {
44
+ timeoutMs: DOWNLOAD_TIMEOUT_MS,
45
+ retries: 2,
46
+ ...(fetchFn ? { fetchFn } : {})
47
+ });
48
+ if (!result.ok || !result.path) {
49
+ throw createError({
50
+ code: ERROR_CODES.DECOMPILER_UNAVAILABLE,
51
+ message: `Failed to download Vineflower ${version} from GitHub.`,
52
+ details: { version, url, statusCode: result.statusCode }
53
+ });
54
+ }
55
+ log("info", "vineflower.download.done", {
56
+ version,
57
+ path: result.path,
58
+ contentLength: result.contentLength
59
+ });
60
+ return result.path;
61
+ }
62
+ //# sourceMappingURL=vineflower-resolver.js.map
@@ -0,0 +1,18 @@
1
+ import type { SourceMapping } from "./types.js";
2
+ export type WorkspaceCompileMappingInput = {
3
+ projectPath: string;
4
+ };
5
+ export type WorkspaceMappingEvidence = {
6
+ filePath: string;
7
+ mapping: SourceMapping;
8
+ reason: string;
9
+ };
10
+ export type WorkspaceCompileMappingOutput = {
11
+ resolved: boolean;
12
+ mappingApplied?: SourceMapping;
13
+ evidence: WorkspaceMappingEvidence[];
14
+ warnings: string[];
15
+ };
16
+ export declare class WorkspaceMappingService {
17
+ detectCompileMapping(input: WorkspaceCompileMappingInput): Promise<WorkspaceCompileMappingOutput>;
18
+ }
@@ -0,0 +1,89 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import fastGlob from "fast-glob";
4
+ import { createError, ERROR_CODES } from "./errors.js";
5
+ function detectMappingsFromContent(content) {
6
+ const detections = [];
7
+ if (/officialMojangMappings\s*\(/i.test(content)) {
8
+ detections.push({
9
+ mapping: "mojang",
10
+ reason: "officialMojangMappings()"
11
+ });
12
+ }
13
+ if (/\bmappings\s*(?:\(|)\s*["']net\.fabricmc:yarn:/i.test(content)) {
14
+ detections.push({
15
+ mapping: "yarn",
16
+ reason: "mappings net.fabricmc:yarn"
17
+ });
18
+ }
19
+ if (/\bmappings\s*(?:\(|)\s*["']net\.fabricmc:intermediary:/i.test(content)) {
20
+ detections.push({
21
+ mapping: "intermediary",
22
+ reason: "mappings net.fabricmc:intermediary"
23
+ });
24
+ }
25
+ return detections;
26
+ }
27
+ export class WorkspaceMappingService {
28
+ async detectCompileMapping(input) {
29
+ const projectPath = input.projectPath?.trim();
30
+ if (!projectPath) {
31
+ throw createError({
32
+ code: ERROR_CODES.INVALID_INPUT,
33
+ message: "projectPath must be a non-empty string.",
34
+ details: {
35
+ projectPath: input.projectPath
36
+ }
37
+ });
38
+ }
39
+ const root = resolve(projectPath);
40
+ const files = fastGlob.sync(["build.gradle", "build.gradle.kts", "**/build.gradle", "**/build.gradle.kts"], {
41
+ cwd: root,
42
+ absolute: true,
43
+ onlyFiles: true,
44
+ ignore: ["**/.git/**", "**/.gradle/**", "**/build/**", "**/out/**", "**/node_modules/**"]
45
+ });
46
+ const evidence = [];
47
+ for (const filePath of files.sort((left, right) => left.localeCompare(right))) {
48
+ let content;
49
+ try {
50
+ content = await readFile(filePath, "utf8");
51
+ }
52
+ catch {
53
+ continue;
54
+ }
55
+ const detections = detectMappingsFromContent(content);
56
+ for (const detection of detections) {
57
+ evidence.push({
58
+ filePath,
59
+ mapping: detection.mapping,
60
+ reason: detection.reason
61
+ });
62
+ }
63
+ }
64
+ if (evidence.length === 0) {
65
+ return {
66
+ resolved: false,
67
+ evidence,
68
+ warnings: ["No compile-time mapping declaration was detected in build.gradle(.kts) files."]
69
+ };
70
+ }
71
+ const mappingSet = new Set(evidence.map((entry) => entry.mapping));
72
+ if (mappingSet.size > 1) {
73
+ return {
74
+ resolved: false,
75
+ evidence,
76
+ warnings: [
77
+ `Multiple compile mappings were detected across the workspace: ${[...mappingSet].join(", ")}.`
78
+ ]
79
+ };
80
+ }
81
+ return {
82
+ resolved: true,
83
+ mappingApplied: evidence[0].mapping,
84
+ evidence,
85
+ warnings: []
86
+ };
87
+ }
88
+ }
89
+ //# sourceMappingURL=workspace-mapping-service.js.map
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@adhisang/minecraft-modding-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server with utilities for Minecraft modding workflows",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "minecraft-modding-mcp": "dist/cli.js"
10
+ },
11
+ "files": [
12
+ "dist/**/*.js",
13
+ "dist/**/*.d.ts",
14
+ "README.md",
15
+ "LICENSE",
16
+ "CHANGELOG.md"
17
+ ],
18
+ "scripts": {
19
+ "dev": "tsx src/cli.ts",
20
+ "clean": "node --input-type=module -e \"import { rmSync } from 'node:fs'; rmSync('dist', { recursive: true, force: true });\"",
21
+ "build": "npm run clean && tsc -p tsconfig.json",
22
+ "prepack": "npm run build",
23
+ "start": "node dist/cli.js",
24
+ "check": "tsc --noEmit -p tsconfig.json",
25
+ "test": "node --test --import tsx tests/*.test.ts",
26
+ "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
+ "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
+ "test:perf": "node --test --test-concurrency=1 --import tsx tests/perf/*.perf.ts",
29
+ "test:perf:update-baseline": "UPDATE_PERF_BASELINE=1 node --test --test-concurrency=1 --import tsx tests/perf/*.perf.ts",
30
+ "test:perf:strict": "STRICT_PERF=1 node --test --test-concurrency=1 --import tsx tests/perf/*.perf.ts",
31
+ "test:manual:mcp-use-smoke": "node --import tsx tests/manual/mcp-use-client-smoke.manual.ts",
32
+ "test:manual:package-smoke": "node --import tsx tests/manual/package-distribution-smoke.manual.ts",
33
+ "validate": "npm run check && npm test && npm run test:coverage && npm run test:perf"
34
+ },
35
+ "keywords": [
36
+ "mcp",
37
+ "minecraft",
38
+ "modding"
39
+ ],
40
+ "author": "adhi-jp",
41
+ "license": "MIT",
42
+ "repository": "github:adhi-jp/minecraft-modding-mcp",
43
+ "bugs": "https://github.com/adhi-jp/minecraft-modding-mcp/issues",
44
+ "homepage": "https://github.com/adhi-jp/minecraft-modding-mcp#readme",
45
+ "engines": {
46
+ "node": ">=22"
47
+ },
48
+ "dependencies": {
49
+ "mcp-use": "^1.20.5",
50
+ "fast-glob": "^3.3.3",
51
+ "smol-toml": "^1.3.0",
52
+ "yauzl": "^3.2.0",
53
+ "zod": "^3.23.8"
54
+ },
55
+ "devDependencies": {
56
+ "@types/node": "^22.13.5",
57
+ "@types/yauzl": "^2.10.3",
58
+ "tsx": "^4.19.2",
59
+ "typescript": "^5.7.3"
60
+ }
61
+ }