@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,161 @@
1
+ import { release } from "node:os";
2
+ import { createError, ERROR_CODES } from "./errors.js";
3
+ const WINDOWS_DRIVE_PATH = /^[A-Za-z]:[\\/]/;
4
+ const MALFORMED_WINDOWS_DRIVE_PATH = /^[A-Za-z]:(?![\\/])/;
5
+ const WSL_MOUNT_PATH = /^\/mnt\/[a-z](?:\/|$)/i;
6
+ const UNC_WSL_PATH = /^(?:\\\\wsl\$\\|\/\/wsl\$\/)/i;
7
+ function normalizeToUnixSlashes(value) {
8
+ return value.replace(/\\/g, "/");
9
+ }
10
+ function normalizeToWindowsSlashes(value) {
11
+ return value.replace(/\//g, "\\");
12
+ }
13
+ function parseUncWslPath(pathValue) {
14
+ const normalized = normalizeToUnixSlashes(pathValue);
15
+ if (!normalized.toLowerCase().startsWith("//wsl$/")) {
16
+ return undefined;
17
+ }
18
+ const remainder = normalized.slice("//wsl$/".length).replace(/^\/+/, "");
19
+ const slashIndex = remainder.indexOf("/");
20
+ if (slashIndex < 0) {
21
+ return {
22
+ distro: remainder,
23
+ innerPath: ""
24
+ };
25
+ }
26
+ return {
27
+ distro: remainder.slice(0, slashIndex),
28
+ innerPath: remainder.slice(slashIndex + 1)
29
+ };
30
+ }
31
+ export function detectPathRuntime() {
32
+ const platform = process.platform;
33
+ const osRelease = release().toLowerCase();
34
+ const isWsl = platform === "linux" &&
35
+ (Boolean(process.env.WSL_DISTRO_NAME) ||
36
+ Boolean(process.env.WSL_INTEROP) ||
37
+ osRelease.includes("microsoft"));
38
+ return {
39
+ platform,
40
+ isWsl,
41
+ wslDistro: process.env.WSL_DISTRO_NAME
42
+ };
43
+ }
44
+ export function isWindowsDrivePath(pathValue) {
45
+ return WINDOWS_DRIVE_PATH.test(pathValue);
46
+ }
47
+ export function isWslMountPath(pathValue) {
48
+ return WSL_MOUNT_PATH.test(pathValue);
49
+ }
50
+ export function isUncWslPath(pathValue) {
51
+ return UNC_WSL_PATH.test(pathValue);
52
+ }
53
+ export function validatePathFormat(pathValue, runtimeInfo) {
54
+ const runtime = runtimeInfo ?? detectPathRuntime();
55
+ const trimmed = pathValue.trim();
56
+ if (!trimmed) {
57
+ return "Path is required.";
58
+ }
59
+ if (trimmed.includes("\0")) {
60
+ return "Path contains an invalid null character.";
61
+ }
62
+ if (MALFORMED_WINDOWS_DRIVE_PATH.test(trimmed)) {
63
+ return 'Windows drive paths must include a separator after the drive (example: "C:\\\\path").';
64
+ }
65
+ if (isUncWslPath(trimmed)) {
66
+ const parsed = parseUncWslPath(trimmed);
67
+ if (!parsed?.distro) {
68
+ return 'UNC WSL paths must include a distro segment (example: "\\\\\\\\wsl$\\\\Ubuntu\\\\path").';
69
+ }
70
+ }
71
+ const usesWindowsStyle = isWindowsDrivePath(trimmed) || isUncWslPath(trimmed);
72
+ if (usesWindowsStyle && runtime.platform !== "win32" && !runtime.isWsl) {
73
+ return "Windows-style paths are not supported on this host.";
74
+ }
75
+ return undefined;
76
+ }
77
+ function throwInvalidPath(pathValue, reason, runtime, field) {
78
+ throw createError({
79
+ code: ERROR_CODES.INVALID_INPUT,
80
+ message: reason,
81
+ details: {
82
+ field: field ?? "path",
83
+ path: pathValue,
84
+ platform: runtime.platform,
85
+ isWsl: runtime.isWsl
86
+ }
87
+ });
88
+ }
89
+ function toWslPath(pathValue, runtime, field) {
90
+ if (isWindowsDrivePath(pathValue)) {
91
+ const normalized = normalizeToUnixSlashes(pathValue);
92
+ const match = normalized.match(/^([A-Za-z]):(?:\/(.*))?$/);
93
+ if (!match?.[1]) {
94
+ throwInvalidPath(pathValue, "Failed to parse Windows drive path.", runtime, field);
95
+ }
96
+ const drive = match[1].toLowerCase();
97
+ const rest = (match[2] ?? "").replace(/^\/+/, "");
98
+ return rest ? `/mnt/${drive}/${rest}` : `/mnt/${drive}`;
99
+ }
100
+ const parsed = parseUncWslPath(pathValue);
101
+ if (parsed) {
102
+ const rest = parsed.innerPath.replace(/^\/+/, "");
103
+ return rest ? `/${rest}` : "/";
104
+ }
105
+ return pathValue;
106
+ }
107
+ function toWindowsPath(pathValue, runtime, field) {
108
+ if (isWslMountPath(pathValue)) {
109
+ const normalized = normalizeToUnixSlashes(pathValue);
110
+ const match = normalized.match(/^\/mnt\/([A-Za-z])(?:\/(.*))?$/);
111
+ if (!match?.[1]) {
112
+ throwInvalidPath(pathValue, "Failed to parse WSL mount path.", runtime, field);
113
+ }
114
+ const drive = match[1].toUpperCase();
115
+ const rest = (match[2] ?? "").replace(/^\/+/, "").replace(/\//g, "\\");
116
+ return rest ? `${drive}:\\${rest}` : `${drive}:\\`;
117
+ }
118
+ if (pathValue.startsWith("/")) {
119
+ const distro = runtime.wslDistro?.trim();
120
+ if (!distro) {
121
+ throwInvalidPath(pathValue, "Cannot convert Unix path to Windows path without WSL distro name.", runtime, field);
122
+ }
123
+ return `\\\\wsl$\\${distro}${pathValue.replace(/\//g, "\\")}`;
124
+ }
125
+ return normalizeToWindowsSlashes(pathValue);
126
+ }
127
+ export function normalizePathForHost(pathValue, runtimeInfo, field) {
128
+ const runtime = runtimeInfo ?? detectPathRuntime();
129
+ const validationError = validatePathFormat(pathValue, runtime);
130
+ if (validationError) {
131
+ throwInvalidPath(pathValue, validationError, runtime, field);
132
+ }
133
+ const trimmed = pathValue.trim();
134
+ if (runtime.isWsl) {
135
+ if (isWindowsDrivePath(trimmed) || isUncWslPath(trimmed)) {
136
+ return toWslPath(trimmed, runtime, field);
137
+ }
138
+ return trimmed;
139
+ }
140
+ if (runtime.platform === "win32") {
141
+ if (isWindowsDrivePath(trimmed) || isUncWslPath(trimmed)) {
142
+ return normalizeToWindowsSlashes(trimmed);
143
+ }
144
+ if (isWslMountPath(trimmed) || trimmed.startsWith("/")) {
145
+ return toWindowsPath(trimmed, runtime, field);
146
+ }
147
+ return trimmed;
148
+ }
149
+ return trimmed;
150
+ }
151
+ export function normalizeOptionalPathForHost(pathValue, runtimeInfo, field) {
152
+ if (!pathValue) {
153
+ return undefined;
154
+ }
155
+ const trimmed = pathValue.trim();
156
+ if (!trimmed) {
157
+ return undefined;
158
+ }
159
+ return normalizePathForHost(trimmed, runtimeInfo, field);
160
+ }
161
+ //# sourceMappingURL=path-converter.js.map
@@ -0,0 +1,19 @@
1
+ import type { ArtifactSignature } from "./types.js";
2
+ export interface ResolvedJarInfo {
3
+ originalPath: string;
4
+ resolvedPath: string;
5
+ }
6
+ export declare function normalizeJarPath(jarPath: string): string;
7
+ export declare function resolveJarPathWithSymlinkCheck(jarPath: string): ResolvedJarInfo;
8
+ export declare function buildJarSignature(stats: {
9
+ mtimeMs: number;
10
+ size: number;
11
+ }): string;
12
+ export declare function artifactSignatureFromFile(jarPath: string): ArtifactSignature;
13
+ export declare function isSecureJarEntryPath(entryPath: string): boolean;
14
+ /**
15
+ * Validate and normalize a user-supplied jar path input.
16
+ * Trims whitespace, validates non-empty, resolves symlinks, and wraps
17
+ * any filesystem error as ERR_INVALID_INPUT.
18
+ */
19
+ export declare function validateAndNormalizeJarPath(jarPathInput: string): string;
@@ -0,0 +1,78 @@
1
+ import { realpathSync, statSync } from "node:fs";
2
+ import { extname, resolve } from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { normalizePathForHost } from "./path-converter.js";
5
+ import { createError, ERROR_CODES } from "./errors.js";
6
+ const INVALID_ENTRY = /(^|\/|\\)\.\.(\/|\\|$)/;
7
+ export function normalizeJarPath(jarPath) {
8
+ const normalizedInput = normalizePathForHost(jarPath, undefined, "jarPath");
9
+ const absolute = resolve(normalizedInput);
10
+ const stats = statSync(absolute);
11
+ if (!stats.isFile()) {
12
+ throw createError({
13
+ code: ERROR_CODES.JAR_NOT_FOUND,
14
+ message: `Expected a file path for jar, got "${normalizedInput}".`,
15
+ details: { jarPath: normalizedInput }
16
+ });
17
+ }
18
+ if (extname(absolute).toLowerCase() !== ".jar") {
19
+ throw createError({
20
+ code: ERROR_CODES.INVALID_INPUT,
21
+ message: `Expected a .jar file, got "${normalizedInput}".`,
22
+ details: { jarPath: normalizedInput }
23
+ });
24
+ }
25
+ const resolved = realpathSync(absolute);
26
+ return resolved;
27
+ }
28
+ export function resolveJarPathWithSymlinkCheck(jarPath) {
29
+ const resolvedPath = normalizeJarPath(jarPath);
30
+ return {
31
+ originalPath: jarPath,
32
+ resolvedPath
33
+ };
34
+ }
35
+ export function buildJarSignature(stats) {
36
+ return `${Math.trunc(stats.mtimeMs)}:${stats.size}`;
37
+ }
38
+ export function artifactSignatureFromFile(jarPath) {
39
+ const resolvedPath = resolveJarPathWithSymlinkCheck(jarPath).resolvedPath;
40
+ const stats = statSync(resolvedPath);
41
+ return {
42
+ sourcePath: resolvedPath,
43
+ sourceArtifactId: createHash("sha256").update(`jar|${resolvedPath}|${buildJarSignature(stats)}`).digest("hex"),
44
+ signature: buildJarSignature(stats),
45
+ signatureParts: {
46
+ mtimeMs: stats.mtimeMs,
47
+ size: stats.size
48
+ }
49
+ };
50
+ }
51
+ export function isSecureJarEntryPath(entryPath) {
52
+ return !INVALID_ENTRY.test(entryPath.replaceAll("\\", "/"));
53
+ }
54
+ /**
55
+ * Validate and normalize a user-supplied jar path input.
56
+ * Trims whitespace, validates non-empty, resolves symlinks, and wraps
57
+ * any filesystem error as ERR_INVALID_INPUT.
58
+ */
59
+ export function validateAndNormalizeJarPath(jarPathInput) {
60
+ const jarPath = jarPathInput.trim();
61
+ if (!jarPath) {
62
+ throw createError({
63
+ code: ERROR_CODES.INVALID_INPUT,
64
+ message: "jarPath must be non-empty."
65
+ });
66
+ }
67
+ try {
68
+ return normalizeJarPath(jarPath);
69
+ }
70
+ catch (cause) {
71
+ throw createError({
72
+ code: ERROR_CODES.INVALID_INPUT,
73
+ message: cause instanceof Error ? cause.message : `Invalid jar path: "${jarPath}".`,
74
+ details: { jarPath }
75
+ });
76
+ }
77
+ }
78
+ //# sourceMappingURL=path-resolver.js.map
@@ -0,0 +1,29 @@
1
+ import { VersionService } from "./version-service.js";
2
+ import type { Config } from "./types.js";
3
+ export type GetRegistryDataInput = {
4
+ version: string;
5
+ registry?: string;
6
+ };
7
+ export type RegistryEntry = {
8
+ protocol_id: number;
9
+ };
10
+ export type RegistryData = {
11
+ default?: string;
12
+ entries: Record<string, RegistryEntry>;
13
+ };
14
+ export type GetRegistryDataOutput = {
15
+ version: string;
16
+ registry?: string;
17
+ registries?: string[];
18
+ data: Record<string, RegistryData> | RegistryData;
19
+ entryCount: number;
20
+ warnings: string[];
21
+ };
22
+ export declare class RegistryService {
23
+ private readonly config;
24
+ private readonly versionService;
25
+ private readonly registryCache;
26
+ constructor(config: Config, versionService: VersionService);
27
+ getRegistryData(input: GetRegistryDataInput): Promise<GetRegistryDataOutput>;
28
+ private loadRegistries;
29
+ }
@@ -0,0 +1,214 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { mkdir } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { spawn } from "node:child_process";
5
+ import { createError, ERROR_CODES } from "./errors.js";
6
+ import { log } from "./logger.js";
7
+ const DATAGEN_TIMEOUT_MS = 5 * 60 * 1000;
8
+ const MAX_STDIO_SNAPSHOT = 6_240;
9
+ function limitOutput(text) {
10
+ if (text.length <= MAX_STDIO_SNAPSHOT)
11
+ return text;
12
+ return text.slice(-MAX_STDIO_SNAPSHOT);
13
+ }
14
+ function resolveRegistryPaths(registryDir) {
15
+ return [
16
+ join(registryDir, "reports", "registries.json"),
17
+ join(registryDir, "generated", "reports", "registries.json"),
18
+ join(registryDir, "registries.json")
19
+ ];
20
+ }
21
+ function findRegistryFile(registryDir) {
22
+ for (const candidate of resolveRegistryPaths(registryDir)) {
23
+ if (existsSync(candidate))
24
+ return candidate;
25
+ }
26
+ return undefined;
27
+ }
28
+ function runDataGen(serverJarPath, outputDir, version) {
29
+ return new Promise((resolve, reject) => {
30
+ // MC 1.18+ uses bundler format, older versions use -cp with main class directly.
31
+ // The bundler approach works for 1.18+ and the -cp approach for older versions.
32
+ // We try bundler first since most modern versions use it.
33
+ const isLegacy = isLegacyVersion(version);
34
+ const args = isLegacy
35
+ ? [
36
+ "-cp", serverJarPath,
37
+ "-Xmx2G", "-Xms512M",
38
+ "net.minecraft.data.Main",
39
+ "--reports", "--all", "--server",
40
+ "--output", outputDir
41
+ ]
42
+ : [
43
+ "-DbundlerMainClass=net.minecraft.data.Main",
44
+ "-Xmx2G", "-Xms512M",
45
+ "-jar", serverJarPath,
46
+ "--reports", "--all", "--server",
47
+ "--output", outputDir
48
+ ];
49
+ log("info", "registry.datagen.start", { version, isLegacy, serverJarPath, outputDir });
50
+ const proc = spawn("java", args, {
51
+ stdio: ["ignore", "pipe", "pipe"],
52
+ cwd: outputDir
53
+ });
54
+ let stderr = "";
55
+ const timer = setTimeout(() => {
56
+ proc.kill();
57
+ reject(createError({
58
+ code: ERROR_CODES.REGISTRY_GENERATION_FAILED,
59
+ message: `Registry data generation timed out for version "${version}".`,
60
+ details: { version, timeoutMs: DATAGEN_TIMEOUT_MS, stderrTail: limitOutput(stderr) }
61
+ }));
62
+ }, DATAGEN_TIMEOUT_MS);
63
+ proc.stderr?.on("data", (chunk) => {
64
+ stderr += chunk.toString("utf8");
65
+ stderr = limitOutput(stderr);
66
+ });
67
+ proc.once("error", (error) => {
68
+ clearTimeout(timer);
69
+ reject(createError({
70
+ code: ERROR_CODES.JAVA_UNAVAILABLE,
71
+ message: "java command is not available for data generation.",
72
+ details: { error: error instanceof Error ? error.message : String(error) }
73
+ }));
74
+ });
75
+ proc.once("exit", (code) => {
76
+ clearTimeout(timer);
77
+ if (code !== 0) {
78
+ reject(createError({
79
+ code: ERROR_CODES.REGISTRY_GENERATION_FAILED,
80
+ message: `Data generation exited with code ${code} for version "${version}".`,
81
+ details: { version, code, stderrTail: limitOutput(stderr) }
82
+ }));
83
+ return;
84
+ }
85
+ log("info", "registry.datagen.done", { version });
86
+ resolve();
87
+ });
88
+ });
89
+ }
90
+ /**
91
+ * MC versions before 1.18 use -cp instead of -jar bundler format for data gen.
92
+ * 1.18 snapshots start with "21w" (2021 weekly), release is "1.18".
93
+ */
94
+ function isLegacyVersion(version) {
95
+ // Snapshot format: YYwNNa
96
+ const snapshotMatch = version.match(/^(\d{2})w(\d{2})[a-z]$/);
97
+ if (snapshotMatch) {
98
+ const year = Number(snapshotMatch[1]);
99
+ const week = Number(snapshotMatch[2]);
100
+ // 1.18 started in 21w37a
101
+ if (year < 21)
102
+ return true;
103
+ if (year === 21 && week < 37)
104
+ return true;
105
+ return false;
106
+ }
107
+ // Release format: 1.X or 1.X.Y
108
+ const releaseMatch = version.match(/^1\.(\d+)(?:\.\d+)?(?:-.*)?$/);
109
+ if (releaseMatch) {
110
+ return Number(releaseMatch[1]) < 18;
111
+ }
112
+ // New format (26.1+) is never legacy
113
+ return false;
114
+ }
115
+ export class RegistryService {
116
+ config;
117
+ versionService;
118
+ registryCache = new Map();
119
+ constructor(config, versionService) {
120
+ this.config = config;
121
+ this.versionService = versionService;
122
+ }
123
+ async getRegistryData(input) {
124
+ const version = input.version.trim();
125
+ if (!version) {
126
+ throw createError({
127
+ code: ERROR_CODES.INVALID_INPUT,
128
+ message: "version must be non-empty."
129
+ });
130
+ }
131
+ const warnings = [];
132
+ const allRegistries = await this.loadRegistries(version, warnings);
133
+ const registryNames = Object.keys(allRegistries).sort();
134
+ if (input.registry) {
135
+ const registryName = normalizeRegistryName(input.registry);
136
+ const data = allRegistries[registryName];
137
+ if (!data) {
138
+ throw createError({
139
+ code: ERROR_CODES.SOURCE_NOT_FOUND,
140
+ message: `Registry "${registryName}" not found for version "${version}".`,
141
+ details: {
142
+ version,
143
+ registry: registryName,
144
+ available: registryNames.slice(0, 20)
145
+ }
146
+ });
147
+ }
148
+ return {
149
+ version,
150
+ registry: registryName,
151
+ data,
152
+ entryCount: Object.keys(data.entries).length,
153
+ warnings
154
+ };
155
+ }
156
+ let totalEntries = 0;
157
+ for (const registry of Object.values(allRegistries)) {
158
+ totalEntries += Object.keys(registry.entries).length;
159
+ }
160
+ return {
161
+ version,
162
+ registries: registryNames,
163
+ data: allRegistries,
164
+ entryCount: totalEntries,
165
+ warnings
166
+ };
167
+ }
168
+ async loadRegistries(version, warnings) {
169
+ const cached = this.registryCache.get(version);
170
+ if (cached)
171
+ return cached;
172
+ const registryDir = join(this.config.cacheDir, "registries", version);
173
+ // Check if we already have generated data
174
+ let registryFile = findRegistryFile(registryDir);
175
+ if (!registryFile) {
176
+ await mkdir(registryDir, { recursive: true });
177
+ const serverJar = await this.versionService.resolveServerJar(version);
178
+ await runDataGen(serverJar.jarPath, registryDir, version);
179
+ registryFile = findRegistryFile(registryDir);
180
+ if (!registryFile) {
181
+ throw createError({
182
+ code: ERROR_CODES.REGISTRY_GENERATION_FAILED,
183
+ message: `Registry data generation did not produce registries.json for version "${version}".`,
184
+ details: { version, registryDir }
185
+ });
186
+ }
187
+ }
188
+ const raw = readFileSync(registryFile, "utf8");
189
+ const parsed = JSON.parse(raw);
190
+ // Validate structure
191
+ if (typeof parsed !== "object" || parsed === null) {
192
+ throw createError({
193
+ code: ERROR_CODES.REGISTRY_GENERATION_FAILED,
194
+ message: `registries.json for version "${version}" has invalid structure.`,
195
+ details: { version }
196
+ });
197
+ }
198
+ this.registryCache.set(version, parsed);
199
+ // Trim cache to avoid unbounded growth
200
+ if (this.registryCache.size > 8) {
201
+ const oldest = this.registryCache.keys().next().value;
202
+ if (oldest !== undefined)
203
+ this.registryCache.delete(oldest);
204
+ }
205
+ return parsed;
206
+ }
207
+ }
208
+ function normalizeRegistryName(name) {
209
+ const trimmed = name.trim();
210
+ if (trimmed.includes(":"))
211
+ return trimmed;
212
+ return `minecraft:${trimmed}`;
213
+ }
214
+ //# sourceMappingURL=registry-service.js.map
@@ -0,0 +1,15 @@
1
+ export interface DownloadResult {
2
+ ok: boolean;
3
+ statusCode?: number;
4
+ etag?: string;
5
+ lastModified?: string;
6
+ contentLength?: number;
7
+ path?: string;
8
+ }
9
+ export interface DownloadOptions {
10
+ timeoutMs?: number;
11
+ retries?: number;
12
+ fetchFn?: typeof fetch;
13
+ }
14
+ export declare function downloadToCache(url: string, destinationPath: string, opts?: DownloadOptions): Promise<DownloadResult>;
15
+ export declare function defaultDownloadPath(cacheDir: string, url: string): string;
@@ -0,0 +1,111 @@
1
+ import { createWriteStream, mkdirSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { createHash, randomBytes } from "node:crypto";
4
+ import { Readable } from "node:stream";
5
+ import { pipeline } from "node:stream/promises";
6
+ import { createError, ERROR_CODES } from "./errors.js";
7
+ function isHttpUrl(url) {
8
+ try {
9
+ const parsed = new URL(url);
10
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
11
+ }
12
+ catch {
13
+ return false;
14
+ }
15
+ }
16
+ function sleep(ms) {
17
+ return new Promise((resolve) => setTimeout(resolve, ms));
18
+ }
19
+ function sha256(input) {
20
+ return createHash("sha256").update(input).digest("hex");
21
+ }
22
+ function retryDelay(baseMs, attempt) {
23
+ return Math.floor(baseMs * 2 ** attempt + Math.random() * 128);
24
+ }
25
+ export async function downloadToCache(url, destinationPath, opts = {}) {
26
+ const timeoutMs = opts.timeoutMs ?? 15000;
27
+ const maxRetries = opts.retries ?? 2;
28
+ const fetchFn = opts.fetchFn ?? globalThis.fetch;
29
+ if (!isHttpUrl(url)) {
30
+ throw createError({
31
+ code: ERROR_CODES.INVALID_INPUT,
32
+ message: `Unsupported scheme for download URL: ${url}`,
33
+ details: { url }
34
+ });
35
+ }
36
+ mkdirSync(dirname(destinationPath), { recursive: true });
37
+ let attempt = 0;
38
+ while (true) {
39
+ const timeout = new AbortController();
40
+ const timer = setTimeout(() => timeout.abort(), timeoutMs);
41
+ try {
42
+ const response = await fetchFn(url, { signal: timeout.signal });
43
+ clearTimeout(timer);
44
+ const status = response.status;
45
+ if (status === 404) {
46
+ return { ok: false, statusCode: status };
47
+ }
48
+ if (status === 429 || (status >= 500 && status < 600)) {
49
+ if (attempt >= maxRetries) {
50
+ return { ok: false, statusCode: status };
51
+ }
52
+ const retryAfter = Number.parseInt(response.headers.get("retry-after") ?? "", 10);
53
+ const waitMs = Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : retryDelay(200, attempt);
54
+ await sleep(waitMs);
55
+ attempt += 1;
56
+ continue;
57
+ }
58
+ if (!response.ok) {
59
+ return {
60
+ ok: false,
61
+ statusCode: status,
62
+ etag: response.headers.get("etag") ?? undefined,
63
+ lastModified: response.headers.get("last-modified") ?? undefined,
64
+ contentLength: Number.parseInt(response.headers.get("content-length") ?? "0", 10)
65
+ };
66
+ }
67
+ const tempPath = `${destinationPath}.${randomBytes(4).toString("hex")}.tmp`;
68
+ try {
69
+ if (!response.body) {
70
+ writeFileSync(tempPath, Buffer.alloc(0));
71
+ }
72
+ else {
73
+ const readable = Readable.fromWeb(response.body);
74
+ await pipeline(readable, createWriteStream(tempPath));
75
+ }
76
+ const contentLength = statSync(tempPath).size;
77
+ renameSync(tempPath, destinationPath);
78
+ return {
79
+ ok: true,
80
+ statusCode: status,
81
+ etag: response.headers.get("etag") ?? undefined,
82
+ lastModified: response.headers.get("last-modified") ?? undefined,
83
+ contentLength,
84
+ path: destinationPath
85
+ };
86
+ }
87
+ catch (streamError) {
88
+ try {
89
+ unlinkSync(tempPath);
90
+ }
91
+ catch {
92
+ // best-effort cleanup
93
+ }
94
+ throw streamError instanceof Error ? streamError : new Error(String(streamError));
95
+ }
96
+ }
97
+ catch (caughtError) {
98
+ clearTimeout(timer);
99
+ if (attempt >= maxRetries) {
100
+ throw caughtError instanceof Error ? caughtError : new Error(String(caughtError));
101
+ }
102
+ await sleep(retryDelay(200, attempt));
103
+ attempt += 1;
104
+ }
105
+ }
106
+ }
107
+ export function defaultDownloadPath(cacheDir, url) {
108
+ const filename = `${sha256(url)}.jar`;
109
+ return `${cacheDir}/downloads/${filename}`;
110
+ }
111
+ //# sourceMappingURL=repo-downloader.js.map
@@ -0,0 +1,3 @@
1
+ import type { MCPServer } from "mcp-use/server";
2
+ import type { SourceService } from "./source-service.js";
3
+ export declare function registerResources(server: InstanceType<typeof MCPServer>, sourceService: SourceService): void;