Skip to content

Commit 84171e1

Browse files
committed
fix: add unobfuscated version awareness to MappingService (#5)
MappingService.getMappings() and lookupMapping() now detect unobfuscated Minecraft versions (26.1+) and throw clear, actionable errors instead of failing with cryptic "Intermediary mappings not available" messages. The RemapService already handled this correctly, but tools like find_mapping and the mappings resource endpoint called MappingService directly, bypassing the check. Bumps version to 1.1.0 for npm publish — v1.0.0 on npm predates the unobfuscated version support added in PR #4.
1 parent cf44fb7 commit 84171e1

6 files changed

Lines changed: 146 additions & 4 deletions

File tree

__tests__/core/mapping-service.test.ts

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { existsSync, readFileSync } from 'node:fs';
22
import { describe, expect, it } from 'vitest';
33
import { getMappingService } from '../../src/services/mapping-service.js';
4-
import { TEST_MAPPING, TEST_VERSION } from '../test-constants.js';
4+
import { TEST_MAPPING, TEST_VERSION, UNOBFUSCATED_TEST_VERSION } from '../test-constants.js';
55

66
/**
77
* Mapping Service Tests
@@ -434,3 +434,79 @@ describe('Mojmap Tiny v2 Structure Verification', () => {
434434
expect(firstLine).toContain('named');
435435
}, 180000);
436436
});
437+
438+
/**
439+
* Unobfuscated version handling (26.1+)
440+
*
441+
* Unobfuscated Minecraft versions ship JARs without obfuscation.
442+
* No intermediary, yarn, or mojmap mapping files exist for these versions.
443+
* MappingService.getMappings() and lookupMapping() must fail with clear,
444+
* actionable error messages instead of cryptic download failures.
445+
*
446+
* Reproduces: https://github.com/MCDxAI/minecraft-dev-mcp/issues/5
447+
*/
448+
describe('Unobfuscated version handling', () => {
449+
it('should throw actionable error for getMappings(intermediary) on unobfuscated version', async () => {
450+
const mappingService = getMappingService();
451+
await expect(
452+
mappingService.getMappings(UNOBFUSCATED_TEST_VERSION, 'intermediary'),
453+
).rejects.toThrow(/unobfuscated/i);
454+
await expect(
455+
mappingService.getMappings(UNOBFUSCATED_TEST_VERSION, 'intermediary'),
456+
).rejects.toThrow(/mojmap/i);
457+
}, 30000);
458+
459+
it('should throw actionable error for getMappings(yarn) on unobfuscated version', async () => {
460+
const mappingService = getMappingService();
461+
await expect(
462+
mappingService.getMappings(UNOBFUSCATED_TEST_VERSION, 'yarn'),
463+
).rejects.toThrow(/unobfuscated/i);
464+
await expect(
465+
mappingService.getMappings(UNOBFUSCATED_TEST_VERSION, 'yarn'),
466+
).rejects.toThrow(/mojmap/i);
467+
}, 30000);
468+
469+
it('should throw actionable error for getMappings(mojmap) on unobfuscated version', async () => {
470+
const mappingService = getMappingService();
471+
await expect(
472+
mappingService.getMappings(UNOBFUSCATED_TEST_VERSION, 'mojmap'),
473+
).rejects.toThrow(/unobfuscated/i);
474+
await expect(
475+
mappingService.getMappings(UNOBFUSCATED_TEST_VERSION, 'mojmap'),
476+
).rejects.toThrow(/already in Mojang/i);
477+
}, 30000);
478+
479+
it('should throw actionable error for lookupMapping on unobfuscated version', async () => {
480+
const mappingService = getMappingService();
481+
await expect(
482+
mappingService.lookupMapping(
483+
UNOBFUSCATED_TEST_VERSION,
484+
'Entity',
485+
'mojmap',
486+
'yarn',
487+
),
488+
).rejects.toThrow(/unobfuscated/i);
489+
await expect(
490+
mappingService.lookupMapping(
491+
UNOBFUSCATED_TEST_VERSION,
492+
'Entity',
493+
'mojmap',
494+
'yarn',
495+
),
496+
).rejects.toThrow(/no mapping translation is needed/i);
497+
}, 30000);
498+
499+
it('should allow same-type lookupMapping on unobfuscated version (identity)', async () => {
500+
const mappingService = getMappingService();
501+
// Same source and target mapping should still return identity (no mapping file needed)
502+
const result = await mappingService.lookupMapping(
503+
UNOBFUSCATED_TEST_VERSION,
504+
'net/minecraft/world/entity/Entity',
505+
'mojmap',
506+
'mojmap',
507+
);
508+
expect(result.found).toBe(true);
509+
expect(result.source).toBe('net/minecraft/world/entity/Entity');
510+
expect(result.target).toBe('net/minecraft/world/entity/Entity');
511+
}, 10000);
512+
});

__tests__/core/version-manager.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,5 +70,24 @@ describe('Version Management', () => {
7070
const result = await versionManager.isVersionUnobfuscated('26.1-snapshot-9');
7171
expect(result).toBe(true);
7272
}, 30000);
73+
74+
// Regression tests for https://github.com/MCDxAI/minecraft-dev-mcp/issues/5
75+
it('should return true for 26.1-snapshot-10 (issue #5)', async () => {
76+
const versionManager = getVersionManager();
77+
const result = await versionManager.isVersionUnobfuscated('26.1-snapshot-10');
78+
expect(result).toBe(true);
79+
}, 30000);
80+
81+
it('should return true for 26.1-snapshot-11 (issue #5)', async () => {
82+
const versionManager = getVersionManager();
83+
const result = await versionManager.isVersionUnobfuscated('26.1-snapshot-11');
84+
expect(result).toBe(true);
85+
}, 30000);
86+
87+
it('should return true for 26.1-rc-3 (issue #5)', async () => {
88+
const versionManager = getVersionManager();
89+
const result = await versionManager.isVersionUnobfuscated('26.1-rc-3');
90+
expect(result).toBe(true);
91+
}, 30000);
7392
});
7493
});

