Skip to content

Commit f40e4be

Browse files
authored
feat(mcp): local mcp server management (#151)
1 parent b7955af commit f40e4be

8 files changed

Lines changed: 104 additions & 41 deletions

File tree

bin.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -154,23 +154,42 @@ yargs(hideBin(process.argv))
154154
'add',
155155
'Install PostHog MCP server to supported clients',
156156
(yargs) => {
157-
return yargs.options({});
157+
return yargs.options({
158+
local: {
159+
default: false,
160+
describe:
161+
'Add local development MCP server (http://localhost:8787)',
162+
type: 'boolean',
163+
},
164+
});
158165
},
159166
(argv) => {
160167
const options = { ...argv };
161168
void runMCPInstall(
162-
options as unknown as { signup: boolean; region?: CloudRegion },
169+
options as unknown as {
170+
signup: boolean;
171+
region?: CloudRegion;
172+
local?: boolean;
173+
},
163174
);
164175
},
165176
)
166177
.command(
167178
'remove',
168179
'Remove PostHog MCP server from supported clients',
169180
(yargs) => {
170-
return yargs.options({});
181+
return yargs.options({
182+
local: {
183+
default: false,
184+
describe:
185+
'Remove local development MCP server (http://localhost:8787)',
186+
type: 'boolean',
187+
},
188+
});
171189
},
172-
() => {
173-
void runMCPRemove();
190+
(argv) => {
191+
const options = { ...argv };
192+
void runMCPRemove(options as { local?: boolean });
174193
},
175194
)
176195
.demandCommand(1, 'You must specify a subcommand (add or remove)')

src/mcp.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,18 @@ import { sleep } from './lib/helper-functions';
1313
export const runMCPInstall = async (options: {
1414
signup: boolean;
1515
region?: CloudRegion;
16+
local?: boolean;
1617
}) => {
17-
clack.intro(chalk.bgGreenBright('Installing the PostHog MCP server'));
18+
clack.intro(
19+
chalk.bgGreenBright(
20+
`Installing the PostHog MCP server ${options.local && '(local)'}`,
21+
),
22+
);
1823

1924
await addMCPServerToClientsStep({
2025
cloudRegion: options.region,
2126
askPermission: false,
27+
local: options.local,
2228
});
2329

2430
clack.log.message(
@@ -36,9 +42,11 @@ export const runMCPInstall = async (options: {
3642
${chalk.blueBright(`https://posthog.com/docs/model-context-protocol`)}`);
3743
};
3844

39-
export const runMCPRemove = async () => {
45+
export const runMCPRemove = async (options?: { local?: boolean }) => {
4046
clack.intro(chalk.bgRed('Removing the PostHog MCP server'));
41-
const results = await removeMCPServerFromClientsStep({});
47+
const results = await removeMCPServerFromClientsStep({
48+
local: options?.local,
49+
});
4250

4351
if (results.length === 0) {
4452
clack.outro(`No PostHog MCP servers found to remove.`);

src/steps/add-mcp-server-to-clients/MCPClient.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ export abstract class MCPClient {
77
name: string;
88
abstract getConfigPath(): Promise<string>;
99
abstract getServerPropertyName(): string;
10-
abstract isServerInstalled(): Promise<boolean>;
10+
abstract isServerInstalled(local?: boolean): Promise<boolean>;
1111
abstract addServer(
1212
apiKey: string,
1313
selectedFeatures?: string[],
14+
local?: boolean,
1415
): Promise<{ success: boolean }>;
15-
abstract removeServer(): Promise<{ success: boolean }>;
16+
abstract removeServer(local?: boolean): Promise<{ success: boolean }>;
1617
abstract isClientSupported(): Promise<boolean>;
1718
}
1819

@@ -31,11 +32,12 @@ export abstract class DefaultMCPClient extends MCPClient {
3132
apiKey: string,
3233
type: 'sse' | 'streamable-http',
3334
selectedFeatures?: string[],
35+
local?: boolean,
3436
) {
35-
return getDefaultServerConfig(apiKey, type, selectedFeatures);
37+
return getDefaultServerConfig(apiKey, type, selectedFeatures, local);
3638
}
3739

38-
async isServerInstalled(): Promise<boolean> {
40+
async isServerInstalled(local?: boolean): Promise<boolean> {
3941
try {
4042
const configPath = await this.getConfigPath();
4143

@@ -46,8 +48,10 @@ export abstract class DefaultMCPClient extends MCPClient {
4648
const configContent = await fs.promises.readFile(configPath, 'utf8');
4749
const config = jsonc.parse(configContent) as Record<string, any>;
4850
const serverPropertyName = this.getServerPropertyName();
51+
const serverName = local ? 'posthog-local' : 'posthog';
52+
4953
return (
50-
serverPropertyName in config && 'posthog' in config[serverPropertyName]
54+
serverPropertyName in config && serverName in config[serverPropertyName]
5155
);
5256
} catch {
5357
return false;
@@ -57,14 +61,16 @@ export abstract class DefaultMCPClient extends MCPClient {
5761
async addServer(
5862
apiKey: string,
5963
selectedFeatures?: string[],
64+
local?: boolean,
6065
): Promise<{ success: boolean }> {
61-
return this._addServerType(apiKey, 'sse', selectedFeatures);
66+
return this._addServerType(apiKey, 'sse', selectedFeatures, local);
6267
}
6368

6469
async _addServerType(
6570
apiKey: string,
6671
type: 'sse' | 'streamable-http',
6772
selectedFeatures?: string[],
73+
local?: boolean,
6874
): Promise<{ success: boolean }> {
6975
try {
7076
const configPath = await this.getConfigPath();
@@ -85,16 +91,18 @@ export abstract class DefaultMCPClient extends MCPClient {
8591
apiKey,
8692
type,
8793
selectedFeatures,
94+
local,
8895
);
8996
const typedConfig = existingConfig as Record<string, any>;
9097
if (!typedConfig[serverPropertyName]) {
9198
typedConfig[serverPropertyName] = {};
9299
}
93-
typedConfig[serverPropertyName].posthog = newServerConfig;
100+
const serverName = local ? 'posthog-local' : 'posthog';
101+
typedConfig[serverPropertyName][serverName] = newServerConfig;
94102

95103
const edits = jsonc.modify(
96104
configContent,
97-
[serverPropertyName, 'posthog'],
105+
[serverPropertyName, serverName],
98106
newServerConfig,
99107
{
100108
formattingOptions: {
@@ -114,7 +122,7 @@ export abstract class DefaultMCPClient extends MCPClient {
114122
}
115123
}
116124

117-
async removeServer(): Promise<{ success: boolean }> {
125+
async removeServer(local?: boolean): Promise<{ success: boolean }> {
118126
try {
119127
const configPath = await this.getConfigPath();
120128

@@ -126,13 +134,15 @@ export abstract class DefaultMCPClient extends MCPClient {
126134
const config = jsonc.parse(configContent) as Record<string, any>;
127135
const serverPropertyName = this.getServerPropertyName();
128136

137+
const serverName = local ? 'posthog-local' : 'posthog';
138+
129139
if (
130140
serverPropertyName in config &&
131-
'posthog' in config[serverPropertyName]
141+
serverName in config[serverPropertyName]
132142
) {
133143
const edits = jsonc.modify(
134144
configContent,
135-
[serverPropertyName, 'posthog'],
145+
[serverPropertyName, serverName],
136146
undefined,
137147
{
138148
formattingOptions: {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ describe('ClaudeMCPClient', () => {
328328
mockApiKey,
329329
'sse',
330330
undefined,
331+
undefined,
331332
);
332333
});
333334
});

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

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,17 @@ export class ClaudeCodeMCPClient extends DefaultMCPClient {
2424
}
2525
}
2626

27-
isServerInstalled(): Promise<boolean> {
27+
isServerInstalled(local?: boolean): Promise<boolean> {
2828
try {
29-
// check if posthog in output
29+
// check if specific server name exists in output
3030
const output = execSync('claude mcp list', {
3131
stdio: 'pipe',
3232
});
3333

34-
if (output.toString().includes('posthog')) {
34+
const outputStr = output.toString();
35+
const serverName = local ? 'posthog-local' : 'posthog';
36+
37+
if (outputStr.includes(serverName)) {
3538
return Promise.resolve(true);
3639
}
3740
} catch {
@@ -48,10 +51,17 @@ export class ClaudeCodeMCPClient extends DefaultMCPClient {
4851
addServer(
4952
apiKey: string,
5053
selectedFeatures?: string[],
54+
local?: boolean,
5155
): Promise<{ success: boolean }> {
52-
const config = getDefaultServerConfig(apiKey, 'sse', selectedFeatures);
53-
54-
const command = `claude mcp add-json posthog -s user '${JSON.stringify(
56+
const config = getDefaultServerConfig(
57+
apiKey,
58+
'sse',
59+
selectedFeatures,
60+
local,
61+
);
62+
const serverName = local ? 'posthog-local' : 'posthog';
63+
64+
const command = `claude mcp add-json ${serverName} -s user '${JSON.stringify(
5565
config,
5666
)}'`;
5767

@@ -71,8 +81,9 @@ export class ClaudeCodeMCPClient extends DefaultMCPClient {
7181
return Promise.resolve({ success: true });
7282
}
7383

74-
removeServer(): Promise<{ success: boolean }> {
75-
const command = `claude mcp remove --scope user posthog`;
84+
removeServer(local?: boolean): Promise<{ success: boolean }> {
85+
const serverName = local ? 'posthog-local' : 'posthog';
86+
const command = `claude mcp remove --scope user ${serverName}`;
7687

7788
try {
7889
execSync(command);

src/steps/add-mcp-server-to-clients/clients/cursor.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ export class CursorMCPClient extends DefaultMCPClient {
2828
async addServer(
2929
apiKey: string,
3030
selectedFeatures?: string[],
31+
local?: boolean,
3132
): Promise<{ success: boolean }> {
32-
return this._addServerType(apiKey, 'sse', selectedFeatures);
33+
return this._addServerType(apiKey, 'sse', selectedFeatures, local);
3334
}
3435
}

src/steps/add-mcp-server-to-clients/defaults.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,10 @@ export const getDefaultServerConfig = (
6868
apiKey: string,
6969
type: MCPServerType,
7070
selectedFeatures?: string[],
71+
local?: boolean,
7172
) => {
72-
const baseUrl = `https://mcp.posthog.com/${type === 'sse' ? 'sse' : 'mcp'}`;
73+
const host = local ? 'localhost:8787' : 'mcp.posthog.com';
74+
const baseUrl = `${host}/${type === 'sse' ? 'sse' : 'mcp'}`;
7375

7476
const isAllFeaturesSelected =
7577
selectedFeatures &&

src/steps/add-mcp-server-to-clients/index.ts

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,21 @@ export const addMCPServerToClientsStep = async ({
3737
integration,
3838
cloudRegion,
3939
askPermission = true,
40+
local = false,
4041
}: {
4142
integration?: Integration;
4243
cloudRegion?: CloudRegion;
4344
askPermission?: boolean;
45+
local?: boolean;
4446
}): Promise<string[]> => {
4547
const region = cloudRegion ?? (await askForCloudRegion());
4648

4749
const hasPermission = askPermission
4850
? await abortIfCancelled(
4951
clack.select({
50-
message:
51-
'Would you like to install the MCP server to use PostHog in your editor?',
52+
message: local
53+
? 'Would you like to install the local development MCP server?'
54+
: 'Would you like to install the MCP server to use PostHog in your editor?',
5255
options: [
5356
{ value: true, label: 'Yes' },
5457
{ value: false, label: 'No' },
@@ -97,7 +100,7 @@ export const addMCPServerToClientsStep = async ({
97100
selectedClientNames.includes(client.name),
98101
);
99102

100-
const installedClients = await getInstalledClients();
103+
const installedClients = await getInstalledClients(local);
101104

102105
if (installedClients.length > 0) {
103106
clack.log.warn(
@@ -134,14 +137,14 @@ export const addMCPServerToClientsStep = async ({
134137
return [];
135138
}
136139

137-
await removeMCPServer(installedClients);
140+
await removeMCPServer(installedClients, local);
138141
clack.log.info('Removed existing installation.');
139142
}
140143

141144
const personalApiKey = await getPersonalApiKey({ cloudRegion: region });
142145

143146
await traceStep('adding mcp servers', async () => {
144-
await addMCPServer(clients, personalApiKey, selectedFeatures);
147+
await addMCPServer(clients, personalApiKey, selectedFeatures, local);
145148
});
146149

147150
clack.log.success(
@@ -160,10 +163,12 @@ export const addMCPServerToClientsStep = async ({
160163

161164
export const removeMCPServerFromClientsStep = async ({
162165
integration,
166+
local = false,
163167
}: {
164168
integration?: Integration;
169+
local?: boolean;
165170
}): Promise<string[]> => {
166-
const installedClients = await getInstalledClients();
171+
const installedClients = await getInstalledClients(local);
167172
if (installedClients.length === 0) {
168173
analytics.capture('wizard interaction', {
169174
action: 'no mcp servers to remove',
@@ -200,7 +205,7 @@ export const removeMCPServerFromClientsStep = async ({
200205
}
201206

202207
const results = await traceStep('removing mcp servers', async () => {
203-
await removeMCPServer(clientsToRemove);
208+
await removeMCPServer(clientsToRemove, local);
204209
return clientsToRemove.map((c) => c.name);
205210
});
206211

@@ -213,12 +218,14 @@ export const removeMCPServerFromClientsStep = async ({
213218
return results;
214219
};
215220

216-
export const getInstalledClients = async (): Promise<MCPClient[]> => {
221+
export const getInstalledClients = async (
222+
local?: boolean,
223+
): Promise<MCPClient[]> => {
217224
const clients = await getSupportedClients();
218225
const installedClients: MCPClient[] = [];
219226

220227
for (const client of clients) {
221-
if (await client.isServerInstalled()) {
228+
if (await client.isServerInstalled(local)) {
222229
installedClients.push(client);
223230
}
224231
}
@@ -230,14 +237,18 @@ export const addMCPServer = async (
230237
clients: MCPClient[],
231238
personalApiKey: string,
232239
selectedFeatures?: string[],
240+
local?: boolean,
233241
): Promise<void> => {
234242
for (const client of clients) {
235-
await client.addServer(personalApiKey, selectedFeatures);
243+
await client.addServer(personalApiKey, selectedFeatures, local);
236244
}
237245
};
238246

239-
export const removeMCPServer = async (clients: MCPClient[]): Promise<void> => {
247+
export const removeMCPServer = async (
248+
clients: MCPClient[],
249+
local?: boolean,
250+
): Promise<void> => {
240251
for (const client of clients) {
241-
await client.removeServer();
252+
await client.removeServer(local);
242253
}
243254
};

0 commit comments

Comments
 (0)