Skip to content

MCP prompts over the bridge + chat(citations=True) - #494

Open
ahonn wants to merge 6 commits into
mainfrom
feat/mcp-bridge-prompts
Open

MCP prompts over the bridge + chat(citations=True)#494
ahonn wants to merge 6 commits into
mainfrom
feat/mcp-bridge-prompts

Conversation

@ahonn

@ahonn ahonn commented Sep 9, 2026

Copy link
Copy Markdown
Member

Two pieces, one stack: the MCP bridge learns to fetch server prompts, and chat(citations=True) is the first consumer, folded in from #496.

Bridge: server prompts over the MCP session

Adds prompts/list and prompts/get to McpBridge, so the SDK can fetch the server's cited_answer prompt (with its optional format argument) over the existing MCP session.

  • list_prompts() / get_prompt(name, arguments=None), plus render_prompt_text() for system-prompt placement.
  • Server capabilities are recorded at initialize; prompt calls fail with a clear error when prompts is not advertised.

Wiring into the *_agent_config bundles stays a follow-up.

chat(citations=True)

Makes the answer cite every claim the way PageIndex chat does: <cite doc="…" page="…"/> tags, block="…" added where the cloud document has blocks.

  • Own-model chat over cloud documents: fetches the server's cited_answer prompt (format=cite, the same rules PageIndex chat uses) over the same bridge session as the tools, and folds it into the system prompt after the managed prompt and before the caller's instructions. All three lanes (answer lane, protocol="responses", protocol="messages") via the existing instructions plumbing; local_chat.py only reworded one refusal.
  • Managed chat: citations=True passes enable_citations=True to the endpoint.
  • Local documents: a frozen copy of the same prompt (LOCAL_CITATION_PROMPTS, the live text minus the one bullet naming the cloud-only get_document_image() tool). Local page content carries no blocks, so citations resolve to pages.
  • citations takes only True/False. Another format goes through client.citation_prompt(format=...) passed as instructions=; the getter also serves framework-built agents. markdown / footnote are the prompt's variants for hosts that strip tags.

Verified

  • 503 tests green (3 new bridge tests, 9 new citation tests, red-verified first), without-frameworks leg simulated, pyright clean. Bridge smoke-tested against api.pageindex.ai/mcp.
  • Key-gated live tests: the three formats served and distinct; local frozen copies equal the live text minus that bullet.
  • Live with real models: block document → <cite doc="document_5.pdf" page="1" block="p1_text_1"/>; legacy document → <cite doc="1706.03762_24.pdf" page="1"/>; locally indexed PDF → <cite doc="q1-fy25-earnings.pdf" page="3"/>; managed citations=True non-stream and stream.

Not in this PR

  • Docs (agents.mdx, chat.mdx's hand-written <cite> system message becomes citations=) and a release-notes line, with the release.
  • Server-side, for the prompt's owner: the managed API's citation prompt carries a literal example id p1_text_001 that the model echoes on legacy documents (resolved citations then carry a block_id with no bbox); and a granularity argument on cited_answer would let the SDK offer page-only citations on block documents.

https://claude.ai/code/session_01XGBdRF2qoTcLNpRahj6QHx

Add prompts/list and prompts/get to McpBridge so the SDK can pull the
PageIndex MCP server's prompts (currently `cited_answer`, with its
optional `format` argument) over the same session as tools and
instructions.

- `list_prompts()` shares the cursor pagination `tools/list` already
  used, now factored into `_list_paginated`.
- `get_prompt(name, arguments=None)` returns `(description, messages)`
  untouched; argument values are stringified per the MCP prompt contract
  and None means no `arguments` field on the wire.
- `render_prompt_text(messages)` flattens prompt messages to plain text
  for placement in a system prompt.
- The initialize handshake now records server capabilities; prompt
  methods raise a clear PageIndexAPIError when the server does not
  advertise `prompts`, instead of surfacing a -32601 protocol error.

This is the transport layer only; wiring into `agent_instructions` and
the `*_agent_config` bundles follows in a separate change.
Comment thread tests/test_agent_tools.py Fixed
Drops the module-object import that CodeQL flagged as mixing 'import'
and 'import from' for pageindex.mcp_bridge.
…hat's format

