From b5f681f76be59a31c4c1922e61457e16e8b518bc Mon Sep 17 00:00:00 2001 From: georgeatparallel Date: Thu, 13 Aug 2026 17:39:26 -0700 Subject: [PATCH] Add a Parallel Search MCP preset Signed-off-by: georgeatparallel --- .../gui/src/components/ManageTabs.test.tsx | 59 +++++++++++++++++++ surfaces/gui/src/components/ManageTabs.tsx | 14 +++-- 2 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 surfaces/gui/src/components/ManageTabs.test.tsx diff --git a/surfaces/gui/src/components/ManageTabs.test.tsx b/surfaces/gui/src/components/ManageTabs.test.tsx new file mode 100644 index 0000000000..730d638640 --- /dev/null +++ b/surfaces/gui/src/components/ManageTabs.test.tsx @@ -0,0 +1,59 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react"; +import { McpTab } from "./ManageTabs"; + +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("McpTab presets", () => { + it("adds Parallel Search through the existing no-auth MCP connection path", async () => { + const calls = stubMcpApi(); + 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/)).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" }, + }); + }); + }); +}); diff --git a/surfaces/gui/src/components/ManageTabs.tsx b/surfaces/gui/src/components/ManageTabs.tsx index f9ab299284..2b0f7ccb3c 100644 --- a/surfaces/gui/src/components/ManageTabs.tsx +++ b/surfaces/gui/src/components/ManageTabs.tsx @@ -232,8 +232,8 @@ function ComposerPickerCard({ ); } -// 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. First: Granola. +// Curated remote MCP quick-adds. OAuth presets keep tokens in the local secret store; +// no-auth presets use the same explicit connection path without opening a browser. const MCP_PRESETS: { name: string; label: string; blurb: string; config: Record }[] = [ { name: "granola", @@ -241,6 +241,12 @@ const MCP_PRESETS: { name: string; label: string; blurb: string; config: Record< blurb: "Meeting notes & transcripts — sign in with your Granola account.", config: { type: "http", url: "https://mcp.granola.ai/mcp", auth: "oauth" }, }, + { + name: "parallel-search", + label: "Parallel Search", + blurb: "Live web search & URL fetching — no account or API key required.", + config: { type: "http", url: "https://search.parallel.ai/mcp" }, + }, ]; export function McpTab() { @@ -306,7 +312,7 @@ export function McpTab() { )} - {/* One-click OAuth presets not yet configured. */} + {/* One-click MCP presets not yet configured. */} {MCP_PRESETS.filter((p) => !servers.some((s) => s.name === p.name)).map((p) => (
@@ -317,7 +323,7 @@ export function McpTab() { className={BTN_ACCENT} 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 refresh(); }} >