1
0
Fork 0
Code Issues Pull requests Projects Releases 2 Packages Wiki Activity Actions Pages

Bound paged retrieval responses

This commit is contained in:
Andraxion 2026-07-29 06:02:07 -04:00
parent 176b2d2784
commit 529accf858
15 changed files with 1567 additions and 30 deletions

View file

@ -16,6 +16,7 @@ from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from mcp.shared.memory import create_connected_server_and_client_session
from docforge.changesets import ChangesetStore
from docforge.index import ProjectIndex
from docforge.mcp_server import (
ALL_TOOLS,
@ -80,6 +81,16 @@ class DocForgeMcpTests(unittest.IsolatedAsyncioTestCase):
):
self.assertIn("limit", tools[name].inputSchema["properties"])
self.assertNotIn("limit", tools[name].inputSchema.get("required", []))
for name in (
"docforge_get_context",
"docforge_list_changesets",
"docforge_get_changeset",
"docforge_validate_changeset",
"docforge_get_changeset_diff",
):
for field in ("limit", "cursor"):
self.assertIn(field, tools[name].inputSchema["properties"])
self.assertNotIn(field, tools[name].inputSchema.get("required", []))
self.assertIn(
"deep",
tools["docforge_render_status"].inputSchema["properties"],
@ -110,6 +121,134 @@ class DocForgeMcpTests(unittest.IsolatedAsyncioTestCase):
self.assertEqual(0, diagnostics["counters"]["project_loads"])
self.assertEqual(0, diagnostics["counters"]["source_files_parsed"])
async def test_context_pagination_is_complete_and_stale_cursors_fail_closed(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = self.copy_fixture("alpha", Path(directory))
ProjectIndex(Project.open(root)).build()
async with create_connected_server_and_client_session(
create_server(root), raise_exceptions=True
) as session:
first = await session.call_tool(
"docforge_get_context",
{"profile": "active", "limit": 1},
)
cursor = first.structuredContent["pagination"]["next_cursor"]
pages = [first.structuredContent]
while cursor is not None:
page = await session.call_tool(
"docforge_get_context",
{
"profile": "active",
"limit": 2,
"cursor": cursor,
},
)
pages.append(page.structuredContent)
cursor = page.structuredContent["pagination"]["next_cursor"]
stale_cursor = first.structuredContent["pagination"]["next_cursor"]
workflow = root / "docs" / "content" / "workflow.md"
workflow.write_text(
workflow.read_text(encoding="utf-8") + "\nChanged between pages.\n",
encoding="utf-8",
)
stale = await session.call_tool(
"docforge_get_context",
{
"profile": "active",
"limit": 1,
"cursor": stale_cursor,
},
)
evidence = [
*(("entry", item["node_id"]) for page in pages for item in page["entries"]),
*(("omission", item["node_id"]) for page in pages for item in page["omissions"]),
]
self.assertEqual(len(evidence), pages[0]["summary"]["evidence_count"])
self.assertEqual(len(evidence), len(set(evidence)))
self.assertEqual("stale_cursor", stale.structuredContent["error"]["code"])
self.assertEqual("stale", stale.structuredContent["staleness"])
self.assertEqual(
"restart_pagination",
stale.structuredContent["error"]["remediation"]["action"],
)
async def test_oversized_changeset_reads_return_exact_pages_and_chunks(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = self.copy_fixture("alpha", Path(directory))
descriptor = root / ".docforge" / "project.toml"
descriptor.write_text(
descriptor.read_text(encoding="utf-8").replace(
"max_changeset_bytes = 100000",
"max_changeset_bytes = 100000\nmax_tool_output_chars = 20000",
),
encoding="utf-8",
)
project = Project.open(root)
ProjectIndex(project).build()
store = ChangesetStore(project, "alpha-editor")
created = store.create("mcp-chunked-diff")
foundation = next(
node for node in project.load().nodes if node.node_id == "guide.foundation"
)
proposed = store.propose_update(
changeset_id="mcp-chunked-diff",
expected_changeset_hash=str(created["changeset_hash"]),
node_id=foundation.node_id,
expected_content_hash=foundation.content_hash,
metadata=None,
content="replacement " * 4_000,
relationship_changes=[],
rationale="Exercise bounded MCP diff reconstruction.",
)
direct = store.diff("mcp-chunked-diff")
async with create_connected_server_and_client_session(
create_server(root), raise_exceptions=True
) as session:
inspected = await session.call_tool(
"docforge_get_changeset",
{"changeset_id": "mcp-chunked-diff"},
)
validated = await session.call_tool(
"docforge_validate_changeset",
{"changeset_id": "mcp-chunked-diff"},
)
cursor: str | None = None
chunks: list[str] = []
while True:
page = await session.call_tool(
"docforge_get_changeset_diff",
{
"changeset_id": "mcp-chunked-diff",
"cursor": cursor,
},
)
self.assertLessEqual(
len(json.dumps(page.structuredContent, separators=(",", ":"))),
20_000,
)
chunks.append(page.structuredContent["chunk"]["content"])
cursor = page.structuredContent["pagination"]["next_cursor"]
if cursor is None:
break
self.assertEqual("operation_summaries", inspected.structuredContent["result_mode"])
self.assertEqual("operation_summaries", validated.structuredContent["result_mode"])
self.assertTrue(validated.structuredContent["valid"])
self.assertEqual(
proposed["changeset_hash"],
validated.structuredContent["changeset_hash"],
)
self.assertEqual(
{
"changes": direct["changes"],
"operations": direct["operations"],
},
json.loads("".join(chunks)),
)
async def test_no_ast_binding_preserves_adapter_and_blocks_logic(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = self.copy_fixture("alpha", Path(directory))