Skip to content

Commit a4ee738

Browse files
committed
feat: added external mcp integrations
1 parent 809b5ef commit a4ee738

File tree

7 files changed

+873
-3
lines changed

7 files changed

+873
-3
lines changed

apps/twig/src/main/services/agent/service.ts

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,9 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
374374
return this.currentToken || fallback;
375375
}
376376

377-
private buildMcpServers(credentials: Credentials): AcpMcpServer[] {
377+
private async buildMcpServers(
378+
credentials: Credentials,
379+
): Promise<AcpMcpServer[]> {
378380
const servers: AcpMcpServer[] = [];
379381

380382
const mcpUrl = this.getPostHogMcpUrl(credentials.apiHost);
@@ -394,9 +396,94 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
394396
],
395397
});
396398

399+
// Fetch user-installed MCP servers from the PostHog backend
400+
const installations = await this.fetchMcpInstallations(credentials);
401+
402+
for (const installation of installations) {
403+
// Skip the PostHog MCP server since it's already included above
404+
if (installation.url === mcpUrl) continue;
405+
406+
if (installation.auth_type === "none") {
407+
servers.push({
408+
name:
409+
installation.name || installation.display_name || installation.url,
410+
type: "http",
411+
url: installation.url,
412+
headers: [],
413+
});
414+
} else {
415+
// Authenticated servers go through the PostHog proxy so credentials
416+
// never leave the backend
417+
servers.push({
418+
name:
419+
installation.name || installation.display_name || installation.url,
420+
type: "http",
421+
url: installation.proxy_url,
422+
headers: [{ name: "Authorization", value: `Bearer ${token}` }],
423+
});
424+
}
425+
}
426+
397427
return servers;
398428
}
399429

430+
private async fetchMcpInstallations(credentials: Credentials): Promise<
431+
Array<{
432+
id: string;
433+
url: string;
434+
proxy_url: string;
435+
name: string;
436+
display_name: string;
437+
auth_type: string;
438+
}>
439+
> {
440+
const token = this.getToken(credentials.apiKey);
441+
const baseUrl = this.getPostHogApiBaseUrl(credentials.apiHost);
442+
const url = `${baseUrl}/api/environments/${credentials.projectId}/mcp_server_installations/`;
443+
444+
try {
445+
const response = await fetch(url, {
446+
headers: {
447+
Authorization: `Bearer ${token}`,
448+
"Content-Type": "application/json",
449+
},
450+
});
451+
452+
if (!response.ok) {
453+
log.warn("Failed to fetch MCP installations", {
454+
status: response.status,
455+
});
456+
return [];
457+
}
458+
459+
const data = (await response.json()) as {
460+
results?: Array<{
461+
id: string;
462+
url: string;
463+
proxy_url?: string;
464+
name: string;
465+
display_name: string;
466+
auth_type: string;
467+
pending_oauth: boolean;
468+
needs_reauth: boolean;
469+
}>;
470+
};
471+
const installations = data.results ?? [];
472+
473+
return installations
474+
.filter((i) => !i.pending_oauth && !i.needs_reauth)
475+
.map((i) => ({
476+
...i,
477+
proxy_url:
478+
i.proxy_url ??
479+
`${baseUrl}/api/environments/${credentials.projectId}/mcp_server_installations/${i.id}/proxy/`,
480+
}));
481+
} catch (err) {
482+
log.warn("Error fetching MCP installations", { error: err });
483+
return [];
484+
}
485+
}
486+
400487
private buildSystemPrompt(
401488
credentials: Credentials,
402489
customInstructions?: string,
@@ -423,6 +510,11 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
423510
return "https://mcp.posthog.com/mcp";
424511
}
425512

513+
private getPostHogApiBaseUrl(apiHost: string): string {
514+
const host = process.env.POSTHOG_PROXY_BASE_URL || apiHost;
515+
return host.endsWith("/") ? host.slice(0, -1) : host;
516+
}
517+
426518
async startSession(params: StartSessionInput): Promise<SessionResponse> {
427519
this.validateSessionParams(params);
428520
const config = this.toSessionConfig(params);
@@ -542,7 +634,8 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
542634
},
543635
});
544636

545-
const mcpServers = this.buildMcpServers(credentials);
637+
const mcpServers =
638+
adapter === "codex" ? [] : await this.buildMcpServers(credentials);
546639

547640
let configOptions: SessionConfigOption[] | undefined;
548641
let agentSessionId: string;

apps/twig/src/renderer/api/posthogClient.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,31 @@ import { createApiClient, type Schemas } from "./generated";
1515

1616
const log = logger.scope("posthog-client");
1717

18+
export interface McpRecommendedServer {
19+
name: string;
20+
url: string;
21+
description: string;
22+
icon_url: string;
23+
auth_type: "none" | "api_key" | "oauth";
24+
oauth_provider_kind?: string;
25+
}
26+
27+
export interface McpServerInstallation {
28+
id: string;
29+
server_id: string | null;
30+
name: string;
31+
display_name: string;
32+
url: string;
33+
description: string;
34+
auth_type: "none" | "api_key" | "oauth";
35+
configuration: Record<string, unknown>;
36+
needs_reauth: boolean;
37+
pending_oauth: boolean;
38+
proxy_url: string;
39+
created_at: string;
40+
updated_at: string;
41+
}
42+
1843
function isObjectRecord(value: unknown): value is Record<string, unknown> {
1944
return typeof value === "object" && value !== null;
2045
}
@@ -797,6 +822,94 @@ export class PostHogAPIClient {
797822
} as RepoAutonomyStatus;
798823
}
799824

