Skip to content

Commit 68ca364

Browse files
authored
Merge pull request #7 from renovys/fix/windows-msix-config-path
fix(setup): 윈도우 MSIX 리다이렉션 때문에 앱이 읽지 않는 경로에 설정을 쓰던 문제
2 parents cb4e028 + a6c6bc6 commit 68ca364

4 files changed

Lines changed: 153 additions & 4 deletions

File tree

‎README-EN.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,11 +417,13 @@ Prompts for the OpenDART key once. 15 DART tools are live after that.
417417

418418
| App | Windows | Mac |
419419
|---|---|---|
420-
| Claude Desktop | `%APPDATA%\Claude\claude_desktop_config.json` | `~/Library/Application Support/Claude/claude_desktop_config.json` |
420+
| Claude Desktop | `%LOCALAPPDATA%\Packages\Claude_<publisher>\LocalCache\Roaming\Claude\claude_desktop_config.json` (see note) | `~/Library/Application Support/Claude/claude_desktop_config.json` |
421421
| Cursor | project `.cursor/mcp.json` | project `.cursor/mcp.json` |
422422
| Windsurf | project `.windsurf/mcp.json` | project `.windsurf/mcp.json` |
423423
| Claude Code | `~/.claude.json` or project `.mcp.json` | `~/.claude.json` or project `.mcp.json` |
424424

425+
> **Windows path note**: Claude Desktop ships as an MSIX package, so `%APPDATA%` is redirected to a package-private location. The file the app actually reads lives under `%LOCALAPPDATA%\Packages\Claude_<publisher>\LocalCache\Roaming\Claude\`; anything written to `%APPDATA%\Claude\` is ignored. `npx korean-dart-mcp setup` resolves this path automatically.
426+
425427
**Config content** (replace `YOUR_API_KEY`):
426428

427429
```json

‎README.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -476,11 +476,13 @@ npx -y korean-dart-mcp setup
476476
477477
| 앱 | Windows | Mac |
478478
|---|---|---|
479-
| Claude Desktop | `%APPDATA%\Claude\claude_desktop_config.json` | `~/Library/Application Support/Claude/claude_desktop_config.json` |
479+
| Claude Desktop | `%LOCALAPPDATA%\Packages\Claude_<publisher>\LocalCache\Roaming\Claude\claude_desktop_config.json` (아래 참고) | `~/Library/Application Support/Claude/claude_desktop_config.json` |
480480
| Cursor | 프로젝트 `.cursor/mcp.json` | 프로젝트 `.cursor/mcp.json` |
481481
| Windsurf | 프로젝트 `.windsurf/mcp.json` | 프로젝트 `.windsurf/mcp.json` |
482482
| Claude Code | `~/.claude.json` 또는 프로젝트 `.mcp.json` | `~/.claude.json` 또는 프로젝트 `.mcp.json` |
483483
484+
> **Windows 경로 주의**: Claude Desktop 은 MSIX 패키지라 `%APPDATA%` 가 패키지 전용 위치로 리다이렉트됩니다. 앱이 실제로 읽는 파일은 `%LOCALAPPDATA%\Packages\Claude_<publisher>\LocalCache\Roaming\Claude\` 아래에 있고, `%APPDATA%\Claude\` 에 써 두면 앱이 읽지 않습니다. `npx korean-dart-mcp setup` 은 이 경로를 자동으로 찾습니다.
485+
484486
**설정 내용:**
485487
486488
```json

