Skip to content

Commit bd2a09d

Browse files
MattBroclaude
andauthored
feat: add region param and OAuth option for MCP setup (#220)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 7d12a39 commit bd2a09d

File tree

11 files changed

+265
-78
lines changed

11 files changed

+265
-78
lines changed

src/mcp.ts

Lines changed: 4 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,7 @@ import {
44
removeMCPServerFromClientsStep,
55
} from './steps/add-mcp-server-to-clients';
66
import clack from './utils/clack';
7-
import { abort } from './utils/clack-utils';
87
import type { CloudRegion } from './utils/types';
9-
import opn from 'opn';
10-
import { getCloudUrlFromRegion } from './utils/urls';
11-
import { sleep } from './lib/helper-functions';
128
import { enableDebugLogs } from './utils/debug';
139

1410
export const runMCPInstall = async (options: {
@@ -38,6 +34,10 @@ export const runMCPInstall = async (options: {
3834
)}`,
3935
);
4036

37+
clack.log.message(
38+
`You'll be prompted to log in to PostHog when you first use the MCP.`,
39+
);
40+
4141
clack.log.message(`Get started with some prompts like:
4242
- What feature flags do I have active?
4343
- Add a new feature flag for our homepage redesign
@@ -66,46 +66,3 @@ export const runMCPRemove = async (options?: { local?: boolean }) => {
6666
)}`,
6767
);
6868
};
69-
70-
export const getPersonalApiKey = async (options: {
71-
cloudRegion: CloudRegion;
72-
}): Promise<string> => {
73-
const cloudUrl = getCloudUrlFromRegion(options.cloudRegion);
74-
75-
const urlToOpen = `${cloudUrl}/settings/user-api-keys?preset=mcp_server`;
76-
77-
const spinner = clack.spinner();
78-
spinner.start(
79-
`Opening your project settings so you can get a Personal API key...`,
80-
);
81-
82-
await sleep(1500);
83-
84-
spinner.stop(
85-
`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(
86-
urlToOpen,
87-
)}`,
88-
);
89-
90-
opn(urlToOpen, { wait: false }).catch(() => {
91-
// opn throws in environments that don't have a browser (e.g. remote shells) so we just noop here
92-
});
93-
94-
const personalApiKey = await clack.password({
95-
message: 'Paste in your Personal API key:',
96-
validate(value) {
97-
if (value.length === 0) return `Value is required!`;
98-
99-
if (!value.startsWith('phx_')) {
100-
return `That doesn't look right, are you sure you copied the right key? It should start with 'phx_'`;
101-
}
102-
},
103-
});
104-
105-
if (!personalApiKey) {
106-
await abort('Unable to proceed without a personal API key.');
107-
return '';
108-
}
109-
110-
return personalApiKey as string;
111-
};

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

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as fs from 'fs';
22
import * as path from 'path';
33
import * as jsonc from 'jsonc-parser';
44
import { getDefaultServerConfig } from './defaults';
5+
import type { CloudRegion } from '../../utils/types';
56

67
export type MCPServerConfig = Record<string, unknown>;
78

@@ -11,9 +12,10 @@ export abstract class MCPClient {
1112
abstract getServerPropertyName(): string;
1213
abstract isServerInstalled(local?: boolean): Promise<boolean>;
1314
abstract addServer(
14-
apiKey: string,
15+
apiKey?: string,
1516
selectedFeatures?: string[],
1617
local?: boolean,
18+
region?: CloudRegion,
1719
): Promise<{ success: boolean }>;
1820
abstract removeServer(local?: boolean): Promise<{ success: boolean }>;
1921
abstract isClientSupported(): Promise<boolean>;
@@ -31,12 +33,19 @@ export abstract class DefaultMCPClient extends MCPClient {
3133
}
3234

3335
getServerConfig(
34-
apiKey: string,
36+
apiKey: string | undefined,
3537
type: 'sse' | 'streamable-http',
3638
selectedFeatures?: string[],
3739
local?: boolean,
40+
region?: CloudRegion,
3841
): MCPServerConfig {
39-
return getDefaultServerConfig(apiKey, type, selectedFeatures, local);
42+
return getDefaultServerConfig(
43+
apiKey,
44+
type,
45+
selectedFeatures,
46+
local,
47+
region,
48+
);
4049
}
4150

4251
async isServerInstalled(local?: boolean): Promise<boolean> {
@@ -61,18 +70,20 @@ export abstract class DefaultMCPClient extends MCPClient {
6170
}
6271

6372
async addServer(
64-
apiKey: string,
73+
apiKey?: string,
6574
selectedFeatures?: string[],
6675
local?: boolean,
76+
region?: CloudRegion,
6777
): Promise<{ success: boolean }> {
68-
return this._addServerType(apiKey, 'sse', selectedFeatures, local);
78+
return this._addServerType(apiKey, 'sse', selectedFeatures, local, region);
6979
}
7080

7181
async _addServerType(
72-
apiKey: string,
82+
apiKey: string | undefined,
7383
type: 'sse' | 'streamable-http',
7484
selectedFeatures?: string[],
7585
local?: boolean,
86+
region?: CloudRegion,
7687
): Promise<{ success: boolean }> {
7788
try {
7889
const configPath = await this.getConfigPath();
@@ -94,6 +105,7 @@ export abstract class DefaultMCPClient extends MCPClient {
94105
type,
95106
selectedFeatures,
96107
local,
108+
region,
97109
);
98110
const typedConfig = existingConfig as Record<string, any>;
99111
if (!typedConfig[serverPropertyName]) {
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import {
2+
buildMCPUrl,
3+
getDefaultServerConfig,
4+
getNativeHTTPServerConfig,
5+
} from '../defaults';
6+
7+
describe('defaults', () => {
8+
describe('buildMCPUrl', () => {
9+
it('should build base URL for streamable-http type', () => {
10+
const url = buildMCPUrl('streamable-http');
11+
expect(url).toBe('https://mcp.posthog.com/mcp');
12+
});
13+
14+
it('should build base URL for sse type', () => {
15+
const url = buildMCPUrl('sse');
16+
expect(url).toBe('https://mcp.posthog.com/sse');
17+
});
18+
19+
it('should use localhost for local mode', () => {
20+
const url = buildMCPUrl('streamable-http', undefined, true);
21+
expect(url).toBe('http://localhost:8787/mcp');
22+
});
23+
24+
it('should add features param when not all features selected', () => {
25+
const url = buildMCPUrl('streamable-http', ['dashboards', 'insights']);
26+
expect(url).toBe(
27+
'https://mcp.posthog.com/mcp?features=dashboards,insights',
28+
);
29+
});
30+
31+
it('should add region param for EU region', () => {
32+
const url = buildMCPUrl('streamable-http', undefined, false, 'eu');
33+
expect(url).toBe('https://mcp.posthog.com/mcp?region=eu');
34+
});
35+
36+
it('should not add region param for US region (default)', () => {
37+
const url = buildMCPUrl('streamable-http', undefined, false, 'us');
38+
expect(url).toBe('https://mcp.posthog.com/mcp');
39+
});
40+
41+
it('should not add region param in local mode', () => {
42+
const url = buildMCPUrl('streamable-http', undefined, true, 'eu');
43+
expect(url).toBe('http://localhost:8787/mcp');
44+
});
45+
46+
it('should combine features and region params', () => {
47+
const url = buildMCPUrl('streamable-http', ['dashboards'], false, 'eu');
48+
expect(url).toBe(
49+
'https://mcp.posthog.com/mcp?features=dashboards&region=eu',
50+
);
51+
});
52+
});
53+
54+
describe('getDefaultServerConfig', () => {
55+
it('should return config with auth header when API key provided', () => {
56+
const config = getDefaultServerConfig('phx_test123', 'sse');
57+
expect(config).toEqual({
58+
command: 'npx',
59+
args: [
60+
'-y',
61+
'mcp-remote@latest',
62+
'https://mcp.posthog.com/sse',
63+
'--header',
64+
'Authorization:${POSTHOG_AUTH_HEADER}',
65+
],
66+
env: {
67+
POSTHOG_AUTH_HEADER: 'Bearer phx_test123',
68+
},
69+
});
70+
});
71+
72+
it('should return config without auth header for OAuth mode (no API key)', () => {
73+
const config = getDefaultServerConfig(undefined, 'sse');
74+
expect(config).toEqual({
75+
command: 'npx',
76+
args: ['-y', 'mcp-remote@latest', 'https://mcp.posthog.com/sse'],
77+
});
78+
expect(config).not.toHaveProperty('env');
79+
});
80+
81+
it('should include region in URL for EU users in OAuth mode', () => {
82+
const config = getDefaultServerConfig(
83+
undefined,
84+
'streamable-http',
85+
undefined,
86+
false,
87+
'eu',
88+
);
89+
expect(config.args).toContain('https://mcp.posthog.com/mcp?region=eu');
90+
});
91+
});
92+
93+
describe('getNativeHTTPServerConfig', () => {
94+
it('should return config with headers when API key provided', () => {
95+
const config = getNativeHTTPServerConfig(
96+
'phx_test123',
97+
'streamable-http',
98+
);
99+
expect(config).toEqual({
100+
url: 'https://mcp.posthog.com/mcp',
101+
headers: {
102+
Authorization: 'Bearer phx_test123',
103+
},
104+
});
105+
});
106+
107+
it('should return config without headers for OAuth mode (no API key)', () => {
108+
const config = getNativeHTTPServerConfig(undefined, 'streamable-http');
109+
expect(config).toEqual({
110+
url: 'https://mcp.posthog.com/mcp',
111+
});
112+
expect(config).not.toHaveProperty('headers');
113+
});
114+
115+
it('should include region in URL for EU users', () => {
116+
const config = getNativeHTTPServerConfig(
117+
undefined,
118+
'streamable-http',
119+
undefined,
120+
false,
121+
'eu',
122+
);
123+
expect(config.url).toBe('https://mcp.posthog.com/mcp?region=eu');
124+
});
125+
});
126+
});

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,35 @@ describe('ClaudeMCPClient', () => {
329329
'sse',
330330
undefined,
331331
undefined,
332+
undefined,
333+
);
334+
});
335+
336+
it('should call getDefaultServerConfig with undefined API key for OAuth mode', async () => {
337+
existsSyncMock.mockReturnValue(false);
338+
339+
await client.addServer(undefined);
340+
341+
expect(getDefaultServerConfigMock).toHaveBeenCalledWith(
342+
undefined,
343+
'sse',
344+
undefined,
345+
undefined,
346+
undefined,
347+
);
348+
});
349+
350+
it('should pass region parameter to getDefaultServerConfig', async () => {
351+
existsSyncMock.mockReturnValue(false);
352+
353+
await client.addServer(mockApiKey, undefined, undefined, 'eu');
354+
355+
expect(getDefaultServerConfigMock).toHaveBeenCalledWith(
356+
mockApiKey,
357+
'sse',
358+
undefined,
359+
undefined,
360+
'eu',
332361
);
333362
});
334363
});

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { z } from 'zod';
44
import { execSync } from 'child_process';
55
import { analytics } from '../../../utils/analytics';
66
import { debug } from '../../../utils/debug';
7+
import type { CloudRegion } from '../../../utils/types';
78
import * as os from 'os';
89
import * as path from 'path';
910
import * as fs from 'fs';
@@ -111,19 +112,23 @@ export class ClaudeCodeMCPClient extends DefaultMCPClient {
111112
}
112113

113114
addServer(
114-
apiKey: string,
115+
apiKey?: string,
115116
selectedFeatures?: string[],
116117
local?: boolean,
118+
region?: CloudRegion,
117119
): Promise<{ success: boolean }> {
118120
const claudeBinary = this.findClaudeBinary();
119121
if (!claudeBinary) {
120122
return Promise.resolve({ success: false });
121123
}
122124

123125
const serverName = local ? 'posthog-local' : 'posthog';
124-
const url = buildMCPUrl('streamable-http', selectedFeatures, local);
126+
const url = buildMCPUrl('streamable-http', selectedFeatures, local, region);
125127

126-
const command = `${claudeBinary} mcp add --transport http ${serverName} ${url} --header "Authorization: Bearer ${apiKey}" -s user`;
128+
// OAuth mode: no auth header
129+
const authArgs = apiKey ? `--header "Authorization: Bearer ${apiKey}"` : '';
130+
const command =
131+
`${claudeBinary} mcp add --transport http ${serverName} ${url} ${authArgs} -s user`.trim();
127132

128133
try {
129134
execSync(command);

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { execSync, spawnSync } from 'node:child_process';
33

44
import { DefaultMCPClient } from '../MCPClient';
55
import { DefaultMCPClientConfig, getDefaultServerConfig } from '../defaults';
6+
import type { CloudRegion } from '../../../utils/types';
67

78
import { analytics } from '../../../utils/analytics';
89

@@ -60,12 +61,14 @@ export class CodexMCPClient extends DefaultMCPClient {
6061
apiKey: string,
6162
selectedFeatures?: string[],
6263
local?: boolean,
64+
region?: CloudRegion,
6365
): Promise<{ success: boolean }> {
6466
const config = getDefaultServerConfig(
6567
apiKey,
6668
'sse',
6769
selectedFeatures,
6870
local,
71+
region,
6972
);
7073
const serverName = local ? 'posthog-local' : 'posthog';
7174

0 commit comments

Comments
 (0)