Skip to content

Commit 31c5167

Browse files
authored
Merge pull request #10 from tgonzalezc5/feat/exa-search-mcp
feat: add Exa AI-powered search tool
2 parents 6a914fc + 27bdc9b commit 31c5167

8 files changed

Lines changed: 91 additions & 8 deletions

File tree

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ It is built for scientists, analysts, and curious people who want a powerful AI
2323
- **Organise your work in projects.** Each project has its own files, chat history, and settings. Upload files, browse folders, preview documents, and download results - all from inside the app.
2424
- **Rich file previews.** Built-in viewers for code, Markdown (with math and diagrams), CSVs, PDFs, images, Jupyter notebooks, and bioinformatics formats (FASTA, FASTQ, VCF, BED, GFF, SAM, BCF).
2525
- **LaTeX editor.** Split-pane editor with live PDF compilation (pdfLaTeX, XeLaTeX, LuaLaTeX).
26-
- **Web search and document conversion.** Kady can search the web (via [Parallel](https://parallel.ai/)) and convert documents between formats (PDF, DOCX, HTML, etc.) with no extra setup.
26+
- **Web search and document conversion.** Kady can search the web (via [Exa](https://exa.ai/) or [Parallel](https://parallel.ai/)) and convert documents between formats (PDF, DOCX, HTML, etc.) with no extra setup.
2727
- **Voice input, drag-and-drop attachments, `@` file mentions,** and a **message queue** for batching up to 5 messages while the agent is working.
2828
- **Publication-ready provenance.** A timeline of every step in your session, plus a one-click "Copy as Methods" button that exports a paragraph ready to paste into a paper.
2929
- **Optional remote compute.** Plug in [Modal](https://modal.com/) to run heavy jobs on cloud GPUs (T4, L4, A10G, A100, H100) or serverless CPUs - selected right from the input bar.
@@ -35,7 +35,8 @@ It is built for scientists, analysts, and curious people who want a powerful AI
3535
|------|-----|-----------------|
3636
| A computer running **macOS or Linux** | The app runs locally on your machine | Windows works too - use [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) |
3737
| An **OpenRouter API key** | This is how the AI models are accessed | [openrouter.ai](https://openrouter.ai/) - sign up and create a key |
38-
| A **Parallel API key** *(optional)* | Lets Kady search the web | [parallel.ai](https://parallel.ai/) |
38+
| An **Exa API key** *(optional)* | Lets Kady search the web with neural (embedding-based) retrieval tuned for scientific content | Get your Exa API key: [dashboard.exa.ai/api-keys](https://dashboard.exa.ai/api-keys) |
39+
| A **Parallel API key** *(optional)* | Alternative web search provider | [parallel.ai](https://parallel.ai/) |
3940
| **Modal** credentials *(optional)* | Only needed for remote GPU/CPU compute | [modal.com](https://modal.com/) |
4041

4142
You do not need any coding experience. The startup script installs everything else for you.
@@ -55,7 +56,7 @@ cd k-dense-byok
5556

5657
Inside the `kady_agent` folder you'll find a file called `env.example`. Make a copy and rename the copy to `.env` (note the dot at the start). Open `.env` in any text editor and paste your **OpenRouter API key** on the first line - that's the only key you need to get started.
5758

58-
The file also has sections for other optional keys (Parallel for web search, Modal for remote compute, and many scientific and government database keys). Leave blank anything you don't need.
59+
The file also has sections for other optional keys (Exa or Parallel for web search, Modal for remote compute, and many scientific and government database keys). Leave blank anything you don't need.
5960

6061
### Step 3 - Start the app
6162

docs/custom-mcp-servers.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Custom MCP Servers
22

3-
K-Dense BYOK comes with built-in [MCP](https://modelcontextprotocol.io/) servers (Docling for document conversion and Parallel for web search). You can add your own MCP servers to give Kady's expert agents more tools - for example, connecting to internal databases, custom APIs, or specialised scientific tooling.
3+
K-Dense BYOK comes with built-in [MCP](https://modelcontextprotocol.io/) servers: Docling for document conversion, and two optional web-search providers — Exa and Parallel — each enabled by supplying the corresponding API key in `.env` (get an Exa key at [dashboard.exa.ai/api-keys](https://dashboard.exa.ai/api-keys)). You can add your own MCP servers to give Kady's expert agents more tools - for example, connecting to internal databases, custom APIs, or specialised scientific tooling.
44

55
## Adding a server through the UI
66

@@ -30,6 +30,6 @@ Two transport types are supported:
3030

3131
## How it's stored
3232

33-
- Your custom servers are **merged** with the built-in defaults (Docling and Parallel) and passed to the Gemini CLI.
33+
- Your custom servers are **merged** with the built-in defaults (Docling, plus Exa and/or Parallel when their API keys are set) and passed to the Gemini CLI.
3434
- The configuration is saved **per project** in `projects/<project-id>/custom_mcps.json` (outside the `sandbox/` folder) so it survives sandbox deletion and app restarts.
3535
- Switching projects automatically swaps the MCP set - each project has its own.

kady_agent/agent.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
or "openrouter/google/gemini-3.1-pro-preview"
3737
)
3838
EXTRA_HEADERS = {"X-Title": "Kady", "HTTP-Referer": "https://www.k-dense.ai"}
39+
EXA_API_KEY = os.getenv("EXA_API_KEY")
3940
PARALLEL_API_KEY = os.getenv("PARALLEL_API_KEY")
4041

4142
logger = logging.getLogger(__name__)

kady_agent/env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ OLLAMA_BASE_URL=http://localhost:11434
3030

3131
## Highly Recommended (Delete if not needed and filled in correctly)
3232

33+
# Exa Search (get your key at https://dashboard.exa.ai/api-keys)
34+
EXA_API_KEY=
35+
3336
# Parallel Search
3437
PARALLEL_API_KEY=
3538

kady_agent/instructions/main_agent.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Choose the lightest reliable path:
2020
You do **NOT** have the ability to activate or execute skills. Skills are capabilities that only the expert (Gemini CLI) inside `delegate_task` can use via its `activate_skill` tool. The skill reference table at the end of these instructions exists **solely** so you can:
2121

2222
1. **Recognize** when a user names a skill (e.g. "use the parallel-web skill", "use literature-review").
23-
2. **Match** a user request to the most relevant skill(s) even when the user does not name one explicitly (e.g. a request for "research best places in SF" should suggest the `parallel-web` skill; a request to "write a report" should suggest the `writing` skill).
23+
2. **Match** a user request to the most relevant skill(s) even when the user does not name one explicitly (e.g. a request for "research best places in SF" should suggest the `exa-search` or `parallel-web` skill; a request to "write a report" should suggest the `writing` skill).
2424
3. **Pass the skill name(s) verbatim** in the `delegate_task` prompt so the expert can activate them.
2525

2626
**Never** attempt to use, activate, or simulate a skill yourself. If a task needs a skill, delegate it.
@@ -31,15 +31,15 @@ You do **NOT** have the ability to activate or execute skills. Skills are capabi
3131
- In `prompt`, pass the user's request, the expert's role/objective/constraints, relevant context, file paths, URLs, and explicit success criteria.
3232
- Do not prescribe implementation approaches, libraries, or fallback methods unless the user explicitly requires them.
3333
- **Skills passthrough (MANDATORY):** If the user's message names specific skills (e.g. "use the parallel-web skill" or "use the skills: 'writing', 'literature-review'"), you MUST include an explicit instruction in the delegate prompt telling the expert to activate those skills. Use the format: `"You MUST activate and follow these skills: 'skill-name-1', 'skill-name-2'."` Do not paraphrase, omit, reorder, or summarize the skill list. The expert relies on exact names to activate the correct skills.
34-
- **Proactive skill matching:** Even when the user does not name a skill, consult the skill reference table and identify skills that match the task. Include them in the delegate prompt the same way: `"You should activate and follow these skills: 'skill-name'."` For example, if the user asks to "search the web for X", include the `parallel-web` skill; if they ask for a "literature review", include `literature-review` and `writing`.
34+
- **Proactive skill matching:** Even when the user does not name a skill, consult the skill reference table and identify skills that match the task. Include them in the delegate prompt the same way: `"You should activate and follow these skills: 'skill-name'."` For example, if the user asks to "search the web for X", include whichever web-search skill matches the enabled MCP (`exa-search` for Exa Search MCP, `parallel-web` for Parallel Search MCP); if they ask for a "literature review", include `literature-review` and `writing`.
3535
- **Modal compute passthrough (MANDATORY):** If the user's prompt requests specific compute infrastructure and mentions **Modal** (e.g. "run this on Modal", "use Modal GPUs", "deploy on Modal"), you MUST:
3636
1. Include the compute requirement explicitly in the `delegate_task` prompt.
3737
2. State that the expert **MUST activate and follow the `modal` skill** before writing or running any Modal-related code.
3838
3. Do not assume the expert will infer Modal usage on its own — spell it out: "You must activate and follow the skill: 'modal' to execute this code on a Modal instance."
3939

4040
## Tool preferences
4141

42-
- Prefer Parallel Search MCP (`web_search`, `web_fetch`) for open-web search and URL content retrieval.
42+
- Prefer Exa Search MCP or Parallel Search MCP for open-web search and URL content retrieval — whichever is available. Both expose MCP tools for search and content fetch; the user's `.env` determines which one (or both) are enabled.
4343
- Prefer Docling for document conversion, text extraction, and markdown export.
4444
- Users may install custom MCP tools (e.g. memory/knowledge-graph, filesystem, databases, specialized APIs) via the Settings panel. These tools appear alongside the built-in ones — use them directly whenever the request matches their capabilities instead of routing through `delegate_task`.
4545
- For reports, papers, literature reviews, or other structured prose, instruct the expert to use the `writing` skill.

kady_agent/manifest.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,19 @@ def _mcp_servers_snapshot() -> list[dict]:
188188
entries: list[dict] = []
189189
for name in sorted(merged):
190190
entries.append({"name": name, "spec": merged[name]})
191+
if os.getenv("EXA_API_KEY"):
192+
entries.append(
193+
{
194+
"name": "exa-search",
195+
"spec": {
196+
"httpUrl": "https://mcp.exa.ai/mcp",
197+
"headers": {
198+
"x-api-key": "YOUR_EXA_API_KEY",
199+
"x-exa-integration": "k-dense-byok",
200+
},
201+
},
202+
}
203+
)
191204
if os.getenv("PARALLEL_API_KEY"):
192205
entries.append(
193206
{

kady_agent/mcps.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,22 @@ async def close(self) -> None:
168168

169169
all_mcps: list[BaseToolset] = []
170170

171+
if os.getenv("EXA_API_KEY"):
172+
exa_search_mcp = ResilientMcpToolset(
173+
McpToolset(
174+
connection_params=StreamableHTTPConnectionParams(
175+
url="https://mcp.exa.ai/mcp",
176+
headers={
177+
"x-api-key": os.getenv("EXA_API_KEY"),
178+
"x-exa-integration": "k-dense-byok",
179+
},
180+
timeout=600,
181+
),
182+
),
183+
label="Exa Search MCP",
184+
)
185+
all_mcps.append(exa_search_mcp)
186+
171187
if os.getenv("PARALLEL_API_KEY"):
172188
parallel_search_mcp = ResilientMcpToolset(
173189
McpToolset(

tests/test_manifest.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,52 @@ def mutator(m: dict) -> None:
178178
# Persisted
179179
reread = manifest_module.read_manifest("s4", turn_id)
180180
assert reread["citations"]["total"] == 3
181+
182+
183+
# ---------------------------------------------------------------------------
184+
# _mcp_servers_snapshot — built-in MCP registration
185+
# ---------------------------------------------------------------------------
186+
187+
188+
def test_mcp_snapshot_includes_parallel_when_key_set(active_project, monkeypatch):
189+
monkeypatch.setenv("PARALLEL_API_KEY", "test-parallel")
190+
monkeypatch.delenv("EXA_API_KEY", raising=False)
191+
entries = manifest_module._mcp_servers_snapshot()
192+
names = [entry["name"] for entry in entries]
193+
assert "parallel-search" in names
194+
assert "exa-search" not in names
195+
parallel = next(e for e in entries if e["name"] == "parallel-search")
196+
# Keys are redacted before they hit disk.
197+
assert parallel["spec"]["headers"]["Authorization"] == "Bearer <redacted>"
198+
199+
200+
def test_mcp_snapshot_includes_exa_when_key_set(active_project, monkeypatch):
201+
monkeypatch.setenv("EXA_API_KEY", "test-exa")
202+
monkeypatch.delenv("PARALLEL_API_KEY", raising=False)
203+
entries = manifest_module._mcp_servers_snapshot()
204+
names = [entry["name"] for entry in entries]
205+
assert "exa-search" in names
206+
assert "parallel-search" not in names
207+
exa = next(e for e in entries if e["name"] == "exa-search")
208+
assert exa["spec"]["httpUrl"] == "https://mcp.exa.ai/mcp"
209+
# API key redacted to a clear placeholder; integration header preserved verbatim.
210+
assert exa["spec"]["headers"]["x-api-key"] == "YOUR_EXA_API_KEY"
211+
assert exa["spec"]["headers"]["x-exa-integration"] == "k-dense-byok"
212+
213+
214+
def test_mcp_snapshot_omits_both_when_keys_unset(active_project, monkeypatch):
215+
monkeypatch.delenv("PARALLEL_API_KEY", raising=False)
216+
monkeypatch.delenv("EXA_API_KEY", raising=False)
217+
entries = manifest_module._mcp_servers_snapshot()
218+
names = [entry["name"] for entry in entries]
219+
assert "parallel-search" not in names
220+
assert "exa-search" not in names
221+
222+
223+
def test_mcp_snapshot_includes_both_when_both_keys_set(active_project, monkeypatch):
224+
monkeypatch.setenv("PARALLEL_API_KEY", "test-parallel")
225+
monkeypatch.setenv("EXA_API_KEY", "test-exa")
226+
entries = manifest_module._mcp_servers_snapshot()
227+
names = [entry["name"] for entry in entries]
228+
assert "parallel-search" in names
229+
assert "exa-search" in names

0 commit comments

Comments
 (0)