‎src/setup.test.ts‎

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
2+
import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
3+
import { tmpdir } from "node:os";
4+
import { join, resolve } from "node:path";
5+
import { windowsClaudeConfigPath } from "./setup.js";
6+
7+
/**
8+
* 윈도우 Claude Desktop 은 MSIX 패키지라 앱 안에서 본 %APPDATA% 가 패키지 전용 위치로
9+
* 리다이렉트된다. setup 이 밖에서 보이는 %APPDATA%\Claude 에 써 두면 설치는 성공했다고
10+
* 나오는데 앱은 그 파일을 읽지 않아 서버가 나타나지 않는다.
11+
*
12+
* 실제 경로 선택은 환경변수와 디렉터리 존재 여부로만 갈리므로, 임시 디렉터리로 두 배치를
13+
* 만들어 어느 쪽을 고르는지 본다(플랫폼 무관).
14+
*/
15+
describe("windowsClaudeConfigPath", () => {
16+
let root: string;
17+
let home: string;
18+
const saved = { appData: process.env["APPDATA"], localAppData: process.env["LOCALAPPDATA"] };
19+
20+
beforeEach(() => {
21+
root = mkdtempSync(join(tmpdir(), "setup-path-test-"));
22+
home = join(root, "home");
23+
process.env["APPDATA"] = join(root, "Roaming");
24+
process.env["LOCALAPPDATA"] = join(root, "Local");
25+
mkdirSync(process.env["APPDATA"], { recursive: true });
26+
mkdirSync(process.env["LOCALAPPDATA"], { recursive: true });
27+
});
28+
29+
afterEach(() => {
30+
if (saved.appData === undefined) delete process.env["APPDATA"];
31+
else process.env["APPDATA"] = saved.appData;
32+
if (saved.localAppData === undefined) delete process.env["LOCALAPPDATA"];
33+
else process.env["LOCALAPPDATA"] = saved.localAppData;
34+
rmSync(root, { recursive: true, force: true });
35+
});
36+
37+
const roamingFallback = () =>
38+
resolve(process.env["APPDATA"]!, "Claude/claude_desktop_config.json");
39+
40+
it("MSIX 패키지 폴더가 있으면 앱이 실제로 읽는 경로를 고른다", () => {
41+
const pkgConfigDir = join(
42+
process.env["LOCALAPPDATA"]!,
43+
"Packages",
44+
"Claude_pzs8sxrjxfjjc",
45+
"LocalCache",
46+
"Roaming",
47+
"Claude",
48+
);
49+
mkdirSync(pkgConfigDir, { recursive: true });
50+
51+
expect(windowsClaudeConfigPath(home)).toBe(
52+
resolve(pkgConfigDir, "claude_desktop_config.json"),
53+
);
54+
});
55+
56+
it("Packages 폴더 자체가 없으면 %APPDATA% 를 쓴다", () => {
57+
expect(windowsClaudeConfigPath(home)).toBe(roamingFallback());
58+
});
59+
60+
it("Claude 패키지가 없으면 %APPDATA% 를 쓴다", () => {
61+
mkdirSync(join(process.env["LOCALAPPDATA"]!, "Packages", "SomeOther_abc123"), {
62+
recursive: true,
63+
});
64+
65+
expect(windowsClaudeConfigPath(home)).toBe(roamingFallback());
66+
});
67+
68+
it("최초 설치라 설정 디렉터리가 아직 없어도 그 패키지 자리를 쓴다", () => {
69+
// setup 을 처음 돌리는 사람이 정확히 이 상태다. 여기서 %APPDATA% 로 떨어지면
70+
// 이 수정이 고치려는 바로 그 증상이 그대로 남는다.
71+
const pkg = join(process.env["LOCALAPPDATA"]!, "Packages", "Claude_pzs8sxrjxfjjc");
72+
mkdirSync(pkg, { recursive: true });
73+
74+
expect(windowsClaudeConfigPath(home)).toBe(
75+
resolve(pkg, "LocalCache/Roaming/Claude/claude_desktop_config.json"),
76+
);
77+
});
78+
79+
it("후보가 여럿이면 설정 디렉터리가 있는 쪽을 고른다", () => {
80+
const packages = join(process.env["LOCALAPPDATA"]!, "Packages");
81+
mkdirSync(join(packages, "Claude_aaaaaaaaaaaaa", "LocalCache", "Roaming"), { recursive: true });
82+
const live = join(packages, "Claude_zzzzzzzzzzzzz", "LocalCache", "Roaming", "Claude");
83+
mkdirSync(live, { recursive: true });
84+
85+
expect(windowsClaudeConfigPath(home)).toBe(resolve(live, "claude_desktop_config.json"));
86+
});
87+
88+
it("후보가 여럿인데 가릴 단서가 없으면 %APPDATA% 를 쓴다", () => {
89+
const packages = join(process.env["LOCALAPPDATA"]!, "Packages");
90+
mkdirSync(join(packages, "Claude_aaaaaaaaaaaaa"), { recursive: true });
91+
mkdirSync(join(packages, "Claude_zzzzzzzzzzzzz"), { recursive: true });
92+
93+
expect(windowsClaudeConfigPath(home)).toBe(roamingFallback());
94+
});
95+
});

