diff --git a/surfaces/gui/src/components/connectors/CustomMcp.test.tsx b/surfaces/gui/src/components/connectors/CustomMcp.test.tsx
new file mode 100644
index 000000000..735a5b949
--- /dev/null
+++ b/surfaces/gui/src/components/connectors/CustomMcp.test.tsx
@@ -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();
+
+ 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(
+
+
+ ,
+ );
+
+ const preset = screen.getByTestId("mcp-preset-parallel-search");
+ expect(within(preset).getByText("实时网页搜索与 URL 内容获取。无需账户或 API 密钥。")).toBeTruthy();
+ });
+});
diff --git a/surfaces/gui/src/components/connectors/CustomMcp.tsx b/surfaces/gui/src/components/connectors/CustomMcp.tsx
index 532ab5eb2..9f43c38e6 100644
--- a/surfaces/gui/src/components/connectors/CustomMcp.tsx
+++ b/surfaces/gui/src/components/connectors/CustomMcp.tsx
@@ -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;
@@ -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) {
@@ -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();
}}
>
diff --git a/surfaces/gui/src/locales/en.json b/surfaces/gui/src/locales/en.json
index 25cbc2602..c2931227a 100644
--- a/surfaces/gui/src/locales/en.json
+++ b/surfaces/gui/src/locales/en.json
@@ -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…",
diff --git a/surfaces/gui/src/locales/zh.json b/surfaces/gui/src/locales/zh.json
index 65c3389d9..30a903f75 100644
--- a/surfaces/gui/src/locales/zh.json
+++ b/surfaces/gui/src/locales/zh.json
@@ -1574,6 +1574,7 @@
"choose_a_folder": "选择一个文件夹…"
},
"mcp": {
+ "preset_parallel_search_blurb": "实时网页搜索与 URL 内容获取。无需账户或 API 密钥。",
"preset_granola_blurb": "会议笔记与转录——用你的 Granola 账户登录。",
"status_off": "已关闭",
"status_signing_in": "登录中…",