__tests__/tools/core-tools.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,21 @@ describe('Decompile and Remap Tools', () => {
560560
expect(text).toMatch(/use ['"]mojmap['"] mapping/i);
561561
}, 120000);
562562

563+
it('should return actionable error for find_mapping on unobfuscated version', async () => {
564+
const result = await handleFindMapping({
565+
symbol: 'Entity',
566+
version: UNOBFUSCATED_TEST_VERSION,
567+
sourceMapping: 'mojmap',
568+
targetMapping: 'yarn',
569+
});
570+
571+
expect(result).toBeDefined();
572+
expect(result.isError).toBe(true);
573+
const text = result.content[0].text;
574+
expect(text).toMatch(/unobfuscated/i);
575+
expect(text).toMatch(/no mapping translation is needed/i);
576+
}, 60000);
577+
563578
it('should handle remap_mod_jar with Fabric mod', async () => {
564579
// Skip if fixture doesn't exist
565580
if (!existsSync(METEOR_JAR_PATH)) {

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@mcdxai/minecraft-dev-mcp",
3-
"version": "1.0.0",
3+
"version": "1.1.0",
44
"description": "MCP server for Minecraft mod development - decompile, remap, and explore Minecraft source code",
55
"type": "module",
66
"main": "./dist/index.js",

src/services/mapping-service.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { MappingNotFoundError } from '../utils/errors.js';
1111
import { ensureDir } from '../utils/file-utils.js';
1212
import { logger } from '../utils/logger.js';
1313
import { getMojmapTinyPath } from '../utils/paths.js';
14+
import { getVersionManager } from './version-manager.js';
1415

1516
/**
1617
* Manages mapping downloads and caching
@@ -19,6 +20,7 @@ export class MappingService {
1920
private mojangDownloader = getMojangDownloader();
2021
private fabricMaven = getFabricMaven();
2122
private cache = getCacheManager();
23+
private versionManager = getVersionManager();
2224

2325
// Lock to prevent concurrent downloads of the same mappings
2426
private downloadLocks = new Map<string, Promise<string>>();
@@ -28,6 +30,25 @@ export class MappingService {
2830
* Uses locking to prevent concurrent downloads of the same mapping
2931
*/
3032
async getMappings(version: string, mappingType: MappingType): Promise<string> {
33+
// Unobfuscated versions (26.1+) ship without obfuscation — no mapping files exist.
34+
const isUnobfuscated = await this.versionManager.isVersionUnobfuscated(version);
35+
if (isUnobfuscated) {
36+
if (mappingType === 'mojmap') {
37+
throw new MappingNotFoundError(
38+
version,
39+
mappingType,
40+
`Mojmap mapping files are not available for unobfuscated version ${version}. ` +
41+
`The JAR is already in Mojang's human-readable names — decompile it directly with mapping 'mojmap'.`,
42+
);
43+
}
44+
throw new MappingNotFoundError(
45+
version,
46+
mappingType,
47+
`${mappingType} mappings are not available for unobfuscated version ${version}. ` +
48+
`Use 'mojmap' mapping instead — the JAR ships without obfuscation.`,
49+
);
50+
}
51+
3152
const lockKey = `${version}-${mappingType}`;
3253

3354
// For Mojmap, check for converted Tiny file first (not raw ProGuard)
@@ -267,6 +288,17 @@ export class MappingService {
267288
return this.createLookupResult(true, symbol, symbol);
268289
}
269290

291+
// Unobfuscated versions (26.1+) have no mapping files — lookups are not possible.
292+
const isUnobfuscated = await this.versionManager.isVersionUnobfuscated(version);
293+
if (isUnobfuscated) {
294+
throw new MappingNotFoundError(
295+
version,
296+
`${sourceMapping}->${targetMapping}`,
297+
`Mapping lookup is not available for unobfuscated version ${version}. ` +
298+
`The source code is already in Mojang's human-readable names — no mapping translation is needed.`,
299+
);
300+
}
301+
270302
// Determine routing strategy
271303
const singleFile = this.getSingleFileLookup(sourceMapping, targetMapping);
272304

0 commit comments

Comments
 (0)