Skip to content
Closed
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
2 changes: 2 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
61 changes: 61 additions & 0 deletions docs/parallel-search.md
Original file line number Diff line number Diff line change
@@ -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.
76 changes: 76 additions & 0 deletions tests/parallel-search-example.test.ts
Original file line number Diff line number Diff line change
@@ -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<void>(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<void>((resolve, reject) => server.close(error => error ? reject(error) : resolve()));
fs.rmSync(directory, { recursive: true, force: true });
}
}, 30_000);