Skip to content

Commit 4707f2a

Browse files
committed
feat: added external mcp integrations
1 parent 461fe02 commit 4707f2a

7 files changed

Lines changed: 872 additions & 3 deletions

File tree

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

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,9 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
348348
return this.currentToken || fallback;
349349
}
350350

351-
private buildMcpServers(credentials: Credentials): AcpMcpServer[] {
351+
private async buildMcpServers(
352+
credentials: Credentials,
353+
): Promise<AcpMcpServer[]> {
352354
const servers: AcpMcpServer[] = [];
353355

354356
const mcpUrl = this.getPostHogMcpUrl(credentials.apiHost);
@@ -367,9 +369,94 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
367369
],
368370
});
369371

372+
// Fetch user-installed MCP servers from the PostHog backend
373+
const installations = await this.fetchMcpInstallations(credentials);
374+
375+
for (const installation of installations) {
376+
// Skip the PostHog MCP server since it's already included above
377+
if (installation.url === mcpUrl) continue;
378+
379+
if (installation.auth_type === "none") {
380+
servers.push({
381+
name:
382+
installation.name || installation.display_name || installation.url,
383+
type: "http",
384+
url: installation.url,
385+
headers: [],
386+
});
387+
} else {
388+
// Authenticated servers go through the PostHog proxy so credentials
389+
// never leave the backend
390+
servers.push({
391+
name:
392+
installation.name || installation.display_name || installation.url,
393+
type: "http",
394+
url: installation.proxy_url,
395+
headers: [{ name: "Authorization", value: `Bearer ${token}` }],
396+
});
397+
}
398+
}
399+
370400
return servers;
371401
}
372402

403+
private async fetchMcpInstallations(credentials: Credentials): Promise<
404+
Array<{
405+
id: string;
406+
url: string;
407+
proxy_url: string;
408+
name: string;
409+
display_name: string;
410+
auth_type: string;
411+
}>
412+
> {
413+
const token = this.getToken(credentials.apiKey);
414+
const baseUrl = this.getPostHogApiBaseUrl(credentials.apiHost);
415+
const url = `${baseUrl}/api/environments/${credentials.projectId}/mcp_server_installations/`;
416+
417+
try {
418+
const response = await fetch(url, {
419+
headers: {
420+
Authorization: `Bearer ${token}`,
421+
"Content-Type": "application/json",
422+
},
423+
});
424+
425+
if (!response.ok) {
426+
log.warn("Failed to fetch MCP installations", {
427+
status: response.status,
428+
});
429+
return [];
430+
}
431+
432+
const data = (await response.json()) as {
433+
results?: Array<{
434+
id: string;
435+
url: string;
436+
proxy_url?: string;
437+
name: string;
438+
display_name: string;
439+
auth_type: string;
440+
pending_oauth: boolean;
441+
needs_reauth: boolean;
442+
}>;
443+
};
444+
const installations = data.results ?? [];
445+
446+
return installations
447+
.filter((i) => !i.pending_oauth && !i.needs_reauth)
448+
.map((i) => ({
449+
...i,
450+
proxy_url:
451+
i.proxy_url ??
452+
`${baseUrl}/api/environments/${credentials.projectId}/mcp_server_installations/${i.id}/proxy/`,
453+
}));
454+
} catch (err) {
455+
log.warn("Error fetching MCP installations", { error: err });
456+
return [];
457+
}
458+
}
459+
373460
private buildSystemPrompt(
374461
credentials: Credentials,
375462
customInstructions?: string,
@@ -396,6 +483,11 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
396483
return "https://mcp.posthog.com/mcp";
397484
}
398485

486+
private getPostHogApiBaseUrl(apiHost: string): string {
487+
const host = process.env.POSTHOG_PROXY_BASE_URL || apiHost;
488+
return host.endsWith("/") ? host.slice(0, -1) : host;
489+
}
490+
399491
async startSession(params: StartSessionInput): Promise<SessionResponse> {
400492
this.validateSessionParams(params);
401493
const config = this.toSessionConfig(params);
@@ -516,7 +608,7 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
516608
});
517609

518610
const mcpServers =
519-
adapter === "codex" ? [] : this.buildMcpServers(credentials);
611+
adapter === "codex" ? [] : await this.buildMcpServers(credentials);
520612

521613
let configOptions: SessionConfigOption[] | undefined;
522614
let agentSessionId: string;

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

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

1414
const log = logger.scope("posthog-client");
1515

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

