pi-web-kit 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -2
- package/README.md +34 -19
- package/SECURITY.md +2 -2
- package/extensions/index.ts +141 -43
- package/package.json +11 -8
- package/src/config.ts +4 -4
- package/src/http.ts +12 -3
- package/src/install-telemetry.ts +48 -40
- package/src/limits.ts +39 -0
- package/src/providers/brave.ts +43 -16
- package/src/providers/exa.ts +28 -11
- package/src/providers/firecrawl.ts +15 -6
- package/src/providers/index.ts +0 -3
- package/src/providers/markdown-new.ts +16 -2
- package/src/providers/tinyfish.ts +137 -17
- package/src/types.ts +12 -3
- package/src/providers/exa-mcp.ts +0 -102
package/src/providers/exa-mcp.ts
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { asSnippet, fetchWithTimeout, normalizeUrls } from "../http.js";
|
|
2
|
-
import { DEFAULT_NUM_RESULTS } from "../limits.js";
|
|
3
|
-
import { urlsMatch } from "../urls.js";
|
|
4
|
-
import type { FetchInput, FetchProvider, SearchInput, SearchProvider, WebKitConfig } from "../types.js";
|
|
5
|
-
import { applyExaFetchFallbacks } from "./fallback.js";
|
|
6
|
-
|
|
7
|
-
export class ExaMcpProvider implements SearchProvider, FetchProvider {
|
|
8
|
-
private sessionId?: string;
|
|
9
|
-
private nextId = 1;
|
|
10
|
-
constructor(private config: WebKitConfig) {}
|
|
11
|
-
|
|
12
|
-
async search(input: SearchInput, signal?: AbortSignal) {
|
|
13
|
-
const result = await this.callTool("web_search_exa", { query: input.query, numResults: input.numResults ?? DEFAULT_NUM_RESULTS }, signal);
|
|
14
|
-
return { provider: "exa_mcp" as const, query: input.query, results: normalizeSearch(result) };
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
async fetch(input: FetchInput, signal?: AbortSignal) {
|
|
18
|
-
const urls = normalizeUrls(input);
|
|
19
|
-
const result = await this.callTool("web_fetch_exa", { urls }, signal);
|
|
20
|
-
const pages = normalizeFetch(result, urls);
|
|
21
|
-
return applyExaFetchFallbacks(this.config, input, urls, { provider: "exa_mcp" as const, results: pages }, signal);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
private async callTool(name: string, args: Record<string, unknown>, signal?: AbortSignal): Promise<any> {
|
|
25
|
-
await this.initialize(signal);
|
|
26
|
-
return this.rpc("tools/call", { name, arguments: args }, signal);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
private async initialize(signal?: AbortSignal) {
|
|
30
|
-
if (this.sessionId) return;
|
|
31
|
-
const data = await this.rpcRaw("initialize", { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "pi-web-kit", version: "0.1.0" } }, signal);
|
|
32
|
-
this.sessionId = data.sessionId ?? data.payload?.sessionId;
|
|
33
|
-
if (!this.sessionId) throw new Error("Exa MCP initialize did not return an mcp-session-id response header.");
|
|
34
|
-
await this.rpcRaw("notifications/initialized", undefined, signal, true);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
private async rpc(method: string, params: unknown, signal?: AbortSignal) {
|
|
38
|
-
const data = await this.rpcRaw(method, params, signal);
|
|
39
|
-
if (data.payload?.error) throw new Error(data.payload.error.message ?? JSON.stringify(data.payload.error));
|
|
40
|
-
return data.payload?.result ?? data.payload;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
private async rpcRaw(method: string, params: unknown, signal?: AbortSignal, notification = false): Promise<{ payload?: any; sessionId?: string }> {
|
|
44
|
-
const headers: Record<string, string> = { "content-type": "application/json", accept: "application/json, text/event-stream" };
|
|
45
|
-
if (this.sessionId) headers["mcp-session-id"] = this.sessionId;
|
|
46
|
-
if (this.config.apiKeys.exa) headers["x-api-key"] = this.config.apiKeys.exa;
|
|
47
|
-
const body = notification ? { jsonrpc: "2.0", method, params } : { jsonrpc: "2.0", id: this.nextId++, method, params };
|
|
48
|
-
const res = await fetchWithTimeout("https://mcp.exa.ai/mcp", { method: "POST", headers, body: JSON.stringify(body), signal, timeoutMs: 45_000 });
|
|
49
|
-
const text = await res.text();
|
|
50
|
-
if (!res.ok) throw new Error(`Exa MCP ${method} failed: ${res.status} ${res.statusText}: ${text.slice(0, 1000)}`);
|
|
51
|
-
if (notification) return { sessionId: this.sessionId };
|
|
52
|
-
const sessionId = res.headers.get("mcp-session-id") ?? undefined;
|
|
53
|
-
const payload = parseMcpResponse(text, res.headers.get("content-type") ?? "");
|
|
54
|
-
return { payload, sessionId };
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function parseMcpResponse(text: string, contentType: string): any {
|
|
59
|
-
if (!text.trim()) return undefined;
|
|
60
|
-
if (contentType.includes("text/event-stream") || text.startsWith("event:") || text.startsWith("data:")) {
|
|
61
|
-
const data = text.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim()).join("\n");
|
|
62
|
-
return data ? JSON.parse(data) : undefined;
|
|
63
|
-
}
|
|
64
|
-
return JSON.parse(text);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function textFromContent(result: any): string {
|
|
68
|
-
const content = result?.content ?? result;
|
|
69
|
-
if (Array.isArray(content)) return content.map((c) => typeof c === "string" ? c : c.text ?? JSON.stringify(c)).join("\n");
|
|
70
|
-
return typeof content === "string" ? content : JSON.stringify(content);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function normalizeSearch(result: any) {
|
|
74
|
-
const structured = result?.structuredContent ?? result?.result ?? result;
|
|
75
|
-
const list = structured.results ?? structured.data ?? structured.items;
|
|
76
|
-
if (Array.isArray(list)) return list.map(toSearchResult).filter((r: any) => r.url);
|
|
77
|
-
const text = textFromContent(result);
|
|
78
|
-
const urls = [...text.matchAll(/https?:\/\/[^\s)\]}>"']+/g)].map((m) => m[0]);
|
|
79
|
-
return [...new Set(urls)].map((url, i) => ({ url, snippet: i === 0 ? text.slice(0, 1000) : undefined, position: i + 1 }));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function toSearchResult(r: any, index: number) {
|
|
83
|
-
return {
|
|
84
|
-
title: r.title,
|
|
85
|
-
url: r.url,
|
|
86
|
-
snippet: asSnippet(r.snippet ?? r.text ?? r.summary ?? r.highlights),
|
|
87
|
-
siteName: r.siteName,
|
|
88
|
-
position: index + 1,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function normalizeFetch(result: any, urls: string[]) {
|
|
93
|
-
const structured = result?.structuredContent ?? result?.result ?? result;
|
|
94
|
-
const list = structured.results ?? structured.data ?? structured.pages;
|
|
95
|
-
if (Array.isArray(list)) return urls.map((url, i) => {
|
|
96
|
-
const r = list.find((x: any) => urlsMatch(x.url, url)) ?? list[i];
|
|
97
|
-
if (!r) return { url, error: "No content returned by Exa MCP." };
|
|
98
|
-
return { url, title: r.title, content: r.markdown ?? r.text ?? r.content ?? r.html, format: "markdown" as const, metadata: r, error: r.error };
|
|
99
|
-
});
|
|
100
|
-
const text = textFromContent(result);
|
|
101
|
-
return urls.length <= 1 ? [{ url: urls[0] ?? "", content: text, format: "markdown" as const }] : urls.map((url) => ({ url, content: text, format: "markdown" as const }));
|
|
102
|
-
}
|