825+
async getMcpServers(): Promise<McpRecommendedServer[]> {
826+
const teamId = await this.getTeamId();
827+
const url = new URL(
828+
`${this.api.baseUrl}/api/environments/${teamId}/mcp_servers/`,
829+
);
830+
const response = await this.api.fetcher.fetch({
831+
method: "get",
832+
url,
833+
path: `/api/environments/${teamId}/mcp_servers/`,
834+
});
835+
836+
if (!response.ok) {
837+
throw new Error(`Failed to fetch MCP servers: ${response.statusText}`);
838+
}
839+
840+
const data = await response.json();
841+
return data.results ?? data ?? [];
842+
}
843+
844+
async getMcpServerInstallations(): Promise<McpServerInstallation[]> {
845+
const teamId = await this.getTeamId();
846+
const url = new URL(
847+
`${this.api.baseUrl}/api/environments/${teamId}/mcp_server_installations/`,
848+
);
849+
const response = await this.api.fetcher.fetch({
850+
method: "get",
851+
url,
852+
path: `/api/environments/${teamId}/mcp_server_installations/`,
853+
});
854+
855+
if (!response.ok) {
856+
throw new Error(
857+
`Failed to fetch MCP server installations: ${response.statusText}`,
858+
);
859+
}
860+
861+
const data = await response.json();
862+
return data.results ?? data ?? [];
863+
}
864+
865+
async installCustomMcpServer(options: {
866+
name: string;
867+
url: string;
868+
auth_type: "none" | "api_key" | "oauth";
869+
api_key?: string;
870+
description?: string;
871+
oauth_provider_kind?: string;
872+
}): Promise<McpServerInstallation & { redirect_url?: string }> {
873+
const teamId = await this.getTeamId();
874+
const apiUrl = new URL(
875+
`${this.api.baseUrl}/api/environments/${teamId}/mcp_server_installations/install_custom/`,
876+
);
877+
const response = await this.api.fetcher.fetch({
878+
method: "post",
879+
url: apiUrl,
880+
path: `/api/environments/${teamId}/mcp_server_installations/install_custom/`,
881+
overrides: {
882+
body: JSON.stringify(options),
883+
},
884+
});
885+
886+
if (!response.ok) {
887+
const errorData = await response.json().catch(() => ({}));
888+
throw new Error(
889+
(errorData as { detail?: string }).detail ??
890+
`Failed to install MCP server: ${response.statusText}`,
891+
);
892+
}
893+
894+
return await response.json();
895+
}
896+
897+
async uninstallMcpServer(installationId: string): Promise<void> {
898+
const teamId = await this.getTeamId();
899+
const url = new URL(
900+
`${this.api.baseUrl}/api/environments/${teamId}/mcp_server_installations/${installationId}/`,
901+
);
902+
const response = await this.api.fetcher.fetch({
903+
method: "delete",
904+
url,
905+
path: `/api/environments/${teamId}/mcp_server_installations/${installationId}/`,
906+
});
907+
908+
if (!response.ok && response.status !== 204) {
909+
throw new Error(`Failed to uninstall MCP server: ${response.statusText}`);
910+
}
911+
}
912+
800913
/**
801914
* Check if a feature flag is enabled for the current project.
802915
* Returns true if the flag exists and is active, false otherwise.

apps/twig/src/renderer/features/settings/components/SettingsDialog.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
GearSix,
1212
Keyboard,
1313
Palette,
14+
Plugs,
1415
PlugsConnected,
1516
User,
1617
Wrench,
@@ -23,6 +24,7 @@ import { AdvancedSettings } from "./sections/AdvancedSettings";
2324
import { ClaudeCodeSettings } from "./sections/ClaudeCodeSettings";
2425
import { GeneralSettings } from "./sections/GeneralSettings";
2526
import { IntegrationsSettings } from "./sections/IntegrationsSettings";
27+
import { McpServersSettings } from "./sections/McpServersSettings";
2628
import { PersonalizationSettings } from "./sections/PersonalizationSettings";
2729
import { ShortcutsSettings } from "./sections/ShortcutsSettings";
2830
import { UpdatesSettings } from "./sections/UpdatesSettings";
@@ -45,6 +47,7 @@ const SIDEBAR_ITEMS: SidebarItem[] = [
4547
icon: <Palette size={16} />,
4648
},
4749
{ id: "claude-code", label: "Claude Code", icon: <Code size={16} /> },
50+
{ id: "mcp-servers", label: "MCP Servers", icon: <Plugs size={16} /> },
4851
{ id: "shortcuts", label: "Shortcuts", icon: <Keyboard size={16} /> },
4952
{
5053
id: "integrations",
@@ -61,6 +64,7 @@ const CATEGORY_TITLES: Record<SettingsCategory, string> = {
6164
workspaces: "Workspaces",
6265
personalization: "Personalization",
6366
"claude-code": "Claude Code",
67+
"mcp-servers": "MCP Servers",
6468
shortcuts: "Shortcuts",
6569
integrations: "Integrations",
6670
updates: "Updates",
@@ -73,6 +77,7 @@ const CATEGORY_COMPONENTS: Record<SettingsCategory, React.ComponentType> = {
7377
workspaces: WorkspacesSettings,
7478
personalization: PersonalizationSettings,
7579
"claude-code": ClaudeCodeSettings,
80+
"mcp-servers": McpServersSettings,
7681
shortcuts: ShortcutsSettings,
7782
integrations: IntegrationsSettings,
7883
updates: UpdatesSettings,

0 commit comments

Comments
 (0)