Skip to content

Commit 2a09dcc

Browse files
authored
fix: remove parsing from mcp configs (#56)
1 parent 7bacf4a commit 2a09dcc

File tree

7 files changed

+194
-768
lines changed

7 files changed

+194
-768
lines changed
Lines changed: 97 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,101 @@
1+
import * as fs from 'fs';
2+
import * as path from 'path';
3+
import { getDefaultServerConfig } from './defaults';
4+
import { merge } from 'lodash';
5+
16
export abstract class MCPClient {
27
name: string;
8+
abstract getConfigPath(): Promise<string>;
39
abstract isServerInstalled(): Promise<boolean>;
4-
abstract addServer(apiKey: string): Promise<void>;
5-
abstract removeServer(): Promise<void>;
6-
abstract isClientSupported(): boolean;
10+
abstract addServer(apiKey: string): Promise<{ success: boolean }>;
11+
abstract removeServer(): Promise<{ success: boolean }>;
12+
abstract isClientSupported(): Promise<boolean>;
13+
}
14+
15+
export abstract class DefaultMCPClient extends MCPClient {
16+
name = 'Default';
17+
18+
constructor() {
19+
super();
20+
}
21+
async isServerInstalled(): Promise<boolean> {
22+
try {
23+
const configPath = await this.getConfigPath();
24+
25+
if (!fs.existsSync(configPath)) {
26+
return false;
27+
}
28+
29+
const configContent = await fs.promises.readFile(configPath, 'utf8');
30+
const config = JSON.parse(configContent);
31+
32+
return 'mcpServers' in config && 'posthog' in config.mcpServers;
33+
} catch {
34+
return false;
35+
}
36+
}
37+
38+
async addServer(apiKey: string): Promise<{ success: boolean }> {
39+
try {
40+
const configPath = await this.getConfigPath();
41+
const configDir = path.dirname(configPath);
42+
43+
await fs.promises.mkdir(configDir, { recursive: true });
44+
45+
const newServerConfig = {
46+
mcpServers: {
47+
posthog: getDefaultServerConfig(apiKey),
48+
},
49+
};
50+
51+
let existingConfig = {};
52+
53+
if (fs.existsSync(configPath)) {
54+
const existingContent = await fs.promises.readFile(configPath, 'utf8');
55+
existingConfig = JSON.parse(existingContent);
56+
}
57+
58+
const mergedConfig = merge({}, existingConfig, newServerConfig);
59+
60+
await fs.promises.writeFile(
61+
configPath,
62+
JSON.stringify(mergedConfig, null, 2),
63+
'utf8',
64+
);
65+
66+
return { success: true };
67+
} catch {
68+
//
69+
}
70+
return { success: false };
71+
}
72+
73+
async removeServer(): Promise<{ success: boolean }> {
74+
try {
75+
const configPath = await this.getConfigPath();
76+
77+
if (!fs.existsSync(configPath)) {
78+
return { success: false };
79+
}
80+
81+
const configContent = await fs.promises.readFile(configPath, 'utf8');
82+
const config = JSON.parse(configContent);
83+
84+
if ('mcpServers' in config && 'posthog' in config.mcpServers) {
85+
delete config.mcpServers.posthog;
86+
87+
await fs.promises.writeFile(
88+
configPath,
89+
JSON.stringify(config, null, 2),
90+
'utf8',
91+
);
92+
93+
return { success: true };
94+
}
95+
} catch {
96+
//
97+
}
98+
99+
return { success: false };
100+
}
7101
}

src/steps/add-mcp-server-to-clients/clients/__tests__/claude.test.ts

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// We use the ClaudeMCPClient as a reference to test the DefaultMCPClient
12
import * as fs from 'fs';
23
import * as path from 'path';
34
import * as os from 'os';
@@ -68,47 +69,47 @@ describe('ClaudeMCPClient', () => {
6869
});
6970

7071
describe('isClientSupported', () => {
71-
it('should return true for macOS', () => {
72+
it('should return true for macOS', async () => {
7273
Object.defineProperty(process, 'platform', {
7374
value: 'darwin',
7475
writable: true,
7576
});
76-
expect(client.isClientSupported()).toBe(true);
77+
await expect(client.isClientSupported()).resolves.toBe(true);
7778
});
7879

79-
it('should return true for Windows', () => {
80+
it('should return true for Windows', async () => {
8081
Object.defineProperty(process, 'platform', {
8182
value: 'win32',
8283
writable: true,
8384
});
84-
expect(client.isClientSupported()).toBe(true);
85+
await expect(client.isClientSupported()).resolves.toBe(true);
8586
});
8687

87-
it('should return false for Linux', () => {
88+
it('should return false for Linux', async () => {
8889
Object.defineProperty(process, 'platform', {
8990
value: 'linux',
9091
writable: true,
9192
});
92-
expect(client.isClientSupported()).toBe(false);
93+
await expect(client.isClientSupported()).resolves.toBe(false);
9394
});
9495

95-
it('should return false for other platforms', () => {
96+
it('should return false for other platforms', async () => {
9697
Object.defineProperty(process, 'platform', {
9798
value: 'freebsd',
9899
writable: true,
99100
});
100-
expect(client.isClientSupported()).toBe(false);
101+
await expect(client.isClientSupported()).resolves.toBe(false);
101102
});
102103
});
103104

104105
describe('getConfigPath', () => {
105-
it('should return correct path for macOS', () => {
106+
it('should return correct path for macOS', async () => {
106107
Object.defineProperty(process, 'platform', {
107108
value: 'darwin',
108109
writable: true,
109110
});
110111

111-
const configPath = (client as any).getConfigPath();
112+
const configPath = await client.getConfigPath();
112113
expect(configPath).toBe(
113114
path.join(
114115
mockHomeDir,
@@ -120,7 +121,7 @@ describe('ClaudeMCPClient', () => {
120121
);
121122
});
122123

123-
it('should return correct path for Windows', () => {
124+
it('should return correct path for Windows', async () => {
124125
Object.defineProperty(process, 'platform', {
125126
value: 'win32',
126127
writable: true,
@@ -129,19 +130,19 @@ describe('ClaudeMCPClient', () => {
129130
const mockAppData = 'C:\\Users\\Test\\AppData\\Roaming';
130131
process.env.APPDATA = mockAppData;
131132

132-
const configPath = (client as any).getConfigPath();
133+
const configPath = await client.getConfigPath();
133134
expect(configPath).toBe(
134135
path.join(mockAppData, 'Claude', 'claude_desktop_config.json'),
135136
);
136137
});
137138

138-
it('should throw error for unsupported platform', () => {
139+
it('should throw error for unsupported platform', async () => {
139140
Object.defineProperty(process, 'platform', {
140141
value: 'linux',
141142
writable: true,
142143
});
143144

144-
expect(() => (client as any).getConfigPath()).toThrow(
145+
await expect(client.getConfigPath()).rejects.toThrow(
145146
'Unsupported platform: linux',
146147
);
147148
});
@@ -231,6 +232,7 @@ describe('ClaudeMCPClient', () => {
231232
expect(mkdirMock).toHaveBeenCalledWith(expectedConfigDir, {
232233
recursive: true,
233234
});
235+
234236
expect(writeFileMock).toHaveBeenCalledWith(
235237
expectedConfigPath,
236238
JSON.stringify(
@@ -277,16 +279,35 @@ describe('ClaudeMCPClient', () => {
277279
);
278280
});
279281

280-
it('should create new config when existing config is invalid', async () => {
282+
it('should not overwrite existing config when it is invalid', async () => {
281283
existsSyncMock.mockReturnValue(true);
282-
readFileMock.mockResolvedValue('invalid json');
284+
readFileMock.mockResolvedValue(
285+
JSON.stringify({
286+
invalidKey: {
287+
existingServer: {
288+
command: 'existing',
289+
args: [],
290+
env: {},
291+
},
292+
},
293+
x: 'y',
294+
}),
295+
);
283296

284297
await client.addServer(mockApiKey);
285298

286299
expect(writeFileMock).toHaveBeenCalledWith(
287300
expect.any(String),
288301
JSON.stringify(
289302
{
303+
invalidKey: {
304+
existingServer: {
305+
command: 'existing',
306+
args: [],
307+
env: {},
308+
},
309+
},
310+
x: 'y',
290311
mcpServers: {
291312
posthog: mockServerConfig,
292313
},

0 commit comments

Comments
 (0)