Skip to content

Commit 0b6b0b5

Browse files
authored
feat: setup mcp server automatically on install (#48)
* feat: add mcp client support, add cursor client * feat: automatically add mcp servers + mcp client support for cursor * fix: remove spare arg * feat: add client for claude desktop * fix: import correctly * chore: add some tests * chore: remove testing defaults * feat: ask for personal api keys, redirect to api key preset * chore: add mcp setup to all frameworks * fix: remove dead test
1 parent 1a00b4c commit 0b6b0b5

File tree

21 files changed

+1623
-67
lines changed

21 files changed

+1623
-67
lines changed

bin.ts

Lines changed: 88 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,93 @@ if (!satisfies(process.version, NODE_VERSION_RANGE)) {
1515
);
1616
process.exit(1);
1717
}
18-
import { run } from './src/run';
1918

20-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
21-
const argv = yargs(hideBin(process.argv)).options({
22-
debug: {
23-
default: false,
24-
describe: 'Enable verbose logging\nenv: POSTHOG_WIZARD_DEBUG',
25-
type: 'boolean',
26-
},
27-
}).argv;
19+
import { runMCPInstall, runMCPRemove } from './src/mcp';
20+
import type { CloudRegion, WizardOptions } from './src/utils/types';
21+
import { runWizard } from './src/run';
2822

29-
void run(argv);
23+
yargs(hideBin(process.argv))
24+
// global options
25+
.options({
26+
debug: {
27+
default: false,
28+
describe: 'Enable verbose logging\nenv: POSTHOG_WIZARD_DEBUG',
29+
type: 'boolean',
30+
},
31+
region: {
32+
describe: 'PostHog cloud region\nenv: POSTHOG_WIZARD_REGION',
33+
choices: ['us', 'eu'],
34+
type: 'string',
35+
},
36+
default: {
37+
default: false,
38+
describe:
39+
'Use default options for all prompts\nenv: POSTHOG_WIZARD_DEFAULT',
40+
type: 'boolean',
41+
},
42+
signup: {
43+
default: false,
44+
describe:
45+
'Create a new PostHog account during setup\nenv: POSTHOG_WIZARD_SIGNUP',
46+
type: 'boolean',
47+
},
48+
})
49+
.command(
50+
['$0'],
51+
'Run the PostHog setup wizard',
52+
(yargs) => {
53+
return yargs.options({
54+
'force-install': {
55+
default: false,
56+
describe:
57+
'Force install packages even if peer dependency checks fail\nenv: POSTHOG_WIZARD_FORCE_INSTALL',
58+
type: 'boolean',
59+
},
60+
'install-dir': {
61+
describe:
62+
'Directory to install PostHog in\nenv: POSTHOG_WIZARD_INSTALL_DIR',
63+
type: 'string',
64+
},
65+
integration: {
66+
describe: 'Integration to set up',
67+
choices: ['nextjs', 'react', 'svelte', 'react-native'],
68+
type: 'string',
69+
},
70+
});
71+
},
72+
(argv) => {
73+
void runWizard(argv as unknown as WizardOptions);
74+
},
75+
)
76+
.command('mcp <command>', 'MCP server management commands', (yargs) => {
77+
return yargs
78+
.command(
79+
'add',
80+
'Install PostHog MCP server to supported clients',
81+
(yargs) => {
82+
return yargs.options({});
83+
},
84+
(argv) => {
85+
void runMCPInstall(
86+
argv as unknown as { signup: boolean; region?: CloudRegion },
87+
);
88+
},
89+
)
90+
.command(
91+
'remove',
92+
'Remove PostHog MCP server from supported clients',
93+
(yargs) => {
94+
return yargs.options({});
95+
},
96+
() => {
97+
void runMCPRemove();
98+
},
99+
)
100+
.demandCommand(1, 'You must specify a subcommand (add or remove)')
101+
.help();
102+
})
103+
.help()
104+
.alias('help', 'h')
105+
.version()
106+
.alias('version', 'v')
107+
.wrap(yargs.terminalWidth()).argv;

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"fast-glob": "^3.3.3",
4141
"glob": "9.3.5",
4242
"inquirer": "^6.2.0",
43+
"lodash": "^4.17.21",
4344
"magicast": "^0.2.10",
4445
"opn": "^5.4.0",
4546
"posthog-node": "^4.9.0",
@@ -59,7 +60,7 @@
5960
"@types/glob": "^7.2.0",
6061
"@types/inquirer": "^0.0.43",
6162
"@types/jest": "^29.5.14",
62-
"@types/lodash": "^4.14.144",
63+
"@types/lodash": "^4.17.15",
6364
"@types/node": "^18.19.76",
6465
"@types/opn": "5.1.0",
6566
"@types/rimraf": "^3.0.2",

