@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,186 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, mkdirSync, statSync, rmSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { createError, ERROR_CODES } from "./errors.js";
6
+ import { log } from "./logger.js";
7
+ import { resolveTinyMappingFile } from "./mapping-service.js";
8
+ import { resolveMojangTinyFile } from "./mojang-tiny-mapping-service.js";
9
+ import { analyzeModJar } from "./mod-analyzer.js";
10
+ import { normalizePathForHost } from "./path-converter.js";
11
+ import { remapJar } from "./tiny-remapper-service.js";
12
+ import { resolveTinyRemapperJar } from "./tiny-remapper-resolver.js";
13
+ function normalizeTargetNamespace(target) {
14
+ return target === "yarn" ? "yarn" : "mojang";
15
+ }
16
+ function sourceNamespaceForLoader(loader) {
17
+ if (loader === "fabric" || loader === "quilt") {
18
+ return "intermediary";
19
+ }
20
+ throw createError({
21
+ code: ERROR_CODES.REMAP_FAILED,
22
+ message: `Unsupported mod loader for remapping: "${loader}". Only Fabric and Quilt mods are supported.`,
23
+ details: { loader }
24
+ });
25
+ }
26
+ function extractMinecraftVersion(dependencies) {
27
+ if (!dependencies) {
28
+ return undefined;
29
+ }
30
+ const mcDep = dependencies.find((dep) => dep.modId === "minecraft");
31
+ if (!mcDep?.versionRange) {
32
+ return undefined;
33
+ }
34
+ // Try to extract exact version from ranges like ">=1.20.4", "~1.20.4", "1.20.4", "^1.20.4"
35
+ const match = mcDep.versionRange.match(/(\d+\.\d+(?:\.\d+)?)/);
36
+ return match?.[1];
37
+ }
38
+ function buildCacheKey(inputJar, fromNamespace, targetNamespace, mcVersion) {
39
+ const stat = statSync(inputJar, { throwIfNoEntry: false });
40
+ const signature = stat ? `${stat.mtimeMs}:${stat.size}` : "unknown";
41
+ return createHash("sha256")
42
+ .update(`${inputJar}|${signature}|${fromNamespace}|${targetNamespace}|${mcVersion}`)
43
+ .digest("hex");
44
+ }
45
+ export async function remapModJar(input, config) {
46
+ const startedAt = Date.now();
47
+ const warnings = [];
48
+ // 1. Normalize input JAR path
49
+ const normalizedInput = normalizePathForHost(input.inputJar, undefined, "inputJar");
50
+ if (!normalizedInput.toLowerCase().endsWith(".jar")) {
51
+ throw createError({
52
+ code: ERROR_CODES.INVALID_INPUT,
53
+ message: "inputJar must point to a .jar file.",
54
+ details: { inputJar: normalizedInput }
55
+ });
56
+ }
57
+ if (!existsSync(normalizedInput)) {
58
+ throw createError({
59
+ code: ERROR_CODES.JAR_NOT_FOUND,
60
+ message: `Input JAR not found: ${normalizedInput}`,
61
+ details: { inputJar: normalizedInput }
62
+ });
63
+ }
64
+ const resolvedTargetNamespace = normalizeTargetNamespace(input.targetMapping);
65
+ // 2. Analyze mod metadata
66
+ const analysis = await analyzeModJar(normalizedInput);
67
+ if (analysis.loader === "unknown") {
68
+ throw createError({
69
+ code: ERROR_CODES.REMAP_FAILED,
70
+ message: "Could not detect mod loader. Only Fabric and Quilt mods are supported.",
71
+ details: { inputJar: normalizedInput }
72
+ });
73
+ }
74
+ const fromNamespace = sourceNamespaceForLoader(analysis.loader);
75
+ // 3. Determine MC version
76
+ const mcVersion = input.mcVersion ?? extractMinecraftVersion(analysis.dependencies);
77
+ if (!mcVersion) {
78
+ throw createError({
79
+ code: ERROR_CODES.INVALID_INPUT,
80
+ message: "Could not determine Minecraft version from mod metadata. Please provide mcVersion explicitly.",
81
+ details: {
82
+ inputJar: normalizedInput,
83
+ loader: analysis.loader,
84
+ modId: analysis.modId
85
+ }
86
+ });
87
+ }
88
+ // 4. Check cache after mapping context is known
89
+ const cacheKey = buildCacheKey(normalizedInput, fromNamespace, resolvedTargetNamespace, mcVersion);
90
+ const cacheDir = join(config.cacheDir, "remapped-mods");
91
+ mkdirSync(cacheDir, { recursive: true });
92
+ const cachedOutput = join(cacheDir, `${cacheKey}.jar`);
93
+ if (existsSync(cachedOutput)) {
94
+ const outputJar = input.outputJar
95
+ ? normalizePathForHost(input.outputJar, undefined, "outputJar")
96
+ : cachedOutput;
97
+ if (outputJar !== cachedOutput) {
98
+ const { copyFileSync } = await import("node:fs");
99
+ mkdirSync(dirname(outputJar), { recursive: true });
100
+ copyFileSync(cachedOutput, outputJar);
101
+ }
102
+ log("info", "remap.cache-hit", { inputJar: normalizedInput, outputJar });
103
+ return {
104
+ outputJar,
105
+ mcVersion,
106
+ fromMapping: fromNamespace,
107
+ targetMapping: input.targetMapping,
108
+ resolvedTargetNamespace,
109
+ durationMs: Date.now() - startedAt,
110
+ warnings: ["Result served from cache."]
111
+ };
112
+ }
113
+ // 5. Resolve tiny-remapper
114
+ const tinyRemapperJar = await resolveTinyRemapperJar(config.cacheDir, config.tinyRemapperJarPath);
115
+ // 6. Resolve mapping file and remap
116
+ let mappingsFile;
117
+ let toNamespace;
118
+ if (resolvedTargetNamespace === "yarn") {
119
+ mappingsFile = await resolveTinyMappingFile(mcVersion, "yarn", config.cacheDir);
120
+ toNamespace = "named";
121
+ }
122
+ else {
123
+ const mojangTiny = await resolveMojangTinyFile(mcVersion, config);
124
+ mappingsFile = mojangTiny.path;
125
+ toNamespace = "mojang";
126
+ warnings.push(...mojangTiny.warnings);
127
+ }
128
+ // 7. Determine output path
129
+ const modId = analysis.modId ?? "mod";
130
+ const modVersion = analysis.modVersion ?? "0";
131
+ const defaultOutputName = `${modId}-${modVersion}-${input.targetMapping}.jar`;
132
+ const outputJar = input.outputJar
133
+ ? normalizePathForHost(input.outputJar, undefined, "outputJar")
134
+ : join(dirname(normalizedInput), defaultOutputName);
135
+ mkdirSync(dirname(outputJar), { recursive: true });
136
+ // 8. Use temporary directory for intermediate work
137
+ const tempDir = join(tmpdir(), `mcp-remap-${cacheKey.slice(0, 12)}`);
138
+ mkdirSync(tempDir, { recursive: true });
139
+ try {
140
+ const tempOutput = join(tempDir, "remapped.jar");
141
+ await remapJar(tinyRemapperJar, {
142
+ inputJar: normalizedInput,
143
+ outputJar: tempOutput,
144
+ mappingsFile,
145
+ fromNamespace,
146
+ toNamespace,
147
+ timeoutMs: config.remapTimeoutMs,
148
+ maxMemoryMb: config.remapMaxMemoryMb
149
+ });
150
+ // Copy to final destination and cache
151
+ const { copyFileSync } = await import("node:fs");
152
+ copyFileSync(tempOutput, outputJar);
153
+ if (outputJar !== cachedOutput) {
154
+ mkdirSync(dirname(cachedOutput), { recursive: true });
155
+ copyFileSync(tempOutput, cachedOutput);
156
+ }
157
+ const durationMs = Date.now() - startedAt;
158
+ log("info", "remap.pipeline.done", {
159
+ inputJar: normalizedInput,
160
+ outputJar,
161
+ mcVersion,
162
+ fromMapping: fromNamespace,
163
+ targetMapping: input.targetMapping,
164
+ durationMs
165
+ });
166
+ return {
167
+ outputJar,
168
+ mcVersion,
169
+ fromMapping: fromNamespace,
170
+ targetMapping: input.targetMapping,
171
+ resolvedTargetNamespace,
172
+ durationMs,
173
+ warnings
174
+ };
175
+ }
176
+ finally {
177
+ // Cleanup temporary directory
178
+ try {
179
+ rmSync(tempDir, { recursive: true, force: true });
180
+ }
181
+ catch {
182
+ // best-effort cleanup
183
+ }
184
+ }
185
+ }
186
+ //# sourceMappingURL=mod-remap-service.js.map
@@ -0,0 +1,28 @@
1
+ import { ModDecompileService } from "./mod-decompile-service.js";
2
+ export type SearchModSourceSearchType = "class" | "method" | "field" | "content" | "all";
3
+ export type SearchModSourceInput = {
4
+ jarPath: string;
5
+ query: string;
6
+ searchType?: SearchModSourceSearchType;
7
+ limit?: number;
8
+ };
9
+ export type SearchModSourceHit = {
10
+ type: "class" | "method" | "field" | "content";
11
+ name: string;
12
+ file: string;
13
+ line?: number;
14
+ context?: string;
15
+ };
16
+ export type SearchModSourceOutput = {
17
+ query: string;
18
+ searchType: SearchModSourceSearchType;
19
+ hits: SearchModSourceHit[];
20
+ totalHits: number;
21
+ truncated: boolean;
22
+ warnings: string[];
23
+ };
24
+ export declare class ModSearchService {
25
+ private readonly modDecompileService;
26
+ constructor(modDecompileService: ModDecompileService);
27
+ searchModSource(input: SearchModSourceInput): Promise<SearchModSourceOutput>;
28
+ }
@@ -0,0 +1,174 @@
1
+ import { readFileSync } 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 { validateAndNormalizeJarPath } from "./path-resolver.js";
6
+ const DEFAULT_LIMIT = 50;
7
+ const MAX_LIMIT = 200;
8
+ const MAX_QUERY_LENGTH = 200;
9
+ const CONTEXT_LINES = 1;
10
+ const METHOD_PATTERN = /^\s*(public|private|protected)\s+.*\(/;
11
+ const FIELD_PATTERN = /^\s*(public|private|protected)\s+(?:static\s+)?(?:final\s+)?[\w<>,\[\]?]+\s+\w+\s*[;=]/;
12
+ function buildRegex(query) {
13
+ try {
14
+ return new RegExp(query, "gi");
15
+ }
16
+ catch {
17
+ // If the query is not valid regex, escape it and use as literal
18
+ const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
19
+ try {
20
+ return new RegExp(escaped, "gi");
21
+ }
22
+ catch {
23
+ return undefined;
24
+ }
25
+ }
26
+ }
27
+ function classifyLine(line) {
28
+ if (METHOD_PATTERN.test(line))
29
+ return "method";
30
+ if (FIELD_PATTERN.test(line))
31
+ return "field";
32
+ return "content";
33
+ }
34
+ function extractContext(lines, lineIndex) {
35
+ const start = Math.max(0, lineIndex - CONTEXT_LINES);
36
+ const end = Math.min(lines.length - 1, lineIndex + CONTEXT_LINES);
37
+ return lines.slice(start, end + 1).join("\n");
38
+ }
39
+ function filePathToClassName(filePath) {
40
+ return filePath.replace(/\.java$/, "").replaceAll("/", ".");
41
+ }
42
+ export class ModSearchService {
43
+ modDecompileService;
44
+ constructor(modDecompileService) {
45
+ this.modDecompileService = modDecompileService;
46
+ }
47
+ async searchModSource(input) {
48
+ const jarPath = validateAndNormalizeJarPath(input.jarPath);
49
+ const query = input.query.trim();
50
+ const searchType = input.searchType ?? "all";
51
+ const requestedLimit = Math.max(1, Math.trunc(input.limit ?? DEFAULT_LIMIT));
52
+ const limit = Math.min(requestedLimit, MAX_LIMIT);
53
+ if (!query) {
54
+ throw createError({
55
+ code: ERROR_CODES.INVALID_INPUT,
56
+ message: "query must be non-empty."
57
+ });
58
+ }
59
+ if (query.length > MAX_QUERY_LENGTH) {
60
+ throw createError({
61
+ code: ERROR_CODES.INVALID_INPUT,
62
+ message: `query exceeds max length of ${MAX_QUERY_LENGTH} characters.`,
63
+ details: { queryLength: query.length, maxLength: MAX_QUERY_LENGTH }
64
+ });
65
+ }
66
+ const regex = buildRegex(query);
67
+ if (!regex) {
68
+ throw createError({
69
+ code: ERROR_CODES.INVALID_INPUT,
70
+ message: `Invalid search query: "${query}".`,
71
+ details: { query }
72
+ });
73
+ }
74
+ const warnings = [];
75
+ if (requestedLimit > MAX_LIMIT) {
76
+ warnings.push(`limit was clamped to ${MAX_LIMIT} from ${requestedLimit}.`);
77
+ }
78
+ const startedAt = Date.now();
79
+ const decompileResult = await this.modDecompileService.decompileModJar({ jarPath });
80
+ const outputDir = decompileResult.outputDir;
81
+ warnings.push(...decompileResult.warnings);
82
+ const classNames = decompileResult.files ?? [];
83
+ const hits = [];
84
+ let totalHits = 0;
85
+ let reachedLimit = false;
86
+ for (const className of classNames) {
87
+ if (hits.length >= limit) {
88
+ reachedLimit = true;
89
+ break;
90
+ }
91
+ const filePath = className.replaceAll(".", "/") + ".java";
92
+ // Class name search: check if the simple class name matches
93
+ if (searchType === "class" || searchType === "all") {
94
+ const simpleClassName = className.split(".").pop() ?? className;
95
+ regex.lastIndex = 0;
96
+ if (regex.test(simpleClassName)) {
97
+ totalHits++;
98
+ if (hits.length < limit) {
99
+ hits.push({
100
+ type: "class",
101
+ name: className,
102
+ file: filePath
103
+ });
104
+ }
105
+ // If searching only classes, skip content search for this file
106
+ if (searchType === "class")
107
+ continue;
108
+ }
109
+ }
110
+ // Content/method/field search: read and scan the file
111
+ if (searchType === "method" || searchType === "field" || searchType === "content" || searchType === "all") {
112
+ let content;
113
+ try {
114
+ content = readFileSync(join(outputDir, filePath), "utf8");
115
+ }
116
+ catch {
117
+ // File might not exist at the expected path, skip
118
+ continue;
119
+ }
120
+ const lines = content.split("\n");
121
+ for (let i = 0; i < lines.length; i++) {
122
+ if (hits.length >= limit) {
123
+ reachedLimit = true;
124
+ break;
125
+ }
126
+ regex.lastIndex = 0;
127
+ if (!regex.test(lines[i]))
128
+ continue;
129
+ const lineType = classifyLine(lines[i]);
130
+ // Filter by search type
131
+ if (searchType !== "all" && searchType !== lineType)
132
+ continue;
133
+ totalHits++;
134
+ if (hits.length < limit) {
135
+ hits.push({
136
+ type: lineType,
137
+ name: lineType === "content" ? className : extractSymbolName(lines[i], lineType),
138
+ file: filePath,
139
+ line: i + 1,
140
+ context: extractContext(lines, i)
141
+ });
142
+ }
143
+ }
144
+ }
145
+ }
146
+ log("info", "mod-search.done", {
147
+ jarPath,
148
+ query,
149
+ searchType,
150
+ hitCount: hits.length,
151
+ durationMs: Date.now() - startedAt
152
+ });
153
+ return {
154
+ query,
155
+ searchType,
156
+ hits,
157
+ totalHits,
158
+ truncated: reachedLimit,
159
+ warnings
160
+ };
161
+ }
162
+ }
163
+ function extractSymbolName(line, type) {
164
+ const trimmed = line.trim();
165
+ if (type === "method") {
166
+ // Extract method name: last identifier before '('
167
+ const match = trimmed.match(/(\w+)\s*\(/);
168
+ return match?.[1] ?? trimmed.slice(0, 60);
169
+ }
170
+ // Extract field name: last identifier before '=' or ';'
171
+ const match = trimmed.match(/(\w+)\s*[;=]/);
172
+ return match?.[1] ?? trimmed.slice(0, 60);
173
+ }
174
+ //# sourceMappingURL=mod-search-service.js.map
@@ -0,0 +1,13 @@
1
+ import type { Config } from "./types.js";
2
+ import { VersionService } from "./version-service.js";
3
+ type VersionMappingsResolver = Pick<VersionService, "resolveVersionMappings">;
4
+ export interface ResolveMojangTinyDeps {
5
+ fetchFn?: typeof fetch;
6
+ versionService?: VersionMappingsResolver;
7
+ }
8
+ export interface ResolveMojangTinyResult {
9
+ path: string;
10
+ warnings: string[];
11
+ }
12
+ export declare function resolveMojangTinyFile(version: string, config: Config, deps?: ResolveMojangTinyDeps): Promise<ResolveMojangTinyResult>;
13
+ export {};