Skip to content

fix(tool): read the code search reply the request asked for - #785

Merged
Ishaan Gangwani (ishaan1124) merged 2 commits into
synthetic-sciences:mainfrom
aniruddhaadak80:fix/codesearch-json-response
Sep 29, 2026
Merged

Ishaan Gangwani (ishaan1124) merged 2 commits into
synthetic-sciences:mainfrom
aniruddhaadak80:fix/codesearch-json-response

Conversation

@aniruddhaadak80

Copy link
Copy Markdown
Contributor

What does this PR do, and why?

codesearch asks for two response types:

accept: "application/json, text/event-stream",

and then reads only one of them:

const lines = responseText.split("\n")
for (const line of lines) {
  if (line.startsWith("data: ")) {                       // ← SSE only
    const data: McpCodeResponse = JSON.parse(line.substring(6))
    if (data.result && data.result.content && data.result.content.length > 0) { … }
  }
}
return { output: "No code snippets or documentation found. …" }

Three shapes fall through that loop to the "nothing found" text. I ran today's
exact reader against each:

reply content type today's result
data: {…result…} text/event-stream the text
{…result…} application/json "No code snippets found"
{…error: invalid params} application/json "No code snippets found"
data:{…result…} (no space) text/event-stream "No code snippets found"

The second row is the important one: application/json is the first thing the
request asks for, and it is a legal Streamable HTTP reply, so a completely
successful search is reported as zero hits. The third is worse — a rejected call
(bad arguments, quota, auth) is a JSON-RPC object with no result at all, so the
model is told the library does not exist and rephrases and fans out instead of
seeing the failure. A missing space after data: is legal per the SSE grammar
and also missed.

The fix reads the declared content type and surfaces an error:

export function parseCodeReply(responseText: string, contentType: string | null) {
  const mime = (contentType ?? "").split(";")[0]!.trim().toLowerCase()
  const frames = mime === "application/json" || mime.endsWith("+json")
    ? [responseText]
    : responseText.split("\n").flatMap((line) =>
        line.startsWith("data:") ? [line.slice(5).replace(/^ /, "")] : [],
      )
  for (const frame of frames) {
    const body = frame.trim()
    if (!body || body === "[DONE]") continue
    const data = JSON.parse(body) as Partial<McpCodeResponse> & { error?: unknown }
    if (data.error) throw new Error(`Code search error: ${JSON.stringify(data.error)}`)
    const text = data.result?.content?.[0]?.text
    if (text) return text
  }
  return undefined
}

Malformed JSON still throws rather than being skipped, so a broken reply cannot
quietly become another "nothing found" — only the terminal sentinel is exempt.

Why the parser is exported

API_CONFIG.BASE_URL is a hardcoded https://mcp.exa.ai, so a test cannot point
the tool at a fake peer without adding configuration surface that has no other
purpose. The defect is the parsing, so I extracted and tested that. If you would
rather have an end-to-end test through a local peer, say so and I will add a base-URL
override instead — OPENSCIENCE_API_BASE in test/preload.ts shows there is
precedent for that shape.

Linked issue

Self-identified. Happy to file a report if you would rather track it; my issue
budget this turn went to the two apply_patch defects and the localStorage
copies.

How did you verify it?

backend/cli/test/tool/codesearch-reply.test.ts, five cases: an SSE frame, a
plain JSON body (with and without a charset parameter), an SSE frame with no
space, a rejected call in both encodings, and replies that genuinely carry no
content.

A note on how that test earned its keep: the no content case caught a bug in
my own first version of the fix — it threw SyntaxError: JSON Parse error: Unexpected identifier "DONE" on the SSE terminal sentinel, which I had not
excluded. That is the reason the sentinel check is in the code.

Commands run:

  • bun test --timeout 60000 ./test/tool/codesearch-reply.test.ts → 5 pass, 0 fail
  • bun run --cwd backend/cli typecheck → exit 0

Two adjacent things I did not change

Both are in the same function and both are real, but each is a separate concern
and I would rather not bundle them:

  • clearTimeout(timeoutId) runs before await response.text(), so the 30 s
    budget covers time-to-headers only and a slow body is unbounded.
  • Unlike webfetch, there is no response size cap on response.text().

Checklist

  • bun run check is green (format, typecheck, backend + frontend/ui + SDK tests) — blocked on this Windows checkout by the CRLF and symlink artifacts in my other pull requests; backend typecheck is clean and the new test is green
  • bun run --cwd frontend/workspace build succeeds if I touched frontend/workspace or frontend/ui — not touched
  • ./tooling/repo/generate.ts was run and the tooling/sdk output committed if I changed backend/cli/src/server — not touched
  • CHANGELOG.md has an Unreleased entry if the change is user-visible
  • The matching docs page under frontend/docs/src/content/openscience/ is updated if behavior changed — no doc change needed: the tool's contract is unchanged, only its parsing of the reply
  • Screenshots or a short video are attached for UI changes — not a UI change
  • No version bumps (package.json versions and tags are written by the release workflow)
  • install and frontend/landing/public/install are still byte-identical if I touched either — not touched

@vercel

vercel Bot commented Sep 28, 2026

Copy link
Copy Markdown

ANIRUDDHA ADAK (@aniruddhaadak80) is attempting to deploy a commit to the InkVell Team on Vercel.

A member of the Team first needs to authorize it.

@aniruddhaadak80
ANIRUDDHA ADAK (aniruddhaadak80) force-pushed the fix/codesearch-json-response branch 2 times, most recently from 266c18a to 46ad740 Compare September 29, 2026 14:04
… for

The tool sends `Accept: application/json, text/event-stream` and a
Streamable HTTP server is free to answer with the first type, so a perfectly
good search came back as "nothing found" because the reader only looked for
`data:` frames. Read the body directly when the response is JSON, keep the
SSE path for everything else, and take a `data:` frame with or without the
space after the colon.

A rejected call arrives as a JSON-RPC `error` object with no `result` at
all, which also fell through to the same "nothing found" text, so it is
raised as a tool error carrying the server's message. Only a reply that
really carried no content now reads as empty.
@ishaan1124
Ishaan Gangwani (ishaan1124) merged commit a13fd66 into synthetic-sciences:main Sep 29, 2026
8 of 9 checks passed
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