Skip to content
Open
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
81 changes: 81 additions & 0 deletions surfaces/gui/src/components/connectors/CustomMcp.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import { createInstance } from "i18next";
import { I18nextProvider } from "react-i18next";
import zh from "../../locales/zh.json";
import { CustomMcpGroup } from "./CustomMcp";

type Call = { url: string; method: string; body: unknown };

function stubMcpApi() {
const calls: Call[] = [];
vi.stubGlobal(
"fetch",
vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input);
const method = (init?.method || "GET").toUpperCase();
calls.push({
url,
method,
body: init?.body ? JSON.parse(String(init.body)) : undefined,
});
const json = method === "GET" && url.endsWith("/v1/mcp") ? { servers: [] } : { ok: true };
return { ok: true, json: async () => json } as Response;
}),
);
return calls;
}

afterEach(() => {
cleanup();
vi.unstubAllGlobals();
});

describe("CustomMcpGroup presets", () => {
it("adds Parallel Search through the existing no-auth MCP connection path", async () => {
const calls = stubMcpApi();
const onChanged = vi.fn();
render(<CustomMcpGroup servers={[]} onOpen={vi.fn()} onChanged={onChanged} />);

expect(await screen.findByText("Granola")).toBeTruthy();
const preset = screen.getByTestId("mcp-preset-parallel-search");
expect(within(preset).getByText("Parallel Search")).toBeTruthy();
expect(within(preset).getByText(/no account or API key required/i)).toBeTruthy();

fireEvent.click(within(preset).getByRole("button", { name: "Connect" }));

await waitFor(() => {
const addIndex = calls.findIndex(
(call) => call.method === "POST" && call.url.endsWith("/v1/mcp"),
);
const connectIndex = calls.findIndex(
(call) =>
call.method === "POST" && call.url.endsWith("/v1/mcp/parallel-search/connect"),
);
expect(addIndex).toBeGreaterThan(-1);
expect(connectIndex).toBeGreaterThan(addIndex);
expect(calls[addIndex].body).toEqual({
name: "parallel-search",
config: { type: "http", url: "https://search.parallel.ai/mcp" },
});
expect(onChanged).toHaveBeenCalledOnce();
});
});

it("renders the Parallel Search description in Chinese", async () => {
const i18n = createInstance();
await i18n.init({
resources: { zh: { translation: zh } },
lng: "zh",
fallbackLng: false,
});
render(
<I18nextProvider i18n={i18n}>
<CustomMcpGroup servers={[]} onOpen={vi.fn()} onChanged={vi.fn()} />
</I18nextProvider>,
);

const preset = screen.getByTestId("mcp-preset-parallel-search");
expect(within(preset).getByText("实时网页搜索与 URL 内容获取。无需账户或 API 密钥。")).toBeTruthy();
});
});
12 changes: 9 additions & 3 deletions surfaces/gui/src/components/connectors/CustomMcp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ import {
// for a stdio entry: Live = a connection is open right now; Ready = the one-time
// Test passed (subtitle carries "tested ⟨when⟩", persisted server-side).

// Curated OAuth quick-adds: remote MCP servers with browser sign-in (OAuth 2.1 +
// DCR) — no keys to paste, tokens stay in the local secret store.
// Curated remote MCP quick-adds. OAuth presets keep tokens in the local secret
// store; no-auth presets use the same explicit connection path without browser sign-in.
// `blurb` is an i18n key, resolved at render time.
export const MCP_PRESETS: {
name: string;
Expand All @@ -46,6 +46,12 @@ export const MCP_PRESETS: {
blurb: "mcp.preset_granola_blurb",
config: { type: "http", url: "https://mcp.granola.ai/mcp", auth: "oauth" },
},
{
name: "parallel-search",
label: "Parallel Search",
blurb: "mcp.preset_parallel_search_blurb",
config: { type: "http", url: "https://search.parallel.ai/mcp" },
},
];

export function mcpChip(s: McpServer) {
Expand Down Expand Up @@ -148,7 +154,7 @@ export function CustomMcpGroup({
role="button"
onClick={async () => {
await addMcpServer(p.name, p.config);
await connectMcp(p.name); // opens the browser sign-in right away
await connectMcp(p.name); // OAuth presets may open browser sign-in
onChanged();
}}
>
Expand Down
1 change: 1 addition & 0 deletions surfaces/gui/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1605,6 +1605,7 @@
"choose_a_folder": "Choose a folder…"
},
"mcp": {
"preset_parallel_search_blurb": "Live web search & URL fetching. No account or API key required.",
"preset_granola_blurb": "Meeting notes & transcripts — sign in with your Granola account.",
"status_off": "Off",
"status_signing_in": "Signing in…",
Expand Down
1 change: 1 addition & 0 deletions surfaces/gui/src/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -1574,6 +1574,7 @@
"choose_a_folder": "选择一个文件夹…"
},
"mcp": {
"preset_parallel_search_blurb": "实时网页搜索与 URL 内容获取。无需账户或 API 密钥。",
"preset_granola_blurb": "会议笔记与转录——用你的 Granola 账户登录。",
"status_off": "已关闭",
"status_signing_in": "登录中…",
Expand Down