pnpm-lock.yaml

Lines changed: 4 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/helper-functions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const sleep = (ms: number) =>
2+
new Promise((resolve) => setTimeout(resolve, ms));

src/mcp.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import chalk from 'chalk';
2+
import {
3+
addMCPServerToClientsStep,
4+
removeMCPServerFromClientsStep,
5+
} from './steps/add-mcp-server-to-clients';
6+
import clack from './utils/clack';
7+
import { abort, askForCloudRegion } from './utils/clack-utils';
8+
import type { CloudRegion } from './utils/types';
9+
import opn from 'opn';
10+
import { getCloudUrlFromRegion } from './utils/urls';
11+
import { sleep } from './lib/helper-functions';
12+
13+
export const runMCPInstall = async (options: {
14+
signup: boolean;
15+
region?: CloudRegion;
16+
}) => {
17+
clack.intro('Installing the PostHog MCP server.');
18+
19+
await addMCPServerToClientsStep({
20+
cloudRegion: options.region,
21+
askPermission: false,
22+
});
23+
24+
clack.outro(`${chalk.green(
25+
'You might need to restart your MCP clients to see the changes.',
26+
)}
27+
28+
Get started with some prompts like:
29+
30+
- What feature flags do I have active?
31+
- Add a new feature flag for our homepage redesign
32+
- What are my most common errors?
33+
`);
34+
};
35+
36+
export const runMCPRemove = async () => {
37+
const results = await removeMCPServerFromClientsStep({});
38+
39+
if (results.length === 0) {
40+
clack.outro(`No PostHog MCP servers found to remove.`);
41+
return;
42+
}
43+
44+
clack.outro(`PostHog MCP server removed from:
45+
${results.map((c) => `- ${c}`).join('\n ')}
46+
47+
${chalk.green(
48+
'You might need to restart your MCP clients to see the changes.',
49+
)}`);
50+
};
51+
52+
export const getPersonalApiKey = async (options: {
53+
region?: CloudRegion;
54+
}): Promise<string> => {
55+
const cloudRegion = options.region ?? (await askForCloudRegion());
56+
57+
const cloudUrl = getCloudUrlFromRegion(cloudRegion);
58+
59+
const urlToOpen = `${cloudUrl}/settings/user-api-keys?preset=mcp_server`;
60+
61+
const spinner = clack.spinner();
62+
spinner.start(
63+
`Opening your project settings so you can get a Personal API key...`,
64+
);
65+
66+
await sleep(1500);
67+
68+
spinner.stop(
69+
`Opened your project settings. If the link didn't open automatically, open the following URL in your browser to get a Personal API key: \n\n${chalk.cyan(
70+
urlToOpen,
71+
)}`,
72+
);
73+
74+
opn(urlToOpen, { wait: false }).catch(() => {
75+
// opn throws in environments that don't have a browser (e.g. remote shells) so we just noop here
76+
});
77+
78+
const personalApiKey = await clack.password({
79+
message: 'Paste in your Personal API key:',
80+
validate(value) {
81+
if (value.length === 0) return `Value is required!`;
82+
83+
if (!value.startsWith('phx_')) {
84+
return `That doesn't look right, are you sure you copied the right key? It should start with 'phx_'`;
85+
}
86+
},
87+
});
88+
89+
if (!personalApiKey) {
90+
await abort('Unable to proceed without a personal API key.');
91+
return '';
92+
}
93+
94+
return personalApiKey as string;
95+
};

