Skip to content

Commit b6aab99

Browse files
committed
feat: Add WSL/Windows cross-platform path support
Implement comprehensive bidirectional path translation between WSL and Windows environments, enabling seamless cross-platform usage of the MCP server. Changes: - Add path-converter utility for WSL ↔ Windows path translation - /mnt/c/... ↔ C:\... - /home/... ↔ \wsl$\Ubuntu\... - Add platform detection utilities (isWindowsHost, isWslHost) - Auto-normalize paths in Java process execution (JAR paths, args, cwd) - Update all MCP tools to accept both WSL and Windows paths: - remap_mod_jar, analyze_mod_jar, analyze_mixin, validate_access_widener - Add CACHE_DIR environment variable path normalization - Include comprehensive test suite for path conversion This enables users to work with the MCP server from either Windows or WSL environments without manually converting paths, and allows sharing a unified cache directory between both environments.
1 parent 13dc54d commit b6aab99

8 files changed

Lines changed: 810 additions & 42 deletions

File tree

.claude/settings.local.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,12 @@
2828
"Bash(head:*)",
2929
"Bash(npx vitest:*)",
3030
"Bash(git reset:*)",
31-
"Bash(npx tsx:*)"
31+
"Bash(npx tsx:*)",
32+
"Bash(git commit:*)"
3233
]
3334
},
3435
"enableAllProjectMcpServers": true,
35-
"enabledMcpjsonServers": ["minecraft-dev"]
36+
"enabledMcpjsonServers": [
37+
"minecraft-dev"
38+
]
3639
}
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2+
import {
3+
convertToWindowsPath,
4+
convertToWslPath,
5+
describePathFormat,
6+
isUncWslPath,
7+
isWindowsDrivePath,
8+
isWslMountPath,
9+
normalizePath,
10+
normalizeOptionalPath,
11+
validatePathFormat,
12+
} from '../../src/utils/path-converter.js';
13+
import { resetPlatformCache } from '../../src/utils/platform.js';
14+
15+
/**
16+
* Path Converter Tests
17+
*
18+
* Tests the bidirectional path translation between WSL and Windows paths.
19+
* Adapted from gradle-mcp-server path handling tests.
20+
*/
21+
22+
describe('Path Converter', () => {
23+
beforeEach(() => {
24+
// Reset platform cache before each test
25+
resetPlatformCache();
26+
});
27+
28+
afterEach(() => {
29+
// Restore environment after each test
30+
vi.unstubAllEnvs();
31+
resetPlatformCache();
32+
});
33+
34+
describe('isWindowsDrivePath', () => {
35+
it('should detect Windows drive paths with backslashes', () => {
36+
expect(isWindowsDrivePath('C:\\Users\\test')).toBe(true);
37+
expect(isWindowsDrivePath('D:\\project\\code')).toBe(true);
38+
expect(isWindowsDrivePath('E:\\')).toBe(true);
39+
});
40+
41+
it('should detect Windows drive paths with forward slashes', () => {
42+
expect(isWindowsDrivePath('C:/Users/test')).toBe(true);
43+
expect(isWindowsDrivePath('D:/project/code')).toBe(true);
44+
});
45+
46+
it('should not detect non-Windows paths', () => {
47+
expect(isWindowsDrivePath('/mnt/c/Users/test')).toBe(false);
48+
expect(isWindowsDrivePath('/home/user/project')).toBe(false);
49+
expect(isWindowsDrivePath('relative/path')).toBe(false);
50+
expect(isWindowsDrivePath('')).toBe(false);
51+
});
52+
});
53+
54+
describe('isWslMountPath', () => {
55+
it('should detect WSL mount paths', () => {
56+
expect(isWslMountPath('/mnt/c/Users/test')).toBe(true);
57+
expect(isWslMountPath('/mnt/d/project')).toBe(true);
58+
expect(isWslMountPath('/mnt/e/')).toBe(true);
59+
expect(isWslMountPath('/mnt/c')).toBe(true);
60+
});
61+
62+
it('should not detect non-WSL mount paths', () => {
63+
expect(isWslMountPath('C:\\Users\\test')).toBe(false);
64+
expect(isWslMountPath('/home/user/project')).toBe(false);
65+
expect(isWslMountPath('/mnt')).toBe(false);
66+
expect(isWslMountPath('/mnt/')).toBe(false);
67+
});
68+
});
69+
70+
describe('isUncWslPath', () => {
71+
it('should detect UNC WSL paths with backslashes', () => {
72+
expect(isUncWslPath('\\\\wsl$\\Ubuntu\\home\\user')).toBe(true);
73+
expect(isUncWslPath('\\\\wsl$\\Debian\\home')).toBe(true);
74+
});
75+
76+
it('should detect UNC WSL paths with forward slashes', () => {
77+
expect(isUncWslPath('//wsl$/Ubuntu/home/user')).toBe(true);
78+
expect(isUncWslPath('//wsl$/Debian/home')).toBe(true);
79+
});
80+
81+
it('should not detect non-UNC paths', () => {
82+
expect(isUncWslPath('C:\\Users\\test')).toBe(false);
83+
expect(isUncWslPath('/home/user')).toBe(false);
84+
expect(isUncWslPath('/mnt/c/Users')).toBe(false);
85+
});
86+
});
87+
88+
describe('convertToWindowsPath', () => {
89+
it('should convert /mnt paths to Windows paths', () => {
90+
expect(convertToWindowsPath('/mnt/c/Users/test')).toBe('C:\\Users\\test');
91+
expect(convertToWindowsPath('/mnt/d/project/code')).toBe('D:\\project\\code');
92+
expect(convertToWindowsPath('/mnt/e/')).toBe('E:\\');
93+
});
94+
95+
it('should handle native WSL paths with WSL$ UNC', () => {
96+
// Mock WSL_DISTRO_NAME environment variable
97+
vi.stubEnv('WSL_DISTRO_NAME', 'TestDistro');
98+
99+
const result = convertToWindowsPath('/home/user/project');
100+
expect(result).toBe('\\\\wsl$\\TestDistro\\home\\user\\project');
101+
});
102+
103+
it('should default to Ubuntu distro when WSL_DISTRO_NAME not set', () => {
104+
vi.stubEnv('WSL_DISTRO_NAME', '');
105+
106+
const result = convertToWindowsPath('/home/user/project');
107+
expect(result).toContain('\\\\wsl$\\Ubuntu\\');
108+
});
109+
110+
it('should pass through Windows paths unchanged (normalized)', () => {
111+
expect(convertToWindowsPath('C:\\Users\\test')).toBe('C:\\Users\\test');
112+
expect(convertToWindowsPath('D:\\project\\code')).toBe('D:\\project\\code');
113+
});
114+
115+
it('should normalize forward slashes to backslashes for Windows paths', () => {
116+
expect(convertToWindowsPath('C:/Users/test/project')).toBe('C:\\Users\\test\\project');
117+
});
118+
119+
it('should handle empty paths', () => {
120+
expect(convertToWindowsPath('')).toBe('');
121+
});
122+
123+
it('should handle root drive paths', () => {
124+
expect(convertToWindowsPath('/mnt/c')).toBe('C:\\');
125+
});
126+
});
127+
128+
describe('convertToWslPath', () => {
129+
it('should convert Windows drive letters to WSL paths', () => {
130+
expect(convertToWslPath('C:\\Users\\test\\proj')).toBe('/mnt/c/Users/test/proj');
131+
expect(convertToWslPath('D:/workspace')).toBe('/mnt/d/workspace');
132+
});
133+
134+
it('should convert UNC WSL paths back to Linux paths', () => {
135+
const result = convertToWslPath('\\\\wsl$\\Ubuntu-22.04\\home\\user\\repo');
136+
expect(result).toBe('/home/user/repo');
137+
});
138+
139+
it('should handle drive root paths', () => {
140+
expect(convertToWslPath('C:\\')).toBe('/mnt/c');
141+
expect(convertToWslPath('C:')).toBe('/mnt/c');
142+
});
143+
144+
it('should normalize backslashes to forward slashes', () => {
145+
expect(convertToWslPath('C:\\path\\to\\file')).toBe('/mnt/c/path/to/file');
146+
});
147+
148+
it('should handle empty paths', () => {
149+
expect(convertToWslPath('')).toBe('');
150+
});
151+
});
152+
153+
describe('normalizePath', () => {
154+
// Note: These tests verify the normalization logic works correctly
155+
// In actual execution, behavior depends on isWindowsHost() and isWslHost()
156+
157+
it('should handle empty paths', () => {
158+
expect(normalizePath('')).toBe('');
159+
});
160+
161+
it('should trim whitespace from paths', () => {
162+
const result = normalizePath(' /home/user/project ');
163+
// On Windows, Unix paths get converted to Windows UNC paths
164+
// On Linux/WSL, they stay as Unix paths
165+
// Either way, whitespace should be trimmed
166+
expect(result).not.toMatch(/^\s/);
167+
expect(result).not.toMatch(/\s$/);
168+
expect(result.length).toBeGreaterThan(0);
169+
});
170+
171+
it('should preserve well-formed paths', () => {
172+
// On Windows, WSL paths get converted; on non-Windows they stay as-is
173+
const path = '/home/user/project';
174+
const result = normalizePath(path);
175+
// Result depends on platform, but should be a valid path
176+
expect(result.length).toBeGreaterThan(0);
177+
});
178+
});
179+
180+
describe('normalizeOptionalPath', () => {
181+
it('should return undefined for empty paths', () => {
182+
expect(normalizeOptionalPath('')).toBeUndefined();
183+
expect(normalizeOptionalPath(undefined)).toBeUndefined();
184+
});
185+
186+
it('should return undefined for whitespace-only paths', () => {
187+
expect(normalizeOptionalPath(' ')).toBeUndefined();
188+
});
189+
190+
it('should normalize valid paths', () => {
191+
const result = normalizeOptionalPath('/home/user/project');
192+
expect(result).toBeDefined();
193+
expect(result!.length).toBeGreaterThan(0);
194+
});
195+
});
196+
197+
describe('validatePathFormat', () => {
198+
it('should reject empty paths', () => {
199+
expect(validatePathFormat('')).toBe('Path is required');
200+
expect(validatePathFormat(' ')).toBe('Path is required');
201+
});
202+
203+
it('should reject paths with null characters', () => {
204+
expect(validatePathFormat('/path/with\0null')).toBe('Path contains null character');
205+
});
206+
207+
it('should accept valid paths', () => {
208+
expect(validatePathFormat('/mnt/c/Users/test')).toBeUndefined();
209+
expect(validatePathFormat('C:\\Users\\test')).toBeUndefined();
210+
expect(validatePathFormat('/home/user')).toBeUndefined();
211+
});
212+
});
213+
214+
describe('describePathFormat', () => {
215+
it('should describe Windows drive paths', () => {
216+
expect(describePathFormat('C:\\Users\\test')).toBe('Windows drive path');
217+
expect(describePathFormat('D:/project')).toBe('Windows drive path');
218+
});
219+
220+
it('should describe WSL mount paths', () => {
221+
expect(describePathFormat('/mnt/c/Users/test')).toBe('WSL mount path');
222+
});
223+
224+
it('should describe UNC WSL paths', () => {
225+
expect(describePathFormat('\\\\wsl$\\Ubuntu\\home')).toBe('UNC WSL path');
226+
});
227+
228+
it('should describe Unix paths', () => {
229+
expect(describePathFormat('/home/user')).toBe('Unix path');
230+
});
231+
232+
it('should describe relative paths', () => {
233+
expect(describePathFormat('relative/path')).toBe('relative path');
234+
});
235+
236+
it('should describe empty paths', () => {
237+
expect(describePathFormat('')).toBe('empty path');
238+
});
239+
});
240+
});

0 commit comments

Comments
 (0)