762+
async getMcpServers(): Promise<McpRecommendedServer[]> {
763+
const teamId = await this.getTeamId();
764+
const url = new URL(
765+
`${this.api.baseUrl}/api/environments/${teamId}/mcp_servers/`,
766+
);
767+
const response = await this.api.fetcher.fetch({
768+
method: "get",
769+
url,
770+
path: `/api/environments/${teamId}/mcp_servers/`,
771+
});
772+
773+
if (!response.ok) {
774+
throw new Error(`Failed to fetch MCP servers: ${response.statusText}`);
775+
}
776+
777+
const data = await response.json();
778+
return data.results ?? data ?? [];
779+
}
780+
781+
async getMcpServerInstallations(): Promise<McpServerInstallation[]> {
782+
const teamId = await this.getTeamId();
783+
const url = new URL(
784+
`${this.api.baseUrl}/api/environments/${teamId}/mcp_server_installations/`,
785+
);
786+
const response = await this.api.fetcher.fetch({
787+
method: "get",
788+
url,
789+
path: `/api/environments/${teamId}/mcp_server_installations/`,
790+
});
791+
792+
if (!response.ok) {
793+
throw new Error(
794+
`Failed to fetch MCP server installations: ${response.statusText}`,
795+
);
796+
}
797+
798+
const data = await response.json();
799+
return data.results ?? data ?? [];
800+
}
801+
802+
async installCustomMcpServer(options: {
803+
name: string;
804+
url: string;
805+
auth_type: "none" | "api_key" | "oauth";
806+
api_key?: string;
807+
description?: string;
808+
oauth_provider_kind?: string;
809+
}): Promise<McpServerInstallation & { redirect_url?: string }> {
810+
const teamId = await this.getTeamId();
811+
const apiUrl = new URL(
812+
`${this.api.baseUrl}/api/environments/${teamId}/mcp_server_installations/install_custom/`,
813+
);
814+
const response = await this.api.fetcher.fetch({
815+
method: "post",
816+
url: apiUrl,
817+
path: `/api/environments/${teamId}/mcp_server_installations/install_custom/`,
818+
overrides: {
819+
body: JSON.stringify(options),
820+
},
821+
});
822+
823+
if (!response.ok) {
824+
const errorData = await response.json().catch(() => ({}));
825+
throw new Error(
826+
(errorData as { detail?: string }).detail ??
827+
`Failed to install MCP server: ${response.statusText}`,
828+
);
829+
}
830+
831+
return await response.json();
832+
}
833+
834+
async uninstallMcpServer(installationId: string): Promise<void> {
835+
const teamId = await this.getTeamId();
836+
const url = new URL(
837+
`${this.api.baseUrl}/api/environments/${teamId}/mcp_server_installations/${installationId}/`,
838+
);
839+
const response = await this.api.fetcher.fetch({
840+
method: "delete",
841+
url,
842+
path: `/api/environments/${teamId}/mcp_server_installations/${installationId}/`,
843+
});
844+
845+
if (!response.ok && response.status !== 204) {
846+
throw new Error(`Failed to uninstall MCP server: ${response.statusText}`);
847+
}
848+
}
849+
737850
/**
738851
* Check if a feature flag is enabled for the current project.
739852
* 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
User,
1516
Wrench,
1617
} from "@phosphor-icons/react";
@@ -21,6 +22,7 @@ import { AccountSettings } from "./sections/AccountSettings";
2122
import { AdvancedSettings } from "./sections/AdvancedSettings";
2223
import { ClaudeCodeSettings } from "./sections/ClaudeCodeSettings";
2324
import { GeneralSettings } from "./sections/GeneralSettings";
25+
import { McpServersSettings } from "./sections/McpServersSettings";
2426
import { PersonalizationSettings } from "./sections/PersonalizationSettings";
2527
import { ShortcutsSettings } from "./sections/ShortcutsSettings";
2628
import { UpdatesSettings } from "./sections/UpdatesSettings";
@@ -43,6 +45,7 @@ const SIDEBAR_ITEMS: SidebarItem[] = [
4345
icon: <Palette size={16} />,
4446
},
4547
{ id: "claude-code", label: "Claude Code", icon: <Code size={16} /> },
48+
{ id: "mcp-servers", label: "MCP Servers", icon: <Plugs size={16} /> },
4649
{ id: "shortcuts", label: "Shortcuts", icon: <Keyboard size={16} /> },
4750
{ id: "updates", label: "Updates", icon: <ArrowsClockwise size={16} /> },
4851
{ id: "advanced", label: "Advanced", icon: <Wrench size={16} /> },
@@ -54,6 +57,7 @@ const CATEGORY_TITLES: Record<SettingsCategory, string> = {
5457
workspaces: "Workspaces",
5558
personalization: "Personalization",
5659
"claude-code": "Claude Code",
60+
"mcp-servers": "MCP Servers",
5761
shortcuts: "Shortcuts",
5862
updates: "Updates",
5963
advanced: "Advanced",
@@ -65,6 +69,7 @@ const CATEGORY_COMPONENTS: Record<SettingsCategory, React.ComponentType> = {
6569
workspaces: WorkspacesSettings,
6670
personalization: PersonalizationSettings,
6771
"claude-code": ClaudeCodeSettings,
72+
"mcp-servers": McpServersSettings,
6873
shortcuts: ShortcutsSettings,
6974
updates: UpdatesSettings,
7075
advanced: AdvancedSettings,

0 commit comments

Comments
 (0)