Summary
The ask tool returns no citations, always. _stream_answer in src/ks_mcp/tools/ask.py handles a citations SSE event that the backend never emits, so the citations list stays empty and AskResult.citations == [] for every answer (asserted, unknowingly, by tests/test_schema.py).
Root cause
ask.py is SSE-only — it opens the thread stream, accumulates text_deltas, and expects a citations SSE event to carry citations:
# src/ks_mcp/tools/ask.py (~line 131)
elif event == "citations":
for raw in payload.get("citations") or []:
citations.append(_to_ask_citation(raw))
But on the backend:
- the
citations SSE event was never wired — ThreadStreamEvent.CITATIONS / CitationsPayload had zero producers and have now been removed (ks-backend PR #474);
- even had it fired, its payload was a bare
Citation (chunk_id, quote, offsets) — not the enriched shape _to_ask_citation expects (document_id, document_name, materialized_path).
The backend contract is: citations live on the persisted message, not on the wire. The frontend gets them by refetching the message after the stream (content.citations, enriched with document ancestry + permission-filtered). ks-mcp never refetches, so it gets nothing.
Fix
After the stream ends (message_end / [DONE]), do a REST GET of the message ks-mcp already tracks (it captures message_id from the message_start event) and populate citations from content.citations:
GET /v1/threads/{thread_id}/messages/{message_id} (or list) via ksapi
- read
content.citations — already enriched (document_id, document_name, materialized_path) and permission-filtered by the read API
- map into
AskCitation (the shape already matches the enriched content.citations)
with_details=false is enough: ks-backend PR #474 decoupled citation enrichment from with_details, so a light request now still returns enriched citations — one small request, no step-timeline payload.
Then remove the dead elif event == "citations" branch from _stream_answer.
Refs
- ks-backend PR #474 (removed the dead
citations SSE event; decoupled enrichment from with_details)
src/ks_mcp/tools/ask.py _stream_answer / _to_ask_citation
tests/test_schema.py (assert result.citations == [] — update once fixed)
Summary
The
asktool returns no citations, always._stream_answerinsrc/ks_mcp/tools/ask.pyhandles acitationsSSE event that the backend never emits, so thecitationslist stays empty andAskResult.citations == []for every answer (asserted, unknowingly, bytests/test_schema.py).Root cause
ask.pyis SSE-only — it opens the thread stream, accumulatestext_deltas, and expects acitationsSSE event to carry citations:But on the backend:
citationsSSE event was never wired —ThreadStreamEvent.CITATIONS/CitationsPayloadhad zero producers and have now been removed (ks-backend PR #474);Citation(chunk_id,quote, offsets) — not the enriched shape_to_ask_citationexpects (document_id,document_name,materialized_path).The backend contract is: citations live on the persisted message, not on the wire. The frontend gets them by refetching the message after the stream (
content.citations, enriched with document ancestry + permission-filtered). ks-mcp never refetches, so it gets nothing.Fix
After the stream ends (
message_end/[DONE]), do a REST GET of the message ks-mcp already tracks (it capturesmessage_idfrom themessage_startevent) and populate citations fromcontent.citations:GET /v1/threads/{thread_id}/messages/{message_id}(or list) viaksapicontent.citations— already enriched (document_id,document_name,materialized_path) and permission-filtered by the read APIAskCitation(the shape already matches the enrichedcontent.citations)with_details=falseis enough: ks-backend PR #474 decoupled citation enrichment fromwith_details, so a light request now still returns enriched citations — one small request, no step-timeline payload.Then remove the dead
elif event == "citations"branch from_stream_answer.Refs
citationsSSE event; decoupled enrichment fromwith_details)src/ks_mcp/tools/ask.py_stream_answer/_to_ask_citationtests/test_schema.py(assert result.citations == []— update once fixed)