src/nextjs/nextjs-wizard.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,9 @@ import {
3535
addOrUpdateEnvironmentVariablesStep,
3636
createPRStep,
3737
runPrettierStep,
38+
addMCPServerToClientsStep,
39+
uploadEnvironmentVariablesStep,
3840
} from '../steps';
39-
import { uploadEnvironmentVariablesStep } from '../steps/upload-environment-variables';
4041
export async function runNextjsWizard(options: WizardOptions): Promise<void> {
4142
printWelcome({
4243
wizardName: 'PostHog Next.js wizard',
@@ -163,7 +164,6 @@ export async function runNextjsWizard(options: WizardOptions): Promise<void> {
163164
rulesName: 'next-rules.md',
164165
installDir: options.installDir,
165166
integration: Integration.nextjs,
166-
default: options.default,
167167
});
168168

169169
const prUrl = await createPRStep({
@@ -172,6 +172,11 @@ export async function runNextjsWizard(options: WizardOptions): Promise<void> {
172172
addedEditorRules,
173173
});
174174

175+
await addMCPServerToClientsStep({
176+
cloudRegion,
177+
integration: Integration.nextjs,
178+
});
179+
175180
const outroMessage = getOutroMessage({
176181
options,
177182
integration: Integration.nextjs,

src/react-native/react-native-wizard.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ import {
2424
} from '../utils/file-utils';
2525
import type { WizardOptions } from '../utils/types';
2626
import { askForCloudRegion } from '../utils/clack-utils';
27-
import { addEditorRulesStep, runPrettierStep } from '../steps';
27+
import {
28+
addEditorRulesStep,
29+
addMCPServerToClientsStep,
30+
runPrettierStep,
31+
} from '../steps';
2832
import { EXPO } from '../utils/package-manager';
2933
import { getOutroMessage } from '../lib/messages';
3034

@@ -150,7 +154,11 @@ export async function runReactNativeWizard(
150154
installDir: options.installDir,
151155
rulesName: 'react-native-rules.md',
152156
integration: Integration.reactNative,
153-
default: options.default,
157+
});
158+
159+
await addMCPServerToClientsStep({
160+
cloudRegion,
161+
integration: Integration.reactNative,
154162
});
155163

156164
const packageManagerForOutro = await getPackageManager({

src/react/react-wizard.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { askForCloudRegion } from '../utils/clack-utils';
2828
import { getOutroMessage } from '../lib/messages';
2929
import {
3030
addEditorRulesStep,
31+
addMCPServerToClientsStep,
3132
addOrUpdateEnvironmentVariablesStep,
3233
runPrettierStep,
3334
} from '../steps';
@@ -136,7 +137,6 @@ export async function runReactWizard(options: WizardOptions): Promise<void> {
136137
installDir: options.installDir,
137138
rulesName: 'react-rules.md',
138139
integration: Integration.react,
139-
default: options.default,
140140
});
141141

142142
const uploadedEnvVars = await uploadEnvironmentVariablesStep(
@@ -150,6 +150,11 @@ export async function runReactWizard(options: WizardOptions): Promise<void> {
150150
},
151151
);
152152

153+
await addMCPServerToClientsStep({
154+
cloudRegion,
155+
integration: Integration.react,
156+
});
157+
153158
const outroMessage = getOutroMessage({
154159
options,
155160
integration: Integration.react,

src/run.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,7 @@ type Args = {
2727
signup?: boolean;
2828
};
2929

30-
export async function run(argv: Args) {
31-
await runWizard(argv);
32-
}
33-
34-
async function runWizard(argv: Args) {
30+
export async function runWizard(argv: Args) {
3531
const finalArgs = {
3632
...argv,
3733
...readEnvironment(),

src/steps/__tests__/add-editor-rules.test.ts

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -242,17 +242,4 @@ A given feature flag should be used in as few places as possible. Do not increas
242242
)}`,
243243
);
244244
});
245-
246-
it('should not install rules when user declines', async () => {
247-
process.env.CURSOR_TRACE_ID = 'test-trace-id';
248-
selectMock.mockResolvedValue(false);
249-
250-
await addEditorRulesStep(mockOptions);
251-
252-
expect(mkdirMock).not.toHaveBeenCalled();
253-
expect(readFileMock).not.toHaveBeenCalled();
254-
expect(writeFileMock).not.toHaveBeenCalled();
255-
expect(captureMock).not.toHaveBeenCalled();
256-
expect(infoMock).not.toHaveBeenCalled();
257-
});
258245
});

0 commit comments

Comments
 (0)