Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/shared/src/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Original file line number Diff line number Diff line change
@@ -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 (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}

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 });
});
});
13 changes: 10 additions & 3 deletions packages/ui/src/features/local-mcp/useLocalMcpCloudServers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];
Expand All @@ -19,21 +21,26 @@ 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,
): LocalMcpCloudServersResult {
const service = useServiceOptional<LocalMcpImportService>(
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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Revocation Leaves Discovery In Flight

When the flag is revoked while getCloudAvailability() is running, setting enabled to false does not cancel that request. The request can still complete and populate the shared cache, so re-enabling the flag within staleTime can immediately expose outdated local MCP servers and include a server removed while access was disabled in a new cloud task.

});

return { servers: query.data ?? NO_SERVERS, isLoading: query.isLoading };
return {
servers: queryEnabled ? (query.data ?? NO_SERVERS) : NO_SERVERS,
isLoading: queryEnabled && query.isLoading,
};
}
Loading