From 9162c498407f2f8fac2dd9625b9f98137f5b2442 Mon Sep 17 00:00:00 2001 From: Richard Solomou Date: Thu, 16 Jul 2026 12:57:41 +0300 Subject: [PATCH] Gate local MCP cloud access behind a feature flag Keep local MCP discovery, cloud import, and desktop relay designation disabled unless the existing PostHog Code flag is enabled. Generated-By: PostHog Code Task-Id: 311f71cc-873c-4a23-b8c7-d9fed8f8ece5 --- packages/shared/src/flags.ts | 2 + .../useLocalMcpCloudServers.test.tsx | 80 +++++++++++++++++++ .../local-mcp/useLocalMcpCloudServers.ts | 13 ++- 3 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 packages/ui/src/features/local-mcp/useLocalMcpCloudServers.test.tsx diff --git a/packages/shared/src/flags.ts b/packages/shared/src/flags.ts index 35899d7d87..d692da1fda 100644 --- a/packages/shared/src/flags.ts +++ b/packages/shared/src/flags.ts @@ -19,3 +19,5 @@ export const TASKS_PREWARM_SANDBOX_FLAG = "tasks-prewarm-sandbox"; export const GLM_MODEL_FLAG = "posthog-code-glm-model"; /** Spoken narration (agent speaks via the `speak` tool). Gated for a staged rollout. */ export const SPOKEN_NARRATION_FLAG = "posthog-code-spoken-narration"; +// Gates importing and relaying local MCP servers into cloud task runs. +export const LOCAL_MCP_IMPORT_FLAG = "posthog-code-local-mcp-import"; diff --git a/packages/ui/src/features/local-mcp/useLocalMcpCloudServers.test.tsx b/packages/ui/src/features/local-mcp/useLocalMcpCloudServers.test.tsx new file mode 100644 index 0000000000..db776d46bf --- /dev/null +++ b/packages/ui/src/features/local-mcp/useLocalMcpCloudServers.test.tsx @@ -0,0 +1,80 @@ +import type { LocalMcpCloudClassification } from "@posthog/core/local-mcp/localMcpImport"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { renderHook, waitFor } from "@testing-library/react"; +import type { ReactNode } from "react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + flagEnabled: false, + getCloudAvailability: vi.fn(), +})); + +vi.mock("@posthog/di/react", () => ({ + useServiceOptional: () => ({ + getCloudAvailability: mocks.getCloudAvailability, + }), +})); + +vi.mock("../feature-flags/useFeatureFlag", () => ({ + useFeatureFlag: () => mocks.flagEnabled, +})); + +import { useLocalMcpCloudServers } from "./useLocalMcpCloudServers"; + +function wrapper({ children }: { children: ReactNode }) { + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + return ( + {children} + ); +} + +const server: LocalMcpCloudClassification = { + name: "internal-tools", + availability: "requires_desktop", + reason: "stdio_transport", +}; + +describe("useLocalMcpCloudServers", () => { + beforeEach(() => { + mocks.flagEnabled = false; + mocks.getCloudAvailability.mockReset(); + mocks.getCloudAvailability.mockResolvedValue([server]); + }); + + it("does not inspect local MCP servers while the feature flag is disabled", () => { + const { result } = renderHook(() => useLocalMcpCloudServers(true), { + wrapper, + }); + + expect(result.current).toEqual({ servers: [], isLoading: false }); + expect(mocks.getCloudAvailability).not.toHaveBeenCalled(); + }); + + it("returns local MCP servers while the feature flag is enabled", async () => { + mocks.flagEnabled = true; + + const { result } = renderHook(() => useLocalMcpCloudServers(true), { + wrapper, + }); + + await waitFor(() => + expect(result.current).toEqual({ servers: [server], isLoading: false }), + ); + }); + + it("hides cached local MCP servers when the feature flag is disabled", async () => { + mocks.flagEnabled = true; + const { result, rerender } = renderHook( + () => useLocalMcpCloudServers(true), + { wrapper }, + ); + await waitFor(() => expect(result.current.servers).toEqual([server])); + + mocks.flagEnabled = false; + rerender(); + + expect(result.current).toEqual({ servers: [], isLoading: false }); + }); +}); diff --git a/packages/ui/src/features/local-mcp/useLocalMcpCloudServers.ts b/packages/ui/src/features/local-mcp/useLocalMcpCloudServers.ts index 425ff33bbb..8ff7976ee6 100644 --- a/packages/ui/src/features/local-mcp/useLocalMcpCloudServers.ts +++ b/packages/ui/src/features/local-mcp/useLocalMcpCloudServers.ts @@ -4,7 +4,9 @@ import type { LocalMcpImportService, } from "@posthog/core/local-mcp/localMcpImport"; import { useServiceOptional } from "@posthog/di/react"; +import { LOCAL_MCP_IMPORT_FLAG } from "@posthog/shared"; import { useQuery } from "@tanstack/react-query"; +import { useFeatureFlag } from "../feature-flags/useFeatureFlag"; // Stable identity so consumers' memo deps don't churn while data is absent. const NO_SERVERS: LocalMcpCloudClassification[] = []; @@ -19,7 +21,7 @@ export interface LocalMcpCloudServersResult { /** * The user's local (~/.claude.json) MCP servers classified by cloud * availability. Empty on hosts without a local workspace (web/mobile — the - * service is only bound on desktop). + * service is only bound on desktop) and while the feature flag is off. */ export function useLocalMcpCloudServers( enabled: boolean, @@ -27,13 +29,18 @@ export function useLocalMcpCloudServers( const service = useServiceOptional( LOCAL_MCP_IMPORT_SERVICE, ); + const flagEnabled = useFeatureFlag(LOCAL_MCP_IMPORT_FLAG); + const queryEnabled = enabled && flagEnabled && !!service; const query = useQuery({ queryKey: ["local-mcp-cloud-availability"], queryFn: () => (service ? service.getCloudAvailability() : NO_SERVERS), - enabled: enabled && !!service, + enabled: queryEnabled, staleTime: 30_000, }); - return { servers: query.data ?? NO_SERVERS, isLoading: query.isLoading }; + return { + servers: queryEnabled ? (query.data ?? NO_SERVERS) : NO_SERVERS, + isLoading: queryEnabled && query.isLoading, + }; }