@oh-my-pi/exa 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/README.md +153 -0
- package/package.json +56 -0
- package/tools/exa/company.ts +35 -0
- package/tools/exa/index.ts +66 -0
- package/tools/exa/linkedin.ts +35 -0
- package/tools/exa/researcher.ts +40 -0
- package/tools/exa/runtime.json +4 -0
- package/tools/exa/search.ts +46 -0
- package/tools/exa/shared.ts +230 -0
- package/tools/exa/websets.ts +62 -0
package/README.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Exa Plugin
|
|
2
|
+
|
|
3
|
+
Exa AI web search and websets tools for pi.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Install with default features (search only)
|
|
9
|
+
omp install @oh-my-pi/exa
|
|
10
|
+
|
|
11
|
+
# Install with all features
|
|
12
|
+
omp install @oh-my-pi/exa[*]
|
|
13
|
+
|
|
14
|
+
# Install with specific features
|
|
15
|
+
omp install @oh-my-pi/exa[search,linkedin,websets]
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
| Feature | Default | Description | Tools |
|
|
21
|
+
|---------|---------|-------------|-------|
|
|
22
|
+
| `search` | ✓ | Core web search capabilities | 4 tools |
|
|
23
|
+
| `linkedin` | | LinkedIn profile and company search | 1 tool |
|
|
24
|
+
| `company` | | Comprehensive company research | 1 tool |
|
|
25
|
+
| `researcher` | | Long-running AI research tasks | 2 tools |
|
|
26
|
+
| `websets` | | Entity collection management | 14 tools |
|
|
27
|
+
|
|
28
|
+
Manage features after install:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
omp features @oh-my-pi/exa # Interactive UI or list features
|
|
32
|
+
omp features @oh-my-pi/exa --enable websets # Enable websets
|
|
33
|
+
omp features @oh-my-pi/exa --disable search # Disable search
|
|
34
|
+
omp features @oh-my-pi/exa --set search,linkedin,websets # Set exact features
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Feature state is stored in `runtime.json` which is copied (not symlinked) to the install location. You can also edit it directly:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
cat ~/.pi/agent/tools/exa/runtime.json
|
|
41
|
+
# {"features": ["search"], "options": {}}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Setup
|
|
45
|
+
|
|
46
|
+
Set your Exa API key:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Option 1: Use omp config
|
|
50
|
+
omp config @oh-my-pi/exa apiKey YOUR_API_KEY
|
|
51
|
+
|
|
52
|
+
# Option 2: Environment variable
|
|
53
|
+
export EXA_API_KEY=YOUR_API_KEY
|
|
54
|
+
|
|
55
|
+
# Option 3: .env file in current directory or ~/.env
|
|
56
|
+
echo "EXA_API_KEY=YOUR_API_KEY" >> ~/.env
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Get your API key from: https://dashboard.exa.ai/api-keys
|
|
60
|
+
|
|
61
|
+
## Tools
|
|
62
|
+
|
|
63
|
+
### search (default)
|
|
64
|
+
|
|
65
|
+
| Tool | Description |
|
|
66
|
+
|------|-------------|
|
|
67
|
+
| `web_search_general` | Real-time web searches with content extraction |
|
|
68
|
+
| `web_search_deep` | Natural language web search with synthesized results |
|
|
69
|
+
| `web_search_code_context` | Search code snippets, docs, and examples |
|
|
70
|
+
| `web_search_crawl_url` | Extract content from specific URLs |
|
|
71
|
+
|
|
72
|
+
### linkedin
|
|
73
|
+
|
|
74
|
+
| Tool | Description |
|
|
75
|
+
|------|-------------|
|
|
76
|
+
| `web_search_linkedin` | Search LinkedIn profiles and companies |
|
|
77
|
+
|
|
78
|
+
### company
|
|
79
|
+
|
|
80
|
+
| Tool | Description |
|
|
81
|
+
|------|-------------|
|
|
82
|
+
| `web_search_company_research` | Comprehensive company research |
|
|
83
|
+
|
|
84
|
+
### researcher
|
|
85
|
+
|
|
86
|
+
| Tool | Description |
|
|
87
|
+
|------|-------------|
|
|
88
|
+
| `web_search_researcher_start` | Start comprehensive AI-powered research task |
|
|
89
|
+
| `web_search_researcher_check` | Check research task status and get results |
|
|
90
|
+
|
|
91
|
+
### websets
|
|
92
|
+
|
|
93
|
+
| Tool | Description |
|
|
94
|
+
|------|-------------|
|
|
95
|
+
| `webset_create` | Create entity collections with search and enrichments |
|
|
96
|
+
| `webset_list` | List all websets in your account |
|
|
97
|
+
| `webset_get` | Get detailed webset information |
|
|
98
|
+
| `webset_update` | Update webset metadata |
|
|
99
|
+
| `webset_delete` | Delete a webset |
|
|
100
|
+
| `webset_items_list` | List items in a webset |
|
|
101
|
+
| `webset_item_get` | Get item details |
|
|
102
|
+
| `webset_search_create` | Add search to find entities for a webset |
|
|
103
|
+
| `webset_search_get` | Check search status |
|
|
104
|
+
| `webset_search_cancel` | Cancel running search |
|
|
105
|
+
| `webset_enrichment_create` | Extract custom data from webset items |
|
|
106
|
+
| `webset_enrichment_get` | Get enrichment details |
|
|
107
|
+
| `webset_enrichment_update` | Update enrichment metadata |
|
|
108
|
+
| `webset_enrichment_delete` | Delete enrichment |
|
|
109
|
+
| `webset_enrichment_cancel` | Cancel running enrichment |
|
|
110
|
+
| `webset_monitor_create` | Auto-update webset on schedule |
|
|
111
|
+
|
|
112
|
+
## Usage Examples
|
|
113
|
+
|
|
114
|
+
### Code Search
|
|
115
|
+
```
|
|
116
|
+
Find examples of how to use React hooks with TypeScript
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Web Search
|
|
120
|
+
```
|
|
121
|
+
Search for the latest news about AI regulation in the EU
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Company Research (requires company feature)
|
|
125
|
+
```
|
|
126
|
+
Research the company OpenAI and find information about their products
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Deep Research (requires researcher feature)
|
|
130
|
+
```
|
|
131
|
+
Start a deep research project on the impact of large language models on software development
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Websets (requires websets feature)
|
|
135
|
+
```
|
|
136
|
+
Create a webset of AI startups in San Francisco founded after 2020,
|
|
137
|
+
find 10 companies and enrich with CEO name and funding amount
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## How It Works
|
|
141
|
+
|
|
142
|
+
The plugin connects to Exa's hosted MCP (Model Context Protocol) servers:
|
|
143
|
+
- `https://mcp.exa.ai/mcp` - Search tools
|
|
144
|
+
- `https://websetsmcp.exa.ai/mcp` - Websets tools
|
|
145
|
+
|
|
146
|
+
Tools are dynamically fetched from these servers, so you always get the latest available tools.
|
|
147
|
+
|
|
148
|
+
## Resources
|
|
149
|
+
|
|
150
|
+
- [Exa Dashboard](https://dashboard.exa.ai/)
|
|
151
|
+
- [Exa MCP Documentation](https://docs.exa.ai/reference/exa-mcp)
|
|
152
|
+
- [Websets MCP Documentation](https://docs.exa.ai/reference/websets-mcp)
|
|
153
|
+
- [Exa API Documentation](https://docs.exa.ai/)
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@oh-my-pi/exa",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Exa AI web search and websets tools for pi",
|
|
5
|
+
"keywords": ["omp-plugin", "exa", "web-search", "websets", "ai-search"],
|
|
6
|
+
"author": "Can Bölük <me@can.ac>",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/can1357/oh-my-pi.git",
|
|
11
|
+
"directory": "plugins/exa"
|
|
12
|
+
},
|
|
13
|
+
"omp": {
|
|
14
|
+
"install": [
|
|
15
|
+
{ "src": "tools/exa/runtime.json", "dest": "agent/tools/exa/runtime.json", "copy": true },
|
|
16
|
+
{ "src": "tools/exa/index.ts", "dest": "agent/tools/exa/index.ts" },
|
|
17
|
+
{ "src": "tools/exa/shared.ts", "dest": "agent/tools/exa/shared.ts" },
|
|
18
|
+
{ "src": "tools/exa/search.ts", "dest": "agent/tools/exa/search.ts" },
|
|
19
|
+
{ "src": "tools/exa/linkedin.ts", "dest": "agent/tools/exa/linkedin.ts" },
|
|
20
|
+
{ "src": "tools/exa/company.ts", "dest": "agent/tools/exa/company.ts" },
|
|
21
|
+
{ "src": "tools/exa/researcher.ts", "dest": "agent/tools/exa/researcher.ts" },
|
|
22
|
+
{ "src": "tools/exa/websets.ts", "dest": "agent/tools/exa/websets.ts" }
|
|
23
|
+
],
|
|
24
|
+
"variables": {
|
|
25
|
+
"apiKey": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"env": "EXA_API_KEY",
|
|
28
|
+
"description": "Exa API key for authentication",
|
|
29
|
+
"required": true
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"features": {
|
|
33
|
+
"search": {
|
|
34
|
+
"description": "Core web search (general, deep, code context, URL crawling)",
|
|
35
|
+
"default": true
|
|
36
|
+
},
|
|
37
|
+
"linkedin": {
|
|
38
|
+
"description": "LinkedIn profile and company search",
|
|
39
|
+
"default": false
|
|
40
|
+
},
|
|
41
|
+
"company": {
|
|
42
|
+
"description": "Comprehensive company research",
|
|
43
|
+
"default": false
|
|
44
|
+
},
|
|
45
|
+
"researcher": {
|
|
46
|
+
"description": "Long-running AI research tasks",
|
|
47
|
+
"default": false
|
|
48
|
+
},
|
|
49
|
+
"websets": {
|
|
50
|
+
"description": "Entity collection management (14 tools)",
|
|
51
|
+
"default": false
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"files": ["tools"]
|
|
56
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exa Company Research Tool
|
|
3
|
+
*
|
|
4
|
+
* Tools:
|
|
5
|
+
* - web_search_company_research: Comprehensive company research
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { TSchema } from "@sinclair/typebox";
|
|
9
|
+
import type { CustomAgentTool, CustomToolFactory, ToolAPI } from "@mariozechner/pi-coding-agent";
|
|
10
|
+
import { callExaTool, createToolWrapper, fetchExaTools, findApiKey } from "./shared";
|
|
11
|
+
|
|
12
|
+
// MCP tool names for this feature
|
|
13
|
+
const TOOL_NAMES = ["company_research_exa"];
|
|
14
|
+
|
|
15
|
+
// Tool name mapping: MCP name -> exposed name
|
|
16
|
+
const NAME_MAP: Record<string, string> = {
|
|
17
|
+
"company_research_exa": "web_search_company_research",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const factory: CustomToolFactory = async (_toolApi: ToolAPI): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
|
|
21
|
+
const apiKey = findApiKey();
|
|
22
|
+
if (!apiKey) return null;
|
|
23
|
+
|
|
24
|
+
const mcpTools = await fetchExaTools(apiKey, TOOL_NAMES);
|
|
25
|
+
if (mcpTools.length === 0) return null;
|
|
26
|
+
|
|
27
|
+
const callFn = (toolName: string, args: Record<string, unknown>) =>
|
|
28
|
+
callExaTool(apiKey, TOOL_NAMES, toolName, args);
|
|
29
|
+
|
|
30
|
+
return mcpTools.map((tool) =>
|
|
31
|
+
createToolWrapper(tool, NAME_MAP[tool.name] ?? tool.name, callFn)
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default factory;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exa Tools - Dynamic loader for feature modules
|
|
3
|
+
*
|
|
4
|
+
* Reads runtime.json to determine which features are enabled,
|
|
5
|
+
* then loads and initializes those feature modules.
|
|
6
|
+
*
|
|
7
|
+
* Available features:
|
|
8
|
+
* - search: Core web search (general, deep, code context, URL crawling)
|
|
9
|
+
* - linkedin: LinkedIn profile and company search
|
|
10
|
+
* - company: Comprehensive company research
|
|
11
|
+
* - researcher: Long-running AI research tasks
|
|
12
|
+
* - websets: Entity collection management
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { TSchema } from "@sinclair/typebox";
|
|
16
|
+
import type { CustomAgentTool, CustomToolFactory, ToolAPI } from "@mariozechner/pi-coding-agent";
|
|
17
|
+
import runtime from "./runtime.json";
|
|
18
|
+
|
|
19
|
+
// Map feature names to their module imports
|
|
20
|
+
const FEATURE_LOADERS: Record<string, () => Promise<{ default: CustomToolFactory }>> = {
|
|
21
|
+
search: () => import("./search"),
|
|
22
|
+
linkedin: () => import("./linkedin"),
|
|
23
|
+
company: () => import("./company"),
|
|
24
|
+
researcher: () => import("./researcher"),
|
|
25
|
+
websets: () => import("./websets"),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Factory function that loads enabled features from runtime.json
|
|
30
|
+
*/
|
|
31
|
+
const factory: CustomToolFactory = async (toolApi: ToolAPI): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
|
|
32
|
+
const allTools: CustomAgentTool<TSchema, unknown>[] = [];
|
|
33
|
+
const enabledFeatures = runtime.features ?? [];
|
|
34
|
+
|
|
35
|
+
for (const feature of enabledFeatures) {
|
|
36
|
+
const loader = FEATURE_LOADERS[feature];
|
|
37
|
+
if (!loader) {
|
|
38
|
+
console.error(`Unknown exa feature: "${feature}"`);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const module = await loader();
|
|
44
|
+
const featureFactory = module.default;
|
|
45
|
+
|
|
46
|
+
if (typeof featureFactory === "function") {
|
|
47
|
+
const result = await featureFactory(toolApi);
|
|
48
|
+
// Handle both single tool and array of tools
|
|
49
|
+
if (result) {
|
|
50
|
+
const tools = Array.isArray(result) ? result : [result];
|
|
51
|
+
for (const tool of tools) {
|
|
52
|
+
if (tool && typeof tool === "object" && "name" in tool) {
|
|
53
|
+
allTools.push(tool);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error(`Failed to load exa feature "${feature}":`, error);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return allTools.length > 0 ? allTools : null;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export default factory;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exa LinkedIn Search Tool
|
|
3
|
+
*
|
|
4
|
+
* Tools:
|
|
5
|
+
* - web_search_linkedin: Search LinkedIn profiles and companies
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { TSchema } from "@sinclair/typebox";
|
|
9
|
+
import type { CustomAgentTool, CustomToolFactory, ToolAPI } from "@mariozechner/pi-coding-agent";
|
|
10
|
+
import { callExaTool, createToolWrapper, fetchExaTools, findApiKey } from "./shared";
|
|
11
|
+
|
|
12
|
+
// MCP tool names for this feature
|
|
13
|
+
const TOOL_NAMES = ["linkedin_search_exa"];
|
|
14
|
+
|
|
15
|
+
// Tool name mapping: MCP name -> exposed name
|
|
16
|
+
const NAME_MAP: Record<string, string> = {
|
|
17
|
+
"linkedin_search_exa": "web_search_linkedin",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const factory: CustomToolFactory = async (_toolApi: ToolAPI): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
|
|
21
|
+
const apiKey = findApiKey();
|
|
22
|
+
if (!apiKey) return null;
|
|
23
|
+
|
|
24
|
+
const mcpTools = await fetchExaTools(apiKey, TOOL_NAMES);
|
|
25
|
+
if (mcpTools.length === 0) return null;
|
|
26
|
+
|
|
27
|
+
const callFn = (toolName: string, args: Record<string, unknown>) =>
|
|
28
|
+
callExaTool(apiKey, TOOL_NAMES, toolName, args);
|
|
29
|
+
|
|
30
|
+
return mcpTools.map((tool) =>
|
|
31
|
+
createToolWrapper(tool, NAME_MAP[tool.name] ?? tool.name, callFn)
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default factory;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exa Deep Researcher Tools
|
|
3
|
+
*
|
|
4
|
+
* Tools:
|
|
5
|
+
* - web_search_researcher_start: Start comprehensive AI research tasks
|
|
6
|
+
* - web_search_researcher_check: Check research task status
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { TSchema } from "@sinclair/typebox";
|
|
10
|
+
import type { CustomAgentTool, CustomToolFactory, ToolAPI } from "@mariozechner/pi-coding-agent";
|
|
11
|
+
import { callExaTool, createToolWrapper, fetchExaTools, findApiKey } from "./shared";
|
|
12
|
+
|
|
13
|
+
// MCP tool names for this feature
|
|
14
|
+
const TOOL_NAMES = [
|
|
15
|
+
"deep_researcher_start",
|
|
16
|
+
"deep_researcher_check",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
// Tool name mapping: MCP name -> exposed name
|
|
20
|
+
const NAME_MAP: Record<string, string> = {
|
|
21
|
+
"deep_researcher_start": "web_search_researcher_start",
|
|
22
|
+
"deep_researcher_check": "web_search_researcher_check",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const factory: CustomToolFactory = async (_toolApi: ToolAPI): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
|
|
26
|
+
const apiKey = findApiKey();
|
|
27
|
+
if (!apiKey) return null;
|
|
28
|
+
|
|
29
|
+
const mcpTools = await fetchExaTools(apiKey, TOOL_NAMES);
|
|
30
|
+
if (mcpTools.length === 0) return null;
|
|
31
|
+
|
|
32
|
+
const callFn = (toolName: string, args: Record<string, unknown>) =>
|
|
33
|
+
callExaTool(apiKey, TOOL_NAMES, toolName, args);
|
|
34
|
+
|
|
35
|
+
return mcpTools.map((tool) =>
|
|
36
|
+
createToolWrapper(tool, NAME_MAP[tool.name] ?? tool.name, callFn)
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export default factory;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exa Search Tools - Core web search capabilities
|
|
3
|
+
*
|
|
4
|
+
* Tools:
|
|
5
|
+
* - web_search_general: Real-time web searches
|
|
6
|
+
* - web_search_deep: Natural language web search with synthesis
|
|
7
|
+
* - web_search_code_context: Code search for libraries, docs, examples
|
|
8
|
+
* - web_search_crawl_url: Extract content from specific URLs
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { TSchema } from "@sinclair/typebox";
|
|
12
|
+
import type { CustomAgentTool, CustomToolFactory, ToolAPI } from "@mariozechner/pi-coding-agent";
|
|
13
|
+
import { callExaTool, createToolWrapper, fetchExaTools, findApiKey } from "./shared";
|
|
14
|
+
|
|
15
|
+
// MCP tool names for this feature
|
|
16
|
+
const TOOL_NAMES = [
|
|
17
|
+
"web_search_exa",
|
|
18
|
+
"deep_search_exa",
|
|
19
|
+
"get_code_context_exa",
|
|
20
|
+
"crawling_exa",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
// Tool name mapping: MCP name -> exposed name
|
|
24
|
+
const NAME_MAP: Record<string, string> = {
|
|
25
|
+
"web_search_exa": "web_search_general",
|
|
26
|
+
"deep_search_exa": "web_search_deep",
|
|
27
|
+
"get_code_context_exa": "web_search_code_context",
|
|
28
|
+
"crawling_exa": "web_search_crawl_url",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const factory: CustomToolFactory = async (_toolApi: ToolAPI): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
|
|
32
|
+
const apiKey = findApiKey();
|
|
33
|
+
if (!apiKey) return null;
|
|
34
|
+
|
|
35
|
+
const mcpTools = await fetchExaTools(apiKey, TOOL_NAMES);
|
|
36
|
+
if (mcpTools.length === 0) return null;
|
|
37
|
+
|
|
38
|
+
const callFn = (toolName: string, args: Record<string, unknown>) =>
|
|
39
|
+
callExaTool(apiKey, TOOL_NAMES, toolName, args);
|
|
40
|
+
|
|
41
|
+
return mcpTools.map((tool) =>
|
|
42
|
+
createToolWrapper(tool, NAME_MAP[tool.name] ?? tool.name, callFn)
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default factory;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for Exa MCP tools
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as os from "node:os";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import type { TSchema } from "@sinclair/typebox";
|
|
9
|
+
import type { CustomAgentTool } from "@mariozechner/pi-coding-agent";
|
|
10
|
+
|
|
11
|
+
// MCP endpoints
|
|
12
|
+
export const EXA_MCP_URL = "https://mcp.exa.ai/mcp";
|
|
13
|
+
export const WEBSETS_MCP_URL = "https://websetsmcp.exa.ai/mcp";
|
|
14
|
+
|
|
15
|
+
export interface MCPTool {
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
inputSchema: TSchema;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface MCPToolsResponse {
|
|
22
|
+
result?: {
|
|
23
|
+
tools: MCPTool[];
|
|
24
|
+
};
|
|
25
|
+
error?: {
|
|
26
|
+
code: number;
|
|
27
|
+
message: string;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parse a .env file and return key-value pairs
|
|
33
|
+
*/
|
|
34
|
+
function parseEnvFile(filePath: string): Record<string, string> {
|
|
35
|
+
const result: Record<string, string> = {};
|
|
36
|
+
if (!fs.existsSync(filePath)) return result;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
40
|
+
for (const line of content.split("\n")) {
|
|
41
|
+
const trimmed = line.trim();
|
|
42
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
43
|
+
|
|
44
|
+
const eqIndex = trimmed.indexOf("=");
|
|
45
|
+
if (eqIndex === -1) continue;
|
|
46
|
+
|
|
47
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
48
|
+
let value = trimmed.slice(eqIndex + 1).trim();
|
|
49
|
+
|
|
50
|
+
// Remove surrounding quotes
|
|
51
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
52
|
+
value = value.slice(1, -1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
result[key] = value;
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// Ignore read errors
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Find EXA_API_KEY from environment or .env files
|
|
66
|
+
*/
|
|
67
|
+
export function findApiKey(): string | null {
|
|
68
|
+
// 1. Check environment variable
|
|
69
|
+
if (process.env.EXA_API_KEY) {
|
|
70
|
+
return process.env.EXA_API_KEY;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 2. Check .env in current directory
|
|
74
|
+
const localEnv = parseEnvFile(path.join(process.cwd(), ".env"));
|
|
75
|
+
if (localEnv.EXA_API_KEY) {
|
|
76
|
+
return localEnv.EXA_API_KEY;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 3. Check ~/.env
|
|
80
|
+
const homeEnv = parseEnvFile(path.join(os.homedir(), ".env"));
|
|
81
|
+
if (homeEnv.EXA_API_KEY) {
|
|
82
|
+
return homeEnv.EXA_API_KEY;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Call an MCP server endpoint
|
|
90
|
+
*/
|
|
91
|
+
async function callMCP(url: string, method: string, params?: Record<string, unknown>): Promise<unknown> {
|
|
92
|
+
const body = {
|
|
93
|
+
jsonrpc: "2.0",
|
|
94
|
+
method,
|
|
95
|
+
params: params ?? {},
|
|
96
|
+
id: 1,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const response = await fetch(url, {
|
|
100
|
+
method: "POST",
|
|
101
|
+
headers: {
|
|
102
|
+
"Content-Type": "application/json",
|
|
103
|
+
Accept: "application/json, text/event-stream",
|
|
104
|
+
},
|
|
105
|
+
body: JSON.stringify(body),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const text = await response.text();
|
|
109
|
+
|
|
110
|
+
// Parse SSE response format
|
|
111
|
+
let jsonData: string | null = null;
|
|
112
|
+
for (const line of text.split("\n")) {
|
|
113
|
+
if (line.startsWith("data: ")) {
|
|
114
|
+
jsonData = line.slice(6);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!jsonData) {
|
|
120
|
+
// Try parsing as plain JSON
|
|
121
|
+
try {
|
|
122
|
+
return JSON.parse(text);
|
|
123
|
+
} catch {
|
|
124
|
+
throw new Error(`Failed to parse MCP response: ${text.slice(0, 500)}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return JSON.parse(jsonData);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Fetch available tools from Exa MCP server
|
|
133
|
+
*/
|
|
134
|
+
export async function fetchExaTools(apiKey: string, toolNames: string[]): Promise<MCPTool[]> {
|
|
135
|
+
const url = `${EXA_MCP_URL}?exaApiKey=${apiKey}&tools=${toolNames.join(",")}`;
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const response = (await callMCP(url, "tools/list")) as MCPToolsResponse;
|
|
139
|
+
if (response.error) {
|
|
140
|
+
throw new Error(response.error.message);
|
|
141
|
+
}
|
|
142
|
+
return response.result?.tools ?? [];
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error(`Failed to fetch Exa tools:`, error);
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Fetch available tools from Websets MCP server
|
|
151
|
+
*/
|
|
152
|
+
export async function fetchWebsetsTools(apiKey: string): Promise<MCPTool[]> {
|
|
153
|
+
const url = `${WEBSETS_MCP_URL}?exaApiKey=${apiKey}`;
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const response = (await callMCP(url, "tools/list")) as MCPToolsResponse;
|
|
157
|
+
if (response.error) {
|
|
158
|
+
throw new Error(response.error.message);
|
|
159
|
+
}
|
|
160
|
+
return response.result?.tools ?? [];
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error(`Failed to fetch Websets tools:`, error);
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Call a tool on Exa MCP server
|
|
169
|
+
*/
|
|
170
|
+
export async function callExaTool(apiKey: string, toolNames: string[], toolName: string, args: Record<string, unknown>): Promise<unknown> {
|
|
171
|
+
const url = `${EXA_MCP_URL}?exaApiKey=${apiKey}&tools=${toolNames.join(",")}`;
|
|
172
|
+
return callMCPTool(url, toolName, args);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Call a tool on Websets MCP server
|
|
177
|
+
*/
|
|
178
|
+
export async function callWebsetsTool(apiKey: string, toolName: string, args: Record<string, unknown>): Promise<unknown> {
|
|
179
|
+
const url = `${WEBSETS_MCP_URL}?exaApiKey=${apiKey}`;
|
|
180
|
+
return callMCPTool(url, toolName, args);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Call a tool on an MCP server
|
|
185
|
+
*/
|
|
186
|
+
async function callMCPTool(url: string, toolName: string, args: Record<string, unknown>): Promise<unknown> {
|
|
187
|
+
const response = (await callMCP(url, "tools/call", {
|
|
188
|
+
name: toolName,
|
|
189
|
+
arguments: args,
|
|
190
|
+
})) as { result?: { content?: Array<{ text?: string }> }; error?: { message: string } };
|
|
191
|
+
|
|
192
|
+
if (response.error) {
|
|
193
|
+
throw new Error(response.error.message);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Extract text content from MCP response
|
|
197
|
+
const content = response.result?.content;
|
|
198
|
+
if (Array.isArray(content)) {
|
|
199
|
+
const texts = content.filter((c) => c.text).map((c) => c.text);
|
|
200
|
+
if (texts.length === 1) {
|
|
201
|
+
// Try to parse as JSON
|
|
202
|
+
try {
|
|
203
|
+
return JSON.parse(texts[0]!);
|
|
204
|
+
} catch {
|
|
205
|
+
return texts[0];
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return texts.join("\n\n");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return response.result;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Create a tool wrapper for an MCP tool
|
|
216
|
+
*/
|
|
217
|
+
export function createToolWrapper(
|
|
218
|
+
mcpTool: MCPTool,
|
|
219
|
+
renamedName: string,
|
|
220
|
+
callFn: (toolName: string, args: Record<string, unknown>) => Promise<unknown>
|
|
221
|
+
): CustomAgentTool<TSchema, unknown> {
|
|
222
|
+
return {
|
|
223
|
+
name: renamedName,
|
|
224
|
+
description: mcpTool.description,
|
|
225
|
+
parameters: mcpTool.inputSchema,
|
|
226
|
+
async execute(args) {
|
|
227
|
+
return callFn(mcpTool.name, args as Record<string, unknown>);
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exa Websets Tools - Entity collection management
|
|
3
|
+
*
|
|
4
|
+
* Tools:
|
|
5
|
+
* - webset_create: Create entity collections with search/enrichments
|
|
6
|
+
* - webset_list: List all websets
|
|
7
|
+
* - webset_get: Get webset details
|
|
8
|
+
* - webset_update: Update webset metadata
|
|
9
|
+
* - webset_delete: Delete a webset
|
|
10
|
+
* - webset_items_list: List items in a webset
|
|
11
|
+
* - webset_item_get: Get item details
|
|
12
|
+
* - webset_search_create: Add search to webset
|
|
13
|
+
* - webset_search_get: Check search status
|
|
14
|
+
* - webset_search_cancel: Cancel running search
|
|
15
|
+
* - webset_enrichment_create: Extract custom data from items
|
|
16
|
+
* - webset_enrichment_get: Get enrichment details
|
|
17
|
+
* - webset_enrichment_update: Update enrichment metadata
|
|
18
|
+
* - webset_enrichment_delete: Delete enrichment
|
|
19
|
+
* - webset_enrichment_cancel: Cancel running enrichment
|
|
20
|
+
* - webset_monitor_create: Auto-update webset on schedule
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { TSchema } from "@sinclair/typebox";
|
|
24
|
+
import type { CustomAgentTool, CustomToolFactory, ToolAPI } from "@mariozechner/pi-coding-agent";
|
|
25
|
+
import { callWebsetsTool, createToolWrapper, fetchWebsetsTools, findApiKey } from "./shared";
|
|
26
|
+
|
|
27
|
+
// Tool name mapping: MCP name -> exposed name
|
|
28
|
+
const NAME_MAP: Record<string, string> = {
|
|
29
|
+
"create_webset": "webset_create",
|
|
30
|
+
"list_websets": "webset_list",
|
|
31
|
+
"get_webset": "webset_get",
|
|
32
|
+
"update_webset": "webset_update",
|
|
33
|
+
"delete_webset": "webset_delete",
|
|
34
|
+
"list_webset_items": "webset_items_list",
|
|
35
|
+
"get_item": "webset_item_get",
|
|
36
|
+
"create_search": "webset_search_create",
|
|
37
|
+
"get_search": "webset_search_get",
|
|
38
|
+
"cancel_search": "webset_search_cancel",
|
|
39
|
+
"create_enrichment": "webset_enrichment_create",
|
|
40
|
+
"get_enrichment": "webset_enrichment_get",
|
|
41
|
+
"update_enrichment": "webset_enrichment_update",
|
|
42
|
+
"delete_enrichment": "webset_enrichment_delete",
|
|
43
|
+
"cancel_enrichment": "webset_enrichment_cancel",
|
|
44
|
+
"create_monitor": "webset_monitor_create",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const factory: CustomToolFactory = async (_toolApi: ToolAPI): Promise<CustomAgentTool<TSchema, unknown>[] | null> => {
|
|
48
|
+
const apiKey = findApiKey();
|
|
49
|
+
if (!apiKey) return null;
|
|
50
|
+
|
|
51
|
+
const mcpTools = await fetchWebsetsTools(apiKey);
|
|
52
|
+
if (mcpTools.length === 0) return null;
|
|
53
|
+
|
|
54
|
+
const callFn = (toolName: string, args: Record<string, unknown>) =>
|
|
55
|
+
callWebsetsTool(apiKey, toolName, args);
|
|
56
|
+
|
|
57
|
+
return mcpTools.map((tool) =>
|
|
58
|
+
createToolWrapper(tool, NAME_MAP[tool.name] ?? tool.name, callFn)
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export default factory;
|