Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 61 additions & 1 deletion __tests__/core/mapping-service.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { existsSync, readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
import { getMappingService } from '../../src/services/mapping-service.js';
import { TEST_MAPPING, TEST_VERSION } from '../test-constants.js';
import { TEST_MAPPING, TEST_VERSION, UNOBFUSCATED_TEST_VERSION } from '../test-constants.js';

/**
* Mapping Service Tests
Expand Down Expand Up @@ -434,3 +434,63 @@ describe('Mojmap Tiny v2 Structure Verification', () => {
expect(firstLine).toContain('named');
}, 180000);
});

/**
* Unobfuscated version handling (26.1+)
*
* Unobfuscated Minecraft versions ship JARs without obfuscation.
* No intermediary, yarn, or mojmap mapping files exist for these versions.
* MappingService.getMappings() and lookupMapping() must fail with clear,
* actionable error messages instead of cryptic download failures.
*
* Reproduces: https://github.com/MCDxAI/minecraft-dev-mcp/issues/5
*/
describe('Unobfuscated version handling', () => {
it('should throw actionable error for getMappings(intermediary) on unobfuscated version', async () => {
const mappingService = getMappingService();
await expect(
mappingService.getMappings(UNOBFUSCATED_TEST_VERSION, 'intermediary'),
).rejects.toThrow(/unobfuscated.*mojmap/is);
}, 30000);

it('should throw actionable error for getMappings(yarn) on unobfuscated version', async () => {
const mappingService = getMappingService();
await expect(
mappingService.getMappings(UNOBFUSCATED_TEST_VERSION, 'yarn'),
).rejects.toThrow(/unobfuscated.*mojmap/is);
}, 30000);

it('should throw actionable error for getMappings(mojmap) on unobfuscated version', async () => {
const mappingService = getMappingService();
await expect(
mappingService.getMappings(UNOBFUSCATED_TEST_VERSION, 'mojmap'),
).rejects.toThrow(/unobfuscated.*already in Mojang/is);
}, 30000);

it('should throw actionable error for lookupMapping on unobfuscated version', async () => {
const mappingService = getMappingService();
// lookupMapping calls getMappings internally, which throws for unobfuscated versions
await expect(
mappingService.lookupMapping(
UNOBFUSCATED_TEST_VERSION,
'Entity',
'mojmap',
'yarn',
),
).rejects.toThrow(/unobfuscated/i);
}, 30000);

it('should allow same-type lookupMapping on unobfuscated version (identity)', async () => {
const mappingService = getMappingService();
// Same source and target mapping should still return identity (no mapping file needed)
const result = await mappingService.lookupMapping(
UNOBFUSCATED_TEST_VERSION,
'net/minecraft/world/entity/Entity',
'mojmap',
'mojmap',
);
expect(result.found).toBe(true);
expect(result.source).toBe('net/minecraft/world/entity/Entity');
expect(result.target).toBe('net/minecraft/world/entity/Entity');
}, 10000);
});
11 changes: 11 additions & 0 deletions __tests__/core/version-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,16 @@ describe('Version Management', () => {
const result = await versionManager.isVersionUnobfuscated('26.1-snapshot-9');
expect(result).toBe(true);
}, 30000);

// Regression tests for https://github.com/MCDxAI/minecraft-dev-mcp/issues/5
it.each([
'26.1-snapshot-10',
'26.1-snapshot-11',
'26.1-rc-3',
])('should return true for %s (issue #5)', async (version) => {
const versionManager = getVersionManager();
const result = await versionManager.isVersionUnobfuscated(version);
expect(result).toBe(true);
}, 30000);
});
});
14 changes: 14 additions & 0 deletions __tests__/tools/core-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,20 @@ describe('Decompile and Remap Tools', () => {
expect(text).toMatch(/use ['"]mojmap['"] mapping/i);
}, 120000);

it('should return actionable error for find_mapping on unobfuscated version', async () => {
const result = await handleFindMapping({
symbol: 'Entity',
version: UNOBFUSCATED_TEST_VERSION,
sourceMapping: 'mojmap',
targetMapping: 'yarn',
});

expect(result).toBeDefined();
expect(result.isError).toBe(true);
const text = result.content[0].text;
expect(text).toMatch(/unobfuscated/i);
}, 60000);

it('should handle remap_mod_jar with Fabric mod', async () => {
// Skip if fixture doesn't exist
if (!existsSync(METEOR_JAR_PATH)) {
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@mcdxai/minecraft-dev-mcp",
"version": "1.0.0",
"version": "1.1.0",
"description": "MCP server for Minecraft mod development - decompile, remap, and explore Minecraft source code",
"type": "module",
"main": "./dist/index.js",
Expand Down
31 changes: 31 additions & 0 deletions src/services/mapping-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { MappingNotFoundError } from '../utils/errors.js';
import { ensureDir } from '../utils/file-utils.js';
import { logger } from '../utils/logger.js';
import { getMojmapTinyPath } from '../utils/paths.js';
import { getVersionManager } from './version-manager.js';

/**
* Manages mapping downloads and caching
Expand All @@ -19,6 +20,7 @@ export class MappingService {
private mojangDownloader = getMojangDownloader();
private fabricMaven = getFabricMaven();
private cache = getCacheManager();
private versionManager = getVersionManager();

// Lock to prevent concurrent downloads of the same mappings
private downloadLocks = new Map<string, Promise<string>>();
Expand All @@ -45,6 +47,9 @@ export class MappingService {
return existingDownload;
}

// Unobfuscated versions (26.1+) have no mapping files — check before attempting download.
await this.throwIfUnobfuscated(version, mappingType);
Comment thread
GhostTypes marked this conversation as resolved.
Outdated

// Download and convert Mojmap with lock
const downloadPromise = this.downloadAndConvertMojmap(version);
this.downloadLocks.set(lockKey, downloadPromise);
Expand All @@ -69,6 +74,9 @@ export class MappingService {
return existingDownload;
}

// Unobfuscated versions (26.1+) have no mapping files — check before attempting download.
await this.throwIfUnobfuscated(version, mappingType);
Comment thread
GhostTypes marked this conversation as resolved.
Outdated
Comment thread
GhostTypes marked this conversation as resolved.
Outdated

// Download based on type with lock
logger.info(`Downloading ${mappingType} mappings for ${version}`);
let downloadPromise: Promise<string>;
Expand Down Expand Up @@ -227,6 +235,29 @@ export class MappingService {
// Intermediary should exist for all Fabric-supported versions
}

/**
* Throw a clear error if the version is unobfuscated and no mapping files exist.
* Called just before attempting a download, AFTER cache checks, so that cached
* mappings still work without hitting the network.
*/
private async throwIfUnobfuscated(version: string, mappingType: MappingType): Promise<void> {
const isUnobfuscated = await this.versionManager.isVersionUnobfuscated(version);
if (!isUnobfuscated) return;

if (mappingType === 'mojmap') {
throw new MappingNotFoundError(
version,
mappingType,
`Mojmap mapping files are not available for unobfuscated version ${version}. The JAR is already in Mojang's human-readable names — decompile it directly with mapping 'mojmap'.`,
);
}
throw new MappingNotFoundError(
version,
mappingType,
`${mappingType} mappings are not available for unobfuscated version ${version}. Use 'mojmap' mapping instead — the JAR ships without obfuscation.`,
);
}

/**
* Lookup result type
*/
Expand Down
Loading