Skip to content

Commit e01a82f

Browse files
feat(local-mcp): gate cloud import and relay (#3495)
1 parent dfc8418 commit e01a82f

3 files changed

Lines changed: 92 additions & 3 deletions

File tree

packages/shared/src/flags.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,5 @@ export const TASKS_PREWARM_SANDBOX_FLAG = "tasks-prewarm-sandbox";
1919
export const GLM_MODEL_FLAG = "posthog-code-glm-model";
2020
/** Spoken narration (agent speaks via the `speak` tool). Gated for a staged rollout. */
2121
export const SPOKEN_NARRATION_FLAG = "posthog-code-spoken-narration";
22+
// Gates importing and relaying local MCP servers into cloud task runs.
23+
export const LOCAL_MCP_IMPORT_FLAG = "posthog-code-local-mcp-import";
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import type { LocalMcpCloudClassification } from "@posthog/core/local-mcp/localMcpImport";
2+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
3+
import { renderHook, waitFor } from "@testing-library/react";
4+
import type { ReactNode } from "react";
5+
import { beforeEach, describe, expect, it, vi } from "vitest";
6+
7+
const mocks = vi.hoisted(() => ({
8+
flagEnabled: false,
9+
getCloudAvailability: vi.fn(),
10+
}));
11+
12+
vi.mock("@posthog/di/react", () => ({
13+
useServiceOptional: () => ({
14+
getCloudAvailability: mocks.getCloudAvailability,
15+
}),
16+
}));
17+
18+
vi.mock("../feature-flags/useFeatureFlag", () => ({
19+
useFeatureFlag: () => mocks.flagEnabled,
20+
}));
21+
22+
import { useLocalMcpCloudServers } from "./useLocalMcpCloudServers";
23+
24+
function wrapper({ children }: { children: ReactNode }) {
25+
const queryClient = new QueryClient({
26+
defaultOptions: { queries: { retry: false } },
27+
});
28+
return (
29+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
30+
);
31+
}
32+
33+
const server: LocalMcpCloudClassification = {
34+
name: "internal-tools",
35+
availability: "requires_desktop",
36+
reason: "stdio_transport",
37+
};
38+
39+
describe("useLocalMcpCloudServers", () => {
40+
beforeEach(() => {
41+
mocks.flagEnabled = false;
42+
mocks.getCloudAvailability.mockReset();
43+
mocks.getCloudAvailability.mockResolvedValue([server]);
44+
});
45+
46+
it("does not inspect local MCP servers while the feature flag is disabled", () => {
47+
const { result } = renderHook(() => useLocalMcpCloudServers(true), {
48+
wrapper,
49+
});
50+
51+
expect(result.current).toEqual({ servers: [], isLoading: false });
52+
expect(mocks.getCloudAvailability).not.toHaveBeenCalled();
53+
});
54+
55+
it("returns local MCP servers while the feature flag is enabled", async () => {
56+
mocks.flagEnabled = true;
57+
58+
const { result } = renderHook(() => useLocalMcpCloudServers(true), {
59+
wrapper,
60+
});
61+
62+
await waitFor(() =>
63+
expect(result.current).toEqual({ servers: [server], isLoading: false }),
64+
);
65+
});
66+
67+
it("hides cached local MCP servers when the feature flag is disabled", async () => {
68+
mocks.flagEnabled = true;
69+
const { result, rerender } = renderHook(
70+
() => useLocalMcpCloudServers(true),
71+
{ wrapper },
72+
);
73+
await waitFor(() => expect(result.current.servers).toEqual([server]));
74+
75+
mocks.flagEnabled = false;
76+
rerender();
77+
78+
expect(result.current).toEqual({ servers: [], isLoading: false });
79+
});
80+
});

packages/ui/src/features/local-mcp/useLocalMcpCloudServers.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import type {
44
LocalMcpImportService,
55
} from "@posthog/core/local-mcp/localMcpImport";
66
import { useServiceOptional } from "@posthog/di/react";
7+
import { LOCAL_MCP_IMPORT_FLAG } from "@posthog/shared";
78
import { useQuery } from "@tanstack/react-query";
9+
import { useFeatureFlag } from "../feature-flags/useFeatureFlag";
810

911
// Stable identity so consumers' memo deps don't churn while data is absent.
1012
const NO_SERVERS: LocalMcpCloudClassification[] = [];
@@ -19,21 +21,26 @@ export interface LocalMcpCloudServersResult {
1921
/**
2022
* The user's local (~/.claude.json) MCP servers classified by cloud
2123
* availability. Empty on hosts without a local workspace (web/mobile — the
22-
* service is only bound on desktop).
24+
* service is only bound on desktop) and while the feature flag is off.
2325
*/
2426
export function useLocalMcpCloudServers(
2527
enabled: boolean,
2628
): LocalMcpCloudServersResult {
2729
const service = useServiceOptional<LocalMcpImportService>(
2830
LOCAL_MCP_IMPORT_SERVICE,
2931
);
32+
const flagEnabled = useFeatureFlag(LOCAL_MCP_IMPORT_FLAG);
33+
const queryEnabled = enabled && flagEnabled && !!service;
3034

3135
const query = useQuery({
3236
queryKey: ["local-mcp-cloud-availability"],
3337
queryFn: () => (service ? service.getCloudAvailability() : NO_SERVERS),
34-
enabled: enabled && !!service,
38+
enabled: queryEnabled,
3539
staleTime: 30_000,
3640
});
3741

38-
return { servers: query.data ?? NO_SERVERS, isLoading: query.isLoading };
42+
return {
43+
servers: queryEnabled ? (query.data ?? NO_SERVERS) : NO_SERVERS,
44+
isLoading: queryEnabled && query.isLoading,
45+
};
3946
}

0 commit comments

Comments
 (0)