Skip to content

feat(exa): add MCP-compatible proxy endpoint for Kilo-authenticated clients#2855

Open
kilo-code-bot[bot] wants to merge 3 commits intomainfrom
mark/exa-mcp-proxy
Open

feat(exa): add MCP-compatible proxy endpoint for Kilo-authenticated clients#2855
kilo-code-bot[bot] wants to merge 3 commits intomainfrom
mark/exa-mcp-proxy

Conversation

@kilo-code-bot
Copy link
Copy Markdown
Contributor

@kilo-code-bot kilo-code-bot Bot commented Apr 28, 2026

Summary

  • Adds POST /api/exa/mcp, an MCP (JSON-RPC 2.0 over SSE) endpoint that mirrors the interface of mcp.exa.ai/mcp but authenticates via Kilo's Authorization: Bearer <token> + X-KiloCode-OrganizationId auth, so the Kilo CLI can route Exa web/code search traffic through api.kilo.ai and get free Exa credits billed to the user's Kilo account.
  • Supports the two tools the CLI uses: web_search_exa (with query, type, numResults, livecrawl, contextMaxCharacters) and get_code_context_exa (with query, tokensNum). Both translate into the same Exa REST /search call that the existing /api/exa/[...path] proxy already uses.
  • Reuses the existing Exa billing plumbing (getExaMonthlyUsage, getExaFreeAllowanceMicrodollars, recordExaUsage, balance check against getBalanceAndOrgSettings) so this traffic counts toward the same monthly allowance / org balance as the REST proxy. Also implements the standard MCP bootstrap (initialize, tools/list, notifications/*) so MCP clients can handshake normally.

Once this merges, the CLI-side change in mcp-exa.ts is just swapping the URL constant from mcp.exa.ai/mcp to api.kilo.ai/api/exa/mcp when Kilo auth is present — no protocol translation on the client.

Verification

  • Manually traced a tools/call web_search_exa body against the Exa REST mapping — matches what /api/exa/[...path] already sends for /search.
  • Added unit tests covering: auth passthrough, initialize / tools/list / notification handling, unknown method, argument translation for both tools (including tokensNum * 4 and default-when-omitted for contextMaxCharacters / numResults / type / livecrawl), missing query, unknown tool, request signal forwarding, free-tier allowance vs balance check, org id propagation, cost recording with chargedToBalance flag, upstream-error wrapping, no cost recording on errors / missing costDollars, SSE content-type/content-encoding headers, 400 for invalid JSON.
  • Tests could not be run locally in this container (no Docker for the Postgres test DB required by jest setup) — relying on CI.

Visual Changes

N/A (backend-only).

Reviewer Notes

  • SSE envelope: uses event: message\ndata: <json>\n\n and returns 200 with content-type: text/event-stream, content-encoding: identity. Notifications (no/null id) get 202 with no body per the MCP streamable-HTTP spec.
  • Error handling: upstream Exa errors are wrapped as JSON-RPC errors inside a 200 SSE response (MCP convention) rather than returning upstream HTTP status codes, since the MCP client only parses result / error from the RPC payload. The existing REST proxy keeps passing through upstream HTTP status codes — that behavior is unchanged.
  • Billing: reuses recordExaUsage with path: '/search', so MCP traffic shows up in exa_usage_log / exa_monthly_usage alongside REST traffic under the same path. If you'd rather distinguish them (e.g. path: '/mcp/search'), easy to change.
  • No new PII, no schema changes.

…lients

Adds POST /api/exa/mcp that speaks JSON-RPC 2.0 over SSE (matching
mcp.exa.ai/mcp) but authenticates via Kilo auth and bills to the user's
Kilo Exa allowance/balance. Translates web_search_exa and
get_code_context_exa tool calls into the underlying Exa /search REST
API, reusing the existing allowance/balance/usage-recording plumbing.
Comment thread apps/web/src/app/api/exa/mcp/route.ts Fixed
Comment thread apps/web/src/app/api/exa/mcp/route.ts Outdated

let rpcRequest: JsonRpcRequest;
try {
rpcRequest = (await request.json()) as JsonRpcRequest;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: JSON null requests can crash the handler

request.json() successfully parses a body of null, but the cast does not validate the shape. The later destructuring of rpcRequest will throw a TypeError and return a 500 instead of an invalid-request response. Please guard that the parsed value is a non-null object before reading id, method, and params.

@kilo-code-bot
Copy link
Copy Markdown
Contributor Author

kilo-code-bot Bot commented Apr 28, 2026

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (1 files)
  • apps/web/src/app/api/exa/mcp/route.ts

Reviewed by gpt-5.5-2026-04-23 · 436,324 tokens

kilo-code-bot Bot added 2 commits April 28, 2026 12:34
request.json() accepts null, primitives, and arrays; destructuring those
was throwing a TypeError and returning 500 instead of a clean 400.
Guard with a typeof/Array.isArray check before reading id/method/params.
Caught by kilo-code-bot review.
…e out of console

CodeQL flagged the upstream-error console.error as log injection because
toolName is derived from the request body. Keep the console line to
numeric status only; report tool/userId through Sentry tags instead.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants