openclaw-exa-search 1.0.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/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # openclaw-exa-search
2
+
3
+ Exa AI search plugin for [OpenClaw](https://openclaw.ai). No API key required — uses [Exa's public MCP endpoint](https://mcp.exa.ai).
4
+
5
+ ## Tools
6
+
7
+ | Tool | Parameters | Description |
8
+ |------|-----------|-------------|
9
+ | `exa_web_search` | `query`, `numResults?`, `type?` (`auto`/`fast`/`deep`), `livecrawl?` (`fallback`/`preferred`) | Neural web search — news, facts, current info. |
10
+ | `exa_code_search` | `query`, `tokensNum?` (1000–50000) | Find code examples, docs, and solutions from GitHub, Stack Overflow, and official documentation. |
11
+ | `exa_company_research` | `companyName`, `numResults?` | Research any company — products, services, recent news, industry position. |
12
+ | `exa_twitter_search` | `query`, `numResults?`, `startPublishedDate?`, `endPublishedDate?` | Search Twitter/X posts and discussions. Filter by date range (ISO 8601). |
13
+ | `exa_people_search` | `query`, `numResults?` | Find people by name, role, company, or expertise. Returns public LinkedIn and professional profiles. |
14
+ | `exa_financial_report_search` | `query`, `numResults?`, `startPublishedDate?`, `endPublishedDate?` | Search SEC filings (10-K, 10-Q), earnings reports, and annual/quarterly financial documents. |
15
+
16
+ All tools are enabled by default. Requests time out after 30 seconds.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ openclaw plugins install openclaw-exa-search
22
+ openclaw plugins enable openclaw-exa-search
23
+ openclaw gateway restart
24
+ ```
25
+
26
+ If you have a `plugins.allow` allowlist configured, add the plugin id:
27
+
28
+ ```json
29
+ {
30
+ "plugins": {
31
+ "allow": ["openclaw-exa-search"]
32
+ }
33
+ }
34
+ ```
35
+
36
+ Verify:
37
+
38
+ ```bash
39
+ openclaw plugins list | grep openclaw-exa-search
40
+ ```
41
+
42
+ ## Examples
43
+
44
+ - "Search Twitter for what people are saying about GPT-5"
45
+ - "Find the latest Apple 10-Q filing"
46
+ - "Research Anthropic as a company"
47
+ - "Find CTO profiles at YC startups"
48
+ - "Search for React Server Components examples"
49
+
50
+ ## Development
51
+
52
+ ```bash
53
+ npm install
54
+ npm run typecheck
55
+ npm test # unit tests
56
+ npm run test:integration # integration tests (hits real Exa endpoint)
57
+ npm run build
58
+ ```
59
+
60
+ ## Notes
61
+
62
+ - This plugin depends on Exa's public MCP endpoint (`mcp.exa.ai`). If Exa adds authentication or rate limiting in the future, the plugin will need updates.
63
+ - Date parameters use ISO 8601 format (e.g., `2024-01-01T00:00:00.000Z`).
64
+
65
+ ## License
66
+
67
+ MIT
68
+
69
+ ## Acknowledgements
70
+
71
+ - [exa-mcp-server](https://github.com/exa-labs/exa-mcp-server) — Official Exa MCP server
72
+ - [exa-search](https://github.com/wysh3/exa-search) — Exa search MCP integration
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Exa MCP Client — HTTP/SSE communication with Exa hosted MCP endpoint
3
+ * No API key required; uses mcp.exa.ai with all tools enabled
4
+ */
5
+ interface ExaSuccessResponse {
6
+ jsonrpc: "2.0";
7
+ id: number;
8
+ result: {
9
+ content?: Array<{
10
+ type: string;
11
+ text: string;
12
+ }>;
13
+ tools?: Array<{
14
+ name: string;
15
+ description: string;
16
+ inputSchema: unknown;
17
+ }>;
18
+ };
19
+ }
20
+ interface ExaErrorResponse {
21
+ jsonrpc: "2.0";
22
+ id: number;
23
+ error: {
24
+ code: number;
25
+ message: string;
26
+ };
27
+ }
28
+ type ExaResponse = ExaSuccessResponse | ExaErrorResponse;
29
+ /**
30
+ * Parse SSE response — extract the last complete JSON-RPC message.
31
+ * Per SSE spec, multiple `data:` lines within one event are joined with "\n",
32
+ * but Exa sends one JSON object per `data:` line. We take the last `data:` line
33
+ * that parses as valid JSON to handle both single and multi-event streams.
34
+ */
35
+ export declare function parseSSEResponse(text: string): string;
36
+ /**
37
+ * Call an Exa MCP tool
38
+ */
39
+ export declare function callExa(method: string, params?: Record<string, unknown>, fetchFn?: typeof fetch): Promise<ExaResponse>;
40
+ /**
41
+ * Extract text content from an Exa response
42
+ */
43
+ export declare function extractTextContent(response: ExaResponse): string;
44
+ export declare function webSearch(params: {
45
+ query: string;
46
+ numResults?: number;
47
+ type?: "auto" | "fast" | "deep";
48
+ livecrawl?: "fallback" | "preferred";
49
+ }): Promise<string>;
50
+ export declare function codeSearch(params: {
51
+ query: string;
52
+ tokensNum?: number;
53
+ }): Promise<string>;
54
+ export declare function companyResearch(params: {
55
+ companyName: string;
56
+ numResults?: number;
57
+ }): Promise<string>;
58
+ export declare function twitterSearch(params: {
59
+ query: string;
60
+ numResults?: number;
61
+ startPublishedDate?: string;
62
+ endPublishedDate?: string;
63
+ }): Promise<string>;
64
+ export declare function peopleSearch(params: {
65
+ query: string;
66
+ numResults?: number;
67
+ }): Promise<string>;
68
+ export declare function financialSearch(params: {
69
+ query: string;
70
+ numResults?: number;
71
+ startPublishedDate?: string;
72
+ endPublishedDate?: string;
73
+ }): Promise<string>;
74
+ export {};
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Exa MCP Client — HTTP/SSE communication with Exa hosted MCP endpoint
3
+ * No API key required; uses mcp.exa.ai with all tools enabled
4
+ */
5
+ const EXA_BASE_URL = "https://mcp.exa.ai/mcp";
6
+ const EXA_TOOLS = [
7
+ "web_search_exa",
8
+ "web_search_advanced_exa",
9
+ "get_code_context_exa",
10
+ "company_research_exa",
11
+ "people_search_exa",
12
+ ].join(",");
13
+ const EXA_URL = `${EXA_BASE_URL}?tools=${EXA_TOOLS}`;
14
+ const REQUEST_TIMEOUT_MS = 30_000;
15
+ let requestIdCounter = 0;
16
+ /**
17
+ * Parse SSE response — extract the last complete JSON-RPC message.
18
+ * Per SSE spec, multiple `data:` lines within one event are joined with "\n",
19
+ * but Exa sends one JSON object per `data:` line. We take the last `data:` line
20
+ * that parses as valid JSON to handle both single and multi-event streams.
21
+ */
22
+ export function parseSSEResponse(text) {
23
+ const lines = text.split("\n");
24
+ const dataLines = [];
25
+ for (const line of lines) {
26
+ if (line.startsWith("data: ")) {
27
+ dataLines.push(line.substring(6));
28
+ }
29
+ }
30
+ if (dataLines.length === 0) {
31
+ throw new Error("No data: field found in SSE response");
32
+ }
33
+ // Try each data line from last to first — return the first valid JSON
34
+ for (let i = dataLines.length - 1; i >= 0; i--) {
35
+ try {
36
+ JSON.parse(dataLines[i]);
37
+ return dataLines[i];
38
+ }
39
+ catch {
40
+ // not valid JSON, try next
41
+ }
42
+ }
43
+ // Fallback: join all and let caller handle parse error
44
+ return dataLines.join("");
45
+ }
46
+ /**
47
+ * Call an Exa MCP tool
48
+ */
49
+ export async function callExa(method, params, fetchFn = fetch) {
50
+ const requestId = ++requestIdCounter;
51
+ const request = {
52
+ jsonrpc: "2.0",
53
+ id: requestId,
54
+ method,
55
+ params,
56
+ };
57
+ const controller = new AbortController();
58
+ const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
59
+ try {
60
+ const response = await fetchFn(EXA_URL, {
61
+ method: "POST",
62
+ headers: {
63
+ "Content-Type": "application/json",
64
+ Accept: "application/json, text/event-stream",
65
+ },
66
+ body: JSON.stringify(request),
67
+ signal: controller.signal,
68
+ });
69
+ if (!response.ok) {
70
+ throw new Error(`HTTP error: ${response.status} ${response.statusText}`);
71
+ }
72
+ const text = await response.text();
73
+ // Try JSON first, fallback to SSE parsing
74
+ try {
75
+ return JSON.parse(text);
76
+ }
77
+ catch {
78
+ const jsonData = parseSSEResponse(text);
79
+ return JSON.parse(jsonData);
80
+ }
81
+ }
82
+ catch (error) {
83
+ if (error instanceof DOMException && error.name === "AbortError") {
84
+ throw new Error(`Exa request timed out after ${REQUEST_TIMEOUT_MS / 1000}s`);
85
+ }
86
+ throw error;
87
+ }
88
+ finally {
89
+ clearTimeout(timer);
90
+ }
91
+ }
92
+ /**
93
+ * Extract text content from an Exa response
94
+ */
95
+ export function extractTextContent(response) {
96
+ if ("error" in response) {
97
+ throw new Error(`Exa API error: ${response.error.code} - ${response.error.message}`);
98
+ }
99
+ const result = response.result;
100
+ if (!result?.content) {
101
+ throw new Error("No content in response");
102
+ }
103
+ return result.content
104
+ .filter((item) => item.type === "text")
105
+ .map((item) => item.text)
106
+ .join("\n\n");
107
+ }
108
+ // ─── Tool Functions ────────────────────────────────────────────────
109
+ export async function webSearch(params) {
110
+ const response = await callExa("tools/call", {
111
+ name: "web_search_exa",
112
+ arguments: params,
113
+ });
114
+ return extractTextContent(response);
115
+ }
116
+ export async function codeSearch(params) {
117
+ const response = await callExa("tools/call", {
118
+ name: "get_code_context_exa",
119
+ arguments: params,
120
+ });
121
+ return extractTextContent(response);
122
+ }
123
+ export async function companyResearch(params) {
124
+ const response = await callExa("tools/call", {
125
+ name: "company_research_exa",
126
+ arguments: params,
127
+ });
128
+ return extractTextContent(response);
129
+ }
130
+ export async function twitterSearch(params) {
131
+ const args = {
132
+ query: params.query,
133
+ category: "tweet",
134
+ numResults: params.numResults ?? 10,
135
+ };
136
+ if (params.startPublishedDate !== undefined)
137
+ args.startPublishedDate = params.startPublishedDate;
138
+ if (params.endPublishedDate !== undefined)
139
+ args.endPublishedDate = params.endPublishedDate;
140
+ const response = await callExa("tools/call", {
141
+ name: "web_search_advanced_exa",
142
+ arguments: args,
143
+ });
144
+ return extractTextContent(response);
145
+ }
146
+ export async function peopleSearch(params) {
147
+ const response = await callExa("tools/call", {
148
+ name: "people_search_exa",
149
+ arguments: params,
150
+ });
151
+ return extractTextContent(response);
152
+ }
153
+ export async function financialSearch(params) {
154
+ const args = {
155
+ query: params.query,
156
+ category: "financial report",
157
+ numResults: params.numResults ?? 10,
158
+ };
159
+ if (params.startPublishedDate !== undefined)
160
+ args.startPublishedDate = params.startPublishedDate;
161
+ if (params.endPublishedDate !== undefined)
162
+ args.endPublishedDate = params.endPublishedDate;
163
+ const response = await callExa("tools/call", {
164
+ name: "web_search_advanced_exa",
165
+ arguments: args,
166
+ });
167
+ return extractTextContent(response);
168
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * OpenClaw Exa Search Plugin
3
+ *
4
+ * Registers 6 tools: web_search, code_search, company_research,
5
+ * twitter_search, people_search, financial_report_search
6
+ */
7
+ interface ToolDefinition {
8
+ name: string;
9
+ description: string;
10
+ parameters: Record<string, unknown>;
11
+ execute: (id: string, params: Record<string, unknown>) => Promise<{
12
+ content: Array<{
13
+ type: string;
14
+ text: string;
15
+ }>;
16
+ isError?: boolean;
17
+ }>;
18
+ }
19
+ interface PluginAPI {
20
+ registerTool: (tool: ToolDefinition) => void;
21
+ }
22
+ declare const TOOLS: ToolDefinition[];
23
+ export default function (api: PluginAPI): void;
24
+ export { TOOLS };
package/dist/index.js ADDED
@@ -0,0 +1,167 @@
1
+ /**
2
+ * OpenClaw Exa Search Plugin
3
+ *
4
+ * Registers 6 tools: web_search, code_search, company_research,
5
+ * twitter_search, people_search, financial_report_search
6
+ */
7
+ import { webSearch, codeSearch, companyResearch, twitterSearch, peopleSearch, financialSearch, } from "./exa-mcp-client.js";
8
+ function wrapExecute(fn) {
9
+ return async (_id, params) => {
10
+ try {
11
+ const result = await fn(params);
12
+ return { content: [{ type: "text", text: result }] };
13
+ }
14
+ catch (error) {
15
+ const message = error instanceof Error ? error.message : String(error);
16
+ return {
17
+ content: [{ type: "text", text: `Error: ${message}` }],
18
+ isError: true,
19
+ };
20
+ }
21
+ };
22
+ }
23
+ const TOOLS = [
24
+ {
25
+ name: "exa_web_search",
26
+ description: "Search the web using Exa AI neural search. Find current information, news, facts, or answer questions about any topic. Returns clean, formatted content ready for LLM use.",
27
+ parameters: {
28
+ type: "object",
29
+ properties: {
30
+ query: { type: "string", description: "Web search query" },
31
+ numResults: {
32
+ type: "number",
33
+ description: "Number of search results to return (default: 8)",
34
+ },
35
+ type: {
36
+ type: "string",
37
+ enum: ["auto", "fast", "deep"],
38
+ description: "Search type - 'auto': balanced search (default), 'fast': quick results, 'deep': comprehensive research",
39
+ },
40
+ livecrawl: {
41
+ type: "string",
42
+ enum: ["fallback", "preferred"],
43
+ description: "Live crawl mode - 'fallback': use live crawling as backup if cached unavailable, 'preferred': prioritize live crawling",
44
+ },
45
+ },
46
+ required: ["query"],
47
+ },
48
+ execute: wrapExecute(webSearch),
49
+ },
50
+ {
51
+ name: "exa_code_search",
52
+ description: "Find code examples, documentation, and programming solutions from GitHub, Stack Overflow, and official docs. Useful for API usage, library examples, code snippets, and debugging help.",
53
+ parameters: {
54
+ type: "object",
55
+ properties: {
56
+ query: {
57
+ type: "string",
58
+ description: "Search query for code context - e.g., 'React useState hook examples', 'Python pandas dataframe filtering'",
59
+ },
60
+ tokensNum: {
61
+ type: "number",
62
+ description: "Number of tokens to return (1000-50000). Lower for focused queries, higher for comprehensive docs (default: 5000)",
63
+ },
64
+ },
65
+ required: ["query"],
66
+ },
67
+ execute: wrapExecute(codeSearch),
68
+ },
69
+ {
70
+ name: "exa_company_research",
71
+ description: "Research any company to get business information, news, and insights. Returns information from trusted business sources about products, services, recent news, or industry position.",
72
+ parameters: {
73
+ type: "object",
74
+ properties: {
75
+ companyName: {
76
+ type: "string",
77
+ description: "Name of the company to research",
78
+ },
79
+ numResults: {
80
+ type: "number",
81
+ description: "Number of search results to return (default: 5)",
82
+ },
83
+ },
84
+ required: ["companyName"],
85
+ },
86
+ execute: wrapExecute(companyResearch),
87
+ },
88
+ {
89
+ name: "exa_twitter_search",
90
+ description: "Search Twitter/X posts. Find tweets, discussions, and social commentary on any topic. Useful for tracking public sentiment, announcements, and trending discussions.",
91
+ parameters: {
92
+ type: "object",
93
+ properties: {
94
+ query: {
95
+ type: "string",
96
+ description: "Search query for Twitter/X posts",
97
+ },
98
+ numResults: {
99
+ type: "number",
100
+ description: "Number of tweets to return (default: 10)",
101
+ },
102
+ startPublishedDate: {
103
+ type: "string",
104
+ description: "Filter tweets published after this date (ISO 8601 format, e.g., '2024-01-01T00:00:00.000Z')",
105
+ },
106
+ endPublishedDate: {
107
+ type: "string",
108
+ description: "Filter tweets published before this date (ISO 8601 format)",
109
+ },
110
+ },
111
+ required: ["query"],
112
+ },
113
+ execute: wrapExecute(twitterSearch),
114
+ },
115
+ {
116
+ name: "exa_people_search",
117
+ description: "Find people and their professional profiles. Search for individuals by name, role, company, or expertise. Returns public LinkedIn and professional profile data.",
118
+ parameters: {
119
+ type: "object",
120
+ properties: {
121
+ query: {
122
+ type: "string",
123
+ description: "Search query for people - e.g., 'CTO at Anthropic', 'machine learning researchers Stanford'",
124
+ },
125
+ numResults: {
126
+ type: "number",
127
+ description: "Number of results to return (default: 5)",
128
+ },
129
+ },
130
+ required: ["query"],
131
+ },
132
+ execute: wrapExecute(peopleSearch),
133
+ },
134
+ {
135
+ name: "exa_financial_report_search",
136
+ description: "Search financial reports — 10-K, 10-Q, annual reports, quarterly earnings, and SEC filings for public companies.",
137
+ parameters: {
138
+ type: "object",
139
+ properties: {
140
+ query: {
141
+ type: "string",
142
+ description: "Search query for financial reports - e.g., 'Apple 2024 Q4 earnings', 'Tesla annual report 2024'",
143
+ },
144
+ numResults: {
145
+ type: "number",
146
+ description: "Number of results to return (default: 10)",
147
+ },
148
+ startPublishedDate: {
149
+ type: "string",
150
+ description: "Filter reports published after this date (ISO 8601 format)",
151
+ },
152
+ endPublishedDate: {
153
+ type: "string",
154
+ description: "Filter reports published before this date (ISO 8601 format)",
155
+ },
156
+ },
157
+ required: ["query"],
158
+ },
159
+ execute: wrapExecute(financialSearch),
160
+ },
161
+ ];
162
+ export default function (api) {
163
+ for (const tool of TOOLS) {
164
+ api.registerTool(tool);
165
+ }
166
+ }
167
+ export { TOOLS };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Integration tests — hits real Exa MCP endpoint
3
+ * Run with: npm run test:integration
4
+ */
5
+ export {};
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Integration tests — hits real Exa MCP endpoint
3
+ * Run with: npm run test:integration
4
+ */
5
+ import { describe, it, expect } from "vitest";
6
+ import { webSearch, codeSearch, companyResearch, twitterSearch, peopleSearch, financialSearch, } from "./exa-mcp-client.js";
7
+ const TIMEOUT = 30_000;
8
+ describe("integration: Exa MCP endpoint", () => {
9
+ it("exa_web_search", async () => {
10
+ const result = await webSearch({ query: "OpenAI latest news", numResults: 3 });
11
+ expect(result).toBeTruthy();
12
+ expect(result.length).toBeGreaterThan(50);
13
+ console.log("[web_search] ✅", result.slice(0, 200), "...");
14
+ }, TIMEOUT);
15
+ it("exa_code_search", async () => {
16
+ const result = await codeSearch({ query: "React useState hook examples", tokensNum: 2000 });
17
+ expect(result).toBeTruthy();
18
+ expect(result.length).toBeGreaterThan(50);
19
+ console.log("[code_search] ✅", result.slice(0, 200), "...");
20
+ }, TIMEOUT);
21
+ it("exa_company_research", async () => {
22
+ const result = await companyResearch({ companyName: "Anthropic", numResults: 3 });
23
+ expect(result).toBeTruthy();
24
+ expect(result.length).toBeGreaterThan(50);
25
+ console.log("[company_research] ✅", result.slice(0, 200), "...");
26
+ }, TIMEOUT);
27
+ it("exa_twitter_search", async () => {
28
+ const result = await twitterSearch({ query: "AI agents", numResults: 5 });
29
+ expect(result).toBeTruthy();
30
+ expect(result.length).toBeGreaterThan(50);
31
+ console.log("[twitter_search] ✅", result.slice(0, 200), "...");
32
+ }, TIMEOUT);
33
+ it("exa_people_search", async () => {
34
+ const result = await peopleSearch({ query: "CTO at Anthropic", numResults: 3 });
35
+ expect(result).toBeTruthy();
36
+ expect(result.length).toBeGreaterThan(50);
37
+ console.log("[people_search] ✅", result.slice(0, 200), "...");
38
+ }, TIMEOUT);
39
+ it("exa_financial_search", async () => {
40
+ const result = await financialSearch({ query: "Apple 2024 annual report", numResults: 3 });
41
+ expect(result).toBeTruthy();
42
+ expect(result.length).toBeGreaterThan(50);
43
+ console.log("[financial_search] ✅", result.slice(0, 200), "...");
44
+ }, TIMEOUT);
45
+ });
@@ -0,0 +1,11 @@
1
+ {
2
+ "id": "openclaw-exa-search",
3
+ "name": "Exa Search",
4
+ "version": "1.0.0",
5
+ "description": "Exa AI search integration for OpenClaw — web search, code search, company research, Twitter/X search, people search, and financial report search. No API key required.",
6
+ "author": "sawyer0x110",
7
+ "license": "MIT",
8
+ "repository": "https://github.com/sawyer0x110/openclaw-exa-search",
9
+ "keywords": ["search", "exa", "web", "code", "twitter", "financial"],
10
+ "main": "./dist/index.js"
11
+ }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "openclaw-exa-search",
3
+ "version": "1.0.0",
4
+ "description": "OpenClaw plugin for Exa AI search — web, code, company, Twitter/X, people, and financial report search.",
5
+ "engines": {
6
+ "node": ">=18"
7
+ },
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "test": "vitest run",
13
+ "test:integration": "vitest run --config vitest.integration.config.ts",
14
+ "test:watch": "vitest",
15
+ "typecheck": "tsc --noEmit"
16
+ },
17
+ "keywords": ["openclaw", "openclaw-plugin", "exa", "search", "web-search", "code-search", "company-research", "twitter-search", "people-search", "financial-reports", "ai", "neural-search", "mcp"],
18
+ "author": "sawyer0x110",
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/sawyer0x110/openclaw-exa-search"
23
+ },
24
+ "devDependencies": {
25
+ "typescript": "^5.7.0",
26
+ "vitest": "^3.0.0"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "openclaw.plugin.json",
31
+ "README.md"
32
+ ]
33
+ }