chat(citations=True) makes the answer cite every claim the way PageIndex
chat does: <cite doc= page=/> tags, block= added where the cloud document
has blocks. Own-model chat over cloud documents fetches the MCP server's
cited_answer prompt (format=cite) over the tools' bridge session and folds
it into the system prompt after the managed prompt and before the caller's
instructions, on all three lanes; the managed chat gets its own
enable_citations; local documents get a frozen copy of the same prompt
(pages only, local page content carries no blocks), pinned to the live text
by a key-gated parity test. client.citation_prompt(format=) exposes the
same text for framework-built agents: cite by default, markdown / footnote
for hosts that strip tags.

Stacks on #494 (bridge prompts/get). Verified live: block documents cite
block ids, legacy and local documents cite pages, on the answer lane and
both protocol lanes.

Claude-Session: https://claude.ai/code/session_01Ex6uKLsDrQjZkqypjSyPwA
The refusal's only exit was dropping the chat model; own-model chat now
cites through chat(citations=True), so name it. Also drop an unused return
in the citation test helper.

Claude-Session: https://claude.ai/code/session_01Ex6uKLsDrQjZkqypjSyPwA
A format name passed as citations= was truthy and silently meant cite;
it now raises and names citation_prompt(format=) + instructions=.
citation_prompt(format="") returned the server's markdown default while
the getter's own default is cite; "" is unset, so it is cite too. That
left fetch_citation_prompt's unset fallbacks without a caller, so it
now takes the format it is given.

The enable_citations refusal's keyless arm still said local mode could
not cite; both arms now point at chat(citations=True). The docstrings
that route other formats through instructions= say own-model chat only.

Claude-Session: https://claude.ai/code/session_01RWo3vKJs6MVMxrfPxEVaxd
…comment

chat() reached the citation text through a lazy import of the private
fetch_citation_prompt(self, "cite") when the public citation_prompt()
getter beside it is exactly that call, so the "cite" default was spelled
in two places. chat() now uses the getter; the default lives in its
signature only.

The comment above LOCAL_CITATION_PROMPTS named the private chat repo and
its server-side prompt file. That file ships to PyPI with the package and
public code carries only the contract, so the comment keeps what the copy
is and why its block rules stay dormant locally.

Claude-Session: https://claude.ai/code/session_01XGBdRF2qoTcLNpRahj6QHx
Comment thread tests/test_agent_tools.py
over the same bridge session as agent_tools(); format rides as the
prompt's argument; PageIndex chat's cite format by default ("" is
unset, so the default too)."""
import pageindex.mcp_bridge as mcp_bridge
Comment thread tests/test_agent_tools.py

def test_citation_prompt_empty_raises(monkeypatch):
"""No silent empty guidance: a prompt with no text raises."""
import pageindex.mcp_bridge as mcp_bridge
@rejojer rejojer changed the title feat(mcp): fetch server prompts over the MCP bridge MCP prompts over the bridge + chat(citations=True) Sep 10, 2026
@rejojer

rejojer commented Sep 10, 2026

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 10, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-10T10:44:02.635444Z 8ae62c2 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8ae62c2163

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread pageindex/agent_tools.py
- When page content includes block_id values, citations MUST be block-level: copy the exact block_id of the supporting block. Page-only cites are allowed ONLY when the tool output carries no block_id (legacy documents, structure outlines). NEVER invent or alter block_id values.
- For a claim drawn from multiple blocks on one page, add one tag per supporting block (at most 3); beyond that, cite the single strongest block.
- Each tag must reference a SINGLE page integer. For multi-page citations, use separate tags.
- Close every answer with a "Sources" section: one plain-text line per cited block, formatted `- <document name>, page <page> (block <block_id>)`. Keep it even when the inline tags are present — a client that strips unknown HTML tags would otherwise leave the answer with no visible citations at all.""",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Allow page-only entries in local Sources sections

When chat(citations=True) runs on local documents, _get_page_content() never supplies block IDs, but this instruction requires every Sources entry to include (block <block_id>) while the preceding rule prohibits inventing one. The model therefore cannot satisfy both instructions and may fabricate block IDs or omit the required Sources section; provide a page-only Sources format when no block ID exists.

Useful? React with 👍 / 👎.

Comment thread pageindex/client.py
"instructions blocks are the Messages protocol's shape — "
"with protocol=\"messages\" they append after the managed "
"system blocks; the other lanes take a string.")
if citations and citations is not True:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject falsey non-boolean citation values

When callers pass a falsey non-boolean such as "", [], None, or 0, this condition is skipped and the value silently disables citations despite the public contract saying only True and False are accepted. Validate the type independently of truthiness so invalid falsey inputs raise just like the tested truthy string does.

Useful? React with 👍 / 👎.

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