‎src/setup.ts‎

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import { createInterface } from "node:readline/promises";
1010
import { readFile, writeFile, mkdir } from "node:fs/promises";
11-
import { existsSync } from "node:fs";
11+
import { existsSync, readdirSync } from "node:fs";
1212
import { resolve, dirname } from "node:path";
1313
import { homedir, platform } from "node:os";
1414
import { stdin, stdout } from "node:process";
@@ -19,14 +19,64 @@ interface ClientConfig {
1919
readonly format: "mcpServers" | "servers" | "context_servers";
2020
}
2121

22+
/**
23+
* 윈도우 Claude Desktop 설정 파일 경로.
24+
*
25+
* 윈도우판은 Store 판이든 공식 다운로드판이든 MSIX 패키지라, 앱 안에서 본 %APPDATA% 가
26+
* 패키지 전용 위치로 리다이렉트된다. 앱이 실제로 읽는 파일은
27+
* `%LOCALAPPDATA%\Packages\Claude_<publisher>\LocalCache\Roaming\Claude\` 아래에 있고,
28+
* 밖에서 보이는 `%APPDATA%\Claude\` 가 아니다. 그쪽에 써 두면 설치는 성공했다고 나오는데
29+
* 앱에는 서버가 나타나지 않는다.
30+
*
31+
* 패키지 폴더가 있으면 그것을 쓰고, 없으면(MSIX 가 아닌 설치본) 기존 경로를 그대로 쓴다.
32+
*/
33+
export function windowsClaudeConfigPath(home: string = homedir()): string {
34+
const roaming = process.env["APPDATA"] ?? resolve(home, "AppData/Roaming");
35+
const fallback = resolve(roaming, "Claude/claude_desktop_config.json");
36+
37+
const localAppData = process.env["LOCALAPPDATA"] ?? resolve(home, "AppData/Local");
38+
const packagesDir = resolve(localAppData, "Packages");
39+
if (!existsSync(packagesDir)) return fallback;
40+
41+
let names: string[];
42+
try {
43+
names = readdirSync(packagesDir);
44+
} catch {
45+
return fallback;
46+
}
47+
48+
// 퍼블리셔 해시는 설치본마다 다를 수 있어 이름으로 못 박지 않고 접두사로 찾는다.
49+
const roots = names
50+
.filter((n) => n.startsWith("Claude_"))
51+
.sort()
52+
.map((n) => resolve(packagesDir, n, "LocalCache/Roaming"));
53+
if (roots.length === 0) return fallback;
54+
55+
const configPath = (root: string): string => resolve(root, "Claude/claude_desktop_config.json");
56+
57+
// 1) 앱이 이미 설정 디렉터리를 만들어 둔 후보가 가장 확실하다.
58+
const withConfigDir = roots.find((r) => existsSync(resolve(r, "Claude")));
59+
if (withConfigDir) return configPath(withConfigDir);
60+
61+
// 2) 설정은 아직 없어도 앱이 한 번이라도 돈 후보를 고른다.
62+
const withRoaming = roots.find((r) => existsSync(r));
63+
if (withRoaming) return configPath(withRoaming);
64+
65+
// 3) 최초 설치라 아무것도 없다. 후보가 하나뿐이면 그 자리를 쓴다(쓰기 단계가 디렉터리를 만든다).
66+
if (roots.length === 1) return configPath(roots[0]);
67+
68+
// 4) 후보가 여럿인데 어느 것이 현행인지 가릴 근거가 없다 — 기존 경로를 건드리지 않는다.
69+
return fallback;
70+
}
71+
2272
function detectClients(): readonly ClientConfig[] {
2373
const home = homedir();
2474
const os = platform();
2575
const clients: ClientConfig[] = [];
2676

2777
const claudePaths: Record<string, string> = {
2878
darwin: resolve(home, "Library/Application Support/Claude/claude_desktop_config.json"),
29-
win32: resolve(process.env["APPDATA"] ?? resolve(home, "AppData/Roaming"), "Claude/claude_desktop_config.json"),
79+
win32: windowsClaudeConfigPath(home),
3080
linux: resolve(home, ".config/Claude/claude_desktop_config.json"),
3181
};
3282
const claudePath = claudePaths[os];

0 commit comments

Comments
 (0)