From 5775bf37183fd948466653135618af9a2572d906 Mon Sep 17 00:00:00 2001 From: jerico pingul Date: Mon, 6 Jul 2026 15:30:30 +0100 Subject: [PATCH 1/2] feat(mcp): add Basic Auth support for custom MCP server connectors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Custom MCP servers that require HTTP Basic Auth (e.g. DataForSEO's remote MCP server) had no way to connect — the only non-OAuth option was "API Key", which sends `Authorization: Bearer ` and 401s against Basic-auth-only upstreams. Adds a `"basic"` auth type (username/password) across the install form, install flow, and mobile's custom-install screen. Client-side only: the actual `Authorization: Basic ...` header is built server-side by the PostHog backend proxy, which needs a matching change (tracked in posthog/posthog#68653) before this is functional end-to-end. Co-Authored-By: Claude Sonnet 5 --- .../mobile/src/app/mcp-servers/add-custom.tsx | 28 +++++++++++++++ .../features/mcp/components/McpServerRow.tsx | 5 +-- apps/mobile/src/features/mcp/types.ts | 4 ++- packages/api-client/src/generated.ts | 10 ++++-- packages/api-client/src/posthog-client.ts | 2 ++ .../src/mcp-servers/customServerForm.test.ts | 26 ++++++++++++++ .../core/src/mcp-servers/customServerForm.ts | 7 ++++ .../core/src/mcp-servers/installFlow.test.ts | 29 +++++++++++++++ packages/core/src/mcp-servers/installFlow.ts | 4 +++ .../AddCustomServerForm.tsx | 36 +++++++++++++++++++ .../mcp-server-manager/useMcpConnect.ts | 2 ++ .../mcp-servers/hooks/useMcpServers.ts | 2 ++ 12 files changed, 150 insertions(+), 5 deletions(-) diff --git a/apps/mobile/src/app/mcp-servers/add-custom.tsx b/apps/mobile/src/app/mcp-servers/add-custom.tsx index fff6f5e3bf..0a0ffa115f 100644 --- a/apps/mobile/src/app/mcp-servers/add-custom.tsx +++ b/apps/mobile/src/app/mcp-servers/add-custom.tsx @@ -25,6 +25,7 @@ const AUTH_OPTIONS: { value: McpAuthType; label: string }[] = [ { value: "none", label: "None" }, { value: "api_key", label: "API key" }, { value: "oauth", label: "OAuth" }, + { value: "basic", label: "Basic Auth" }, ]; export default function AddCustomMcpServerScreen() { @@ -39,6 +40,8 @@ export default function AddCustomMcpServerScreen() { const [apiKey, setApiKey] = useState(""); const [clientId, setClientId] = useState(""); const [clientSecret, setClientSecret] = useState(""); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); @@ -63,6 +66,9 @@ export default function AddCustomMcpServerScreen() { authType === "oauth" && clientSecret.trim() ? clientSecret.trim() : undefined, + username: + authType === "basic" && username.trim() ? username.trim() : undefined, + password: authType === "basic" && password ? password : undefined, }); await installations.refetch(); router.back(); @@ -178,6 +184,28 @@ export default function AddCustomMcpServerScreen() { ) : null} + {authType === "basic" ? ( + <> + Username + + Password + + + ) : null} + {error ? ( {error} ) : null} diff --git a/apps/mobile/src/features/mcp/components/McpServerRow.tsx b/apps/mobile/src/features/mcp/components/McpServerRow.tsx index a2526fa348..93da54d6f0 100644 --- a/apps/mobile/src/features/mcp/components/McpServerRow.tsx +++ b/apps/mobile/src/features/mcp/components/McpServerRow.tsx @@ -11,7 +11,7 @@ interface McpServerRowProps { title: string; subtitle?: string; description?: string; - authType?: "api_key" | "oauth" | "none"; + authType?: "api_key" | "oauth" | "basic" | "none"; badge?: ReactNode; isStdio?: boolean; needsReauth?: boolean; @@ -20,9 +20,10 @@ interface McpServerRowProps { onPress: () => void; } -function authBadge(auth?: "api_key" | "oauth" | "none") { +function authBadge(auth?: "api_key" | "oauth" | "basic" | "none") { if (auth === "oauth") return "OAuth"; if (auth === "api_key") return "API key"; + if (auth === "basic") return "Basic Auth"; return null; } diff --git a/apps/mobile/src/features/mcp/types.ts b/apps/mobile/src/features/mcp/types.ts index b8cdb4c439..322c792d31 100644 --- a/apps/mobile/src/features/mcp/types.ts +++ b/apps/mobile/src/features/mcp/types.ts @@ -1,7 +1,7 @@ // Shared types for MCP server installations and marketplace templates. // Mirrors the PostHog cloud REST schema (see `apps/code/src/renderer/api/generated.ts`). -export type McpAuthType = "api_key" | "oauth" | "none"; +export type McpAuthType = "api_key" | "oauth" | "basic" | "none"; export type McpApprovalState = "approved" | "needs_approval" | "do_not_use"; @@ -81,6 +81,8 @@ export interface InstallCustomMcpServerOptions { description?: string; client_id?: string; client_secret?: string; + username?: string; + password?: string; install_source?: McpInstallSource; posthog_code_callback_url?: string; } diff --git a/packages/api-client/src/generated.ts b/packages/api-client/src/generated.ts index 94b8b7470f..e467cf3a79 100644 --- a/packages/api-client/src/generated.ts +++ b/packages/api-client/src/generated.ts @@ -9348,7 +9348,9 @@ export namespace Schemas { values?: (unknown | null) | undefined; }; export type InsightsToolCall = { query: string; insight_type: InsightTypeEnum }; - export type InstallCustomAuthTypeEnum = "api_key" | "oauth"; + // TODO: hand-patched ahead of a real `pnpm typed-openapi` regen — the + // posthog/posthog backend PR adding "basic" hasn't deployed yet. + export type InstallCustomAuthTypeEnum = "api_key" | "oauth" | "basic"; export type InstallSourceEnum = "posthog" | "posthog-code"; export type InstallCustom = { name: string; @@ -9358,6 +9360,8 @@ export namespace Schemas { description?: string | undefined; client_id?: string | undefined; client_secret?: string | undefined; + username?: string | undefined; + password?: string | undefined; install_source?: (InstallSourceEnum & unknown) | undefined; posthog_code_callback_url?: string | undefined; }; @@ -9706,7 +9710,9 @@ export namespace Schemas { created_by: UserBasic & unknown; updated_at: string | null; }; - export type MCPAuthTypeEnum = "api_key" | "oauth"; + // TODO: hand-patched ahead of a real `pnpm typed-openapi` regen — the + // posthog/posthog backend PR adding "basic" hasn't deployed yet. + export type MCPAuthTypeEnum = "api_key" | "oauth" | "basic"; export type MCPServerInstallation = { id: string; template_id: string | null; diff --git a/packages/api-client/src/posthog-client.ts b/packages/api-client/src/posthog-client.ts index e25eada011..4275945f91 100644 --- a/packages/api-client/src/posthog-client.ts +++ b/packages/api-client/src/posthog-client.ts @@ -3884,6 +3884,8 @@ export class PostHogAPIClient { description?: string; client_id?: string; client_secret?: string; + username?: string; + password?: string; install_source?: "posthog" | "posthog-code"; posthog_code_callback_url?: string; }): Promise { diff --git a/packages/core/src/mcp-servers/customServerForm.test.ts b/packages/core/src/mcp-servers/customServerForm.test.ts index 9b48da11a5..43e08aebb7 100644 --- a/packages/core/src/mcp-servers/customServerForm.test.ts +++ b/packages/core/src/mcp-servers/customServerForm.test.ts @@ -17,6 +17,8 @@ function values( apiKey: "", clientId: "", clientSecret: "", + username: "", + password: "", ...overrides, }; } @@ -88,4 +90,28 @@ describe("buildCustomServerRequest", () => { expect(apiKeyReq.client_id).toBeUndefined(); expect(apiKeyReq.client_secret).toBeUndefined(); }); + + it("includes username/password only for basic auth when both are present", () => { + const req = buildCustomServerRequest( + values({ authType: "basic", username: " user ", password: "pass" }), + ); + expect(req.username).toBe("user"); + expect(req.password).toBe("pass"); + + expect( + buildCustomServerRequest( + values({ authType: "oauth", username: "user", password: "pass" }), + ).username, + ).toBeUndefined(); + expect( + buildCustomServerRequest( + values({ authType: "basic", username: "", password: "pass" }), + ).username, + ).toBeUndefined(); + expect( + buildCustomServerRequest( + values({ authType: "basic", username: "user", password: "" }), + ).password, + ).toBeUndefined(); + }); }); diff --git a/packages/core/src/mcp-servers/customServerForm.ts b/packages/core/src/mcp-servers/customServerForm.ts index 6d9452939d..47d1ee561f 100644 --- a/packages/core/src/mcp-servers/customServerForm.ts +++ b/packages/core/src/mcp-servers/customServerForm.ts @@ -8,6 +8,8 @@ export interface CustomServerFormValues { apiKey: string; clientId: string; clientSecret: string; + username: string; + password: string; } export interface CustomServerRequest { @@ -18,6 +20,8 @@ export interface CustomServerRequest { api_key?: string; client_id?: string; client_secret?: string; + username?: string; + password?: string; } export function isValidMcpUrl(url: string): boolean { @@ -47,5 +51,8 @@ export function buildCustomServerRequest( ...(values.authType === "oauth" && values.clientSecret.trim() ? { client_secret: values.clientSecret.trim() } : {}), + ...(values.authType === "basic" && values.username.trim() && values.password + ? { username: values.username.trim(), password: values.password } + : {}), }; } diff --git a/packages/core/src/mcp-servers/installFlow.test.ts b/packages/core/src/mcp-servers/installFlow.test.ts index 0450b3349f..9584adb41f 100644 --- a/packages/core/src/mcp-servers/installFlow.test.ts +++ b/packages/core/src/mcp-servers/installFlow.test.ts @@ -94,6 +94,35 @@ describe("installCustomWithOAuth", () => { }); expect(result).toEqual({ error: "denied" }); }); + + it("forwards username/password for basic auth", async () => { + const oauth = makeOAuth(); + const client: InstallFlowClient = { + installMcpTemplate: vi.fn(), + installCustomMcpServer: vi.fn().mockResolvedValue(installedInstallation), + authorizeMcpInstallation: vi.fn(), + }; + + await installCustomWithOAuth(client, oauth, { + name: "N", + url: "https://x", + description: "d", + auth_type: "basic", + username: "user", + password: "pass", + }); + + expect(client.installCustomMcpServer).toHaveBeenCalledWith({ + name: "N", + url: "https://x", + description: "d", + auth_type: "basic", + username: "user", + password: "pass", + install_source: "posthog-code", + posthog_code_callback_url: "cb://here", + }); + }); }); describe("reauthorizeWithOAuth", () => { diff --git a/packages/core/src/mcp-servers/installFlow.ts b/packages/core/src/mcp-servers/installFlow.ts index 5dbd11d5c8..d7fba88c34 100644 --- a/packages/core/src/mcp-servers/installFlow.ts +++ b/packages/core/src/mcp-servers/installFlow.ts @@ -24,6 +24,8 @@ export interface InstallFlowClient { api_key?: string; client_id?: string; client_secret?: string; + username?: string; + password?: string; install_source?: "posthog" | "posthog-code"; posthog_code_callback_url?: string; }): Promise; @@ -80,6 +82,8 @@ export async function installCustomWithOAuth( api_key?: string; client_id?: string; client_secret?: string; + username?: string; + password?: string; }, ): Promise { const { callbackUrl } = await oauth.getCallbackUrl(); diff --git a/packages/ui/src/features/mcp-server-manager/AddCustomServerForm.tsx b/packages/ui/src/features/mcp-server-manager/AddCustomServerForm.tsx index 0f21ed9e38..d442bc60b0 100644 --- a/packages/ui/src/features/mcp-server-manager/AddCustomServerForm.tsx +++ b/packages/ui/src/features/mcp-server-manager/AddCustomServerForm.tsx @@ -25,6 +25,8 @@ interface AddCustomServerFormProps { api_key?: string; client_id?: string; client_secret?: string; + username?: string; + password?: string; }) => void; /** Prefill the form (e.g. the agent builder's connect_mcp punch-out supplies a * suggested name/url). The user can still edit every field before connecting. */ @@ -57,6 +59,8 @@ export function AddCustomServerForm({ const [apiKey, setApiKey] = useState(""); const [clientId, setClientId] = useState(""); const [clientSecret, setClientSecret] = useState(""); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); const [showAdvanced, setShowAdvanced] = useState(false); const canSubmit = canSubmitCustomServer({ name, url }) && !pending; @@ -74,6 +78,8 @@ export function AddCustomServerForm({ apiKey, clientId, clientSecret, + username, + password, }), ); }, @@ -86,6 +92,8 @@ export function AddCustomServerForm({ apiKey, clientId, clientSecret, + username, + password, onSubmit, ], ); @@ -161,12 +169,17 @@ export function AddCustomServerForm({ onValueChange={(val) => { setAuthType(val as McpAuthType); if (val !== "api_key") setApiKey(""); + if (val !== "basic") { + setUsername(""); + setPassword(""); + } }} > OAuth API key + Basic Auth @@ -183,6 +196,29 @@ export function AddCustomServerForm({ )} + {authType === "basic" && ( + <> + + Username + setUsername(e.target.value)} + placeholder="Enter username" + spellCheck={false} + /> + + + Password + setPassword(e.target.value)} + placeholder="Enter password" + type="password" + /> + + + )} + {authType === "oauth" && ( <>