From 60fe00ea21a311c6ed35e916131a6a1dc8711ecc Mon Sep 17 00:00:00 2001 From: George at Parallel Date: Fri, 9 Oct 2026 02:59:06 +0000 Subject: [PATCH] docs(mcp): add keyless Parallel search and fetch example --- docs/configuration.md | 2 + docs/parallel-search.md | 61 +++++++++++++++++++++ tests/parallel-search-example.test.ts | 76 +++++++++++++++++++++++++++ 3 files changed, 139 insertions(+) create mode 100644 docs/parallel-search.md create mode 100644 tests/parallel-search-example.test.ts diff --git a/docs/configuration.md b/docs/configuration.md index d35bef878..747d0e488 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -633,6 +633,8 @@ Fabric handles staleness in stale-while-revalidate style. Sessions adopt the cac - `mcp.jev.semanticCandidateLimit`: max tools sent to Jev (2–127, default 127). Half the slots are lexical hits; the rest recover tools the query would not name. - `mcp.jev.semanticMinProbability`: minimum head probability to accept a match (0–1, default 0.2). Below that, or if Jev chooses `none`, search abstains. Timeout, rate-limit, and 5xx responses fall back to lexical ranking and mark `backend.degraded`. +For a keyless HTTP server example, see [Parallel web search and fetch](parallel-search.md). + See the [TypeScript MCP reference](../skillsets/typescript/fabric-exec/references/mcp.md) or [Python MCP reference](../skillsets/python/fabric-exec/references/mcp.md) for the selected call surface. ## UI diff --git a/docs/parallel-search.md b/docs/parallel-search.md new file mode 100644 index 000000000..ad44d3409 --- /dev/null +++ b/docs/parallel-search.md @@ -0,0 +1,61 @@ +# Parallel web search and fetch + +[Parallel Search MCP](https://docs.parallel.ai/integrations/mcp/search-mcp) provides +web search and page extraction without a Parallel API key. Anonymous access is +free for exploration and light use, with rate limits. This example uses Fabric's +default mcporter connection over Streamable HTTP. + +## Configure a project + +[Install Pi Fabric](../README.md#install) with Node.js 24+ and Pi 1.0.0+: + +```sh +pi install npm:pi-fabric +``` + +In the project where you run Pi, add this entry to `config/mcporter.json`. Merge +`parallel` into an existing `mcpServers` object to keep your other servers: + +```json +{ + "mcpServers": { + "parallel": { + "baseUrl": "https://search.parallel.ai/mcp", + "headers": { + "User-Agent": "pi-fabric/parallel-search-example" + } + } + } +} +``` + +No environment variable, authorization header, or OAuth login is needed. Start Pi +in that project, or call `mcp.reload()` inside `fabric_exec` after editing the +configuration. This entry uses mcporter; keep `parallel` out of +`mcp.nativeServers`. See [MCP ownership](configuration.md#opt-in-pi-owned-servers) +if you already manage servers through Pi. + +## Search, then read a page + +Ask Pi to run this TypeScript program with `fabric_exec` using the default +TypeScript kernel: + +```ts +const search = await mcp.parallel.web_search({ + objective: "Find Pi Fabric's MCP configuration documentation.", + search_queries: ["pi-fabric MCP configuration"], +}); +const page = await mcp.parallel.web_fetch({ + urls: ["https://raw.githubusercontent.com/fabric-runtime/pi-fabric/main/docs/configuration.md"], + objective: "Extract the MCP configuration options.", +}); +return { search: search.text, page: page.text }; +``` + +The result contains search excerpts and the fetched page's relevant content. +Replace the query and URL for your task; `web_fetch` accepts up to 20 URLs. +Results retain Fabric's normal `{ text, content, structuredContent }` shape. +Use `tools.describe({ ref: "mcp.parallel.web_search" })` or +`tools.describe({ ref: "mcp.parallel.web_fetch" })` inside `fabric_exec` to inspect +the current input schemas. Calls use the configured `mcp.callTimeoutMs` and +Fabric's existing approval policy. diff --git a/tests/parallel-search-example.test.ts b/tests/parallel-search-example.test.ts new file mode 100644 index 000000000..54f37d4f1 --- /dev/null +++ b/tests/parallel-search-example.test.ts @@ -0,0 +1,76 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import http from "node:http"; +import type { ExtensionContext } from "@earendil-works/pi-coding-agent"; +import { expect, it } from "vitest"; +import { DEFAULT_FABRIC_CONFIG } from "../src/config.js"; +import { ActionRegistry } from "../src/core/action-registry.js"; +import { FabricExecutionService } from "../src/execution-service.js"; +import { McpProvider } from "../src/providers/mcp-provider.js"; + +it("runs the documented Parallel program through Fabric over anonymous HTTP", async () => { + const doc = fs.readFileSync("docs/parallel-search.md", "utf8"); + const example = JSON.parse(doc.match(/```json\n([\s\S]*?)```/)![1]!); + const code = doc.match(/```ts\n([\s\S]*?)```/)![1]!; + expect(example.mcpServers.parallel.baseUrl).toBe("https://search.parallel.ai/mcp"); + const requests: Array<{ url: string; headers: http.IncomingHttpHeaders; body: any }> = []; + const server = http.createServer(async (req, res) => { + if (req.method !== "POST") { res.writeHead(405).end(); return; } + let data = ""; + for await (const chunk of req) data += chunk; + const body = JSON.parse(data); + requests.push({ url: req.url!, headers: req.headers, body }); + if (body.id === undefined) { res.writeHead(202).end(); return; } + let result: unknown; + if (body.method === "initialize") { + result = { protocolVersion: body.params.protocolVersion, capabilities: { tools: {} }, serverInfo: { name: "parallel-fixture", version: "1" } }; + } else if (body.method === "tools/list") { + result = { tools: [ + { name: "web_search", description: "Search", inputSchema: { type: "object", properties: { objective: { type: "string" }, search_queries: { type: "array", items: { type: "string" } } }, required: ["objective", "search_queries"] } }, + { name: "web_fetch", description: "Fetch", inputSchema: { type: "object", properties: { urls: { type: "array", items: { type: "string" } }, objective: { type: "string" } }, required: ["urls"] } }, + ] }; + } else if (body.method === "tools/call") { + result = { content: [{ type: "text", text: body.params.name === "web_search" ? "Pi Fabric MCP configuration: https://github.com/fabric-runtime/pi-fabric" : "MCP uses config/mcporter.json and mcp.callTimeoutMs." }] }; + } else { res.writeHead(400).end(); return; } + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ jsonrpc: "2.0", id: body.id, result })); + }); + await new Promise(resolve => server.listen(0, "127.0.0.1", resolve)); + const directory = fs.mkdtempSync(path.join(os.tmpdir(), "fabric-parallel-")); + const config = structuredClone(DEFAULT_FABRIC_CONFIG); + const defaults = structuredClone(config); + // Only redirect the documented endpoint; mcporter loads the actual JSON shape. + const port = (server.address() as import("node:net").AddressInfo).port; + example.mcpServers.parallel.baseUrl = `http://127.0.0.1:${port}/mcp`; + example.imports = []; + const configPath = path.join(directory, "mcporter.json"); + fs.writeFileSync(configPath, JSON.stringify(example)); + const provider = new McpProvider(directory, { ...config.mcp, configPath }); + const registry = new ActionRegistry(); + registry.register(provider); + try { + const result = await new FabricExecutionService(registry, config).execute({ + code, parentToolCallId: "parallel-example", signal: undefined, + context: { cwd: directory, hasUI: false } as ExtensionContext, onPartial() {}, + }); + expect(result.error).toBeUndefined(); + expect(result.success).toBe(true); + expect(result.value).toEqual({ search: "Pi Fabric MCP configuration: https://github.com/fabric-runtime/pi-fabric", page: "MCP uses config/mcporter.json and mcp.callTimeoutMs." }); + const calls = requests.filter(r => r.body.method === "tools/call"); + expect(calls.map(r => r.body.params.name)).toEqual(["web_search", "web_fetch"]); + expect(calls[0]!.body.params.arguments.search_queries).toEqual(["pi-fabric MCP configuration"]); + expect(calls[1]!.body.params.arguments.urls).toEqual(["https://raw.githubusercontent.com/fabric-runtime/pi-fabric/main/docs/configuration.md"]); + expect(requests.some(r => r.body.method === "tools/list")).toBe(true); + for (const request of requests) { + expect(request.url).toBe("/mcp"); + expect(request.headers["user-agent"]).toBe("pi-fabric/parallel-search-example"); + expect(request.headers.authorization).toBeUndefined(); + } + expect(config).toEqual(defaults); + } finally { + await provider.close(); + await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve())); + fs.rmSync(directory, { recursive: true, force: true }); + } +}, 30_000);