fix(mcp): keep Exa MCP servers that request non-native tools

Exa MCP servers were always filtered out because the native Exa integration covers web_search_exa. But configs that explicitly request web_fetch_exa or web_search_advanced_exa have no native equivalent, so filtering them made /mcp reconnect exa fail and hid those tools. Keep the MCP server mounted when its tools restriction includes anything beyond web_search_exa, while still extracting the API key for native search.
This commit is contained in:
dhruv.kejriwal
2026-08-15 00:39:58 +05:30
parent ad318c7572
commit f36cf20d56
4 changed files with 119 additions and 7 deletions
+1 -1
View File
@@ -42,7 +42,7 @@ Filtering behavior:
- `enableProjectConfig: false` removes project-level entries (`_source.level === "project"`).
- `enabled: false` entries are suppressed unless the active-profile user `enabledServers` allowlist names them; the user `disabledServers` denylist always suppresses a same-named entry.
- Exa servers are filtered out by default and API keys are extracted for native Exa tool integration; browser automation MCP servers are filtered when `filterBrowser` is true.
- Exa servers are filtered out by default and API keys are extracted for native Exa tool integration, unless the config explicitly requests Exa tools the native integration does not provide (`web_fetch_exa`, `web_search_advanced_exa`); browser automation MCP servers are filtered when `filterBrowser` is true.
Result includes both `configs` and `sources` (metadata used later for provider labeling).
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Kept Exa MCP servers mounted when their config explicitly requests tools the native Exa integration does not provide (`web_fetch_exa`, `web_search_advanced_exa`), instead of filtering them out so `/mcp reconnect exa` could never connect.
## [17.3.3] - 2026-08-14
### Fixed
+48 -6
View File
@@ -224,6 +224,41 @@ export function extractExaApiKey(config: MCPServerConfig): string | undefined {
return undefined;
}
/** Exa MCP tools already covered by the native Exa integration. */
const NATIVE_EXA_MCP_TOOLS: Record<string, true> = { web_search_exa: true };
/**
* Parse the comma-separated `tools` restriction from an Exa MCP config.
* Returns `null` when the config does not restrict its tool set.
*/
function getRequestedExaMcpTools(config: MCPServerConfig): string[] | null {
const raw = (() => {
if (config.type === "http" || config.type === "sse") {
const httpConfig = config as { url?: string };
if (!httpConfig.url) return undefined;
try {
return new URL(httpConfig.url).searchParams.get("tools") ?? undefined;
} catch {
return undefined;
}
}
if (!config.type || config.type === "stdio") {
const stdioConfig = config as { args?: string[] };
for (const arg of stdioConfig.args ?? []) {
const match = arg.match(/(?:^|[\s?&])tools=([^&\s]+)/i) ?? arg.match(/--?tools[=\s]([^\s]+)/i);
if (match) return match[1];
}
}
return undefined;
})();
if (!raw) return null;
const tools = raw
.split(",")
.map(tool => tool.trim())
.filter(tool => tool.length > 0);
return tools.length > 0 ? tools : null;
}
/** Result of filtering Exa MCP servers */
export interface ExaFilterResult {
/** Configs with Exa servers removed */
@@ -236,7 +271,9 @@ export interface ExaFilterResult {
/**
* Filter out Exa MCP servers and extract their API keys.
* Since we have native Exa integration, we don't need the MCP server.
* Since we have native Exa integration, we don't need the MCP server —
* unless the config explicitly requests Exa tools the native integration
* does not provide (e.g. `web_fetch_exa`, `web_search_advanced_exa`).
*/
export function filterExaMCPServers(
configs: Record<string, MCPServerConfig>,
@@ -248,17 +285,22 @@ export function filterExaMCPServers(
for (const [name, config] of Object.entries(configs)) {
if (isExaMCPServer(name, config)) {
// Extract API key before filtering
// Extract API key for the native Exa integration even when the MCP
// server is kept below for its extra tools.
const apiKey = extractExaApiKey(config);
if (apiKey) {
exaApiKeys.push(apiKey);
}
} else {
filtered[name] = config;
if (sources[name]) {
filteredSources[name] = sources[name];
const requested = getRequestedExaMcpTools(config);
const hasExtraTools = requested?.some(tool => !NATIVE_EXA_MCP_TOOLS[tool.toLowerCase()]) ?? false;
if (!hasExtraTools) {
continue;
}
}
filtered[name] = config;
if (sources[name]) {
filteredSources[name] = sources[name];
}
}
return { configs: filtered, exaApiKeys, sources: filteredSources };
@@ -0,0 +1,66 @@
/**
* Regression: Exa MCP servers are filtered out by default because the native
* Exa integration covers `web_search_exa`. But a config that explicitly
* requests Exa tools the native integration does NOT provide (e.g.
* `web_fetch_exa`, `web_search_advanced_exa`) must stay mounted as an MCP
* server instead of being dropped.
*/
import { describe, expect, test } from "bun:test";
import type { SourceMeta } from "@oh-my-pi/pi-coding-agent/capability/types";
import { filterExaMCPServers } from "@oh-my-pi/pi-coding-agent/mcp/config";
import type { MCPServerConfig } from "@oh-my-pi/pi-coding-agent/mcp/types";
const SOURCE: SourceMeta = {
provider: "test",
providerName: "Test",
path: "/tmp/mcp.json",
level: "user",
};
describe("Exa MCP filtering", () => {
test("filters an exa server restricted to the native web_search_exa tool", () => {
const configs: Record<string, MCPServerConfig> = {
exa: { type: "http", url: "https://mcp.exa.ai/mcp?tools=web_search_exa&exaApiKey=sk-1" },
};
const result = filterExaMCPServers(configs, { exa: SOURCE });
expect(result.configs).toEqual({});
expect(result.exaApiKeys).toEqual(["sk-1"]);
});
test("keeps an exa server that requests tools beyond the native integration", () => {
const configs: Record<string, MCPServerConfig> = {
exa: {
type: "http",
url: "https://mcp.exa.ai/mcp?tools=web_search_exa,web_fetch_exa,web_search_advanced_exa&exaApiKey=sk-1",
},
};
const result = filterExaMCPServers(configs, { exa: SOURCE });
expect(Object.keys(result.configs)).toEqual(["exa"]);
expect(result.sources.exa).toEqual(SOURCE);
expect(result.exaApiKeys).toEqual(["sk-1"]);
});
test("filters an exa server with no tools restriction", () => {
const configs: Record<string, MCPServerConfig> = {
exa: { type: "http", url: "https://mcp.exa.ai/mcp" },
};
const result = filterExaMCPServers(configs, { exa: SOURCE });
expect(result.configs).toEqual({});
});
test("keeps a stdio exa server that requests extra tools", () => {
const configs: Record<string, MCPServerConfig> = {
exa: {
type: "stdio",
command: "npx",
args: ["-y", "exa-mcp-server", "--tools=web_search_exa,web_fetch_exa"],
},
};
const result = filterExaMCPServers(configs, { exa: SOURCE });
expect(Object.keys(result.configs)).toEqual(["exa"]);
});
});