feat: add adapter read-only MCP boundary
This commit is contained in:
parent
1943c36ef3
commit
f1e31487c0
17 changed files with 209 additions and 45 deletions
|
|
@ -5,6 +5,8 @@ import unittest
|
|||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
|
||||
from mcp.shared.memory import create_connected_server_and_client_session
|
||||
|
||||
from docforge.adapter_contract import (
|
||||
AdapterEdge,
|
||||
AdapterNode,
|
||||
|
|
@ -16,6 +18,7 @@ from docforge.adapter_contract import (
|
|||
)
|
||||
from docforge.errors import DocForgeError
|
||||
from docforge.index import ProjectIndex
|
||||
from docforge.mcp_server import READ_TOOLS, create_read_only_server
|
||||
from docforge.models import Edge, Node
|
||||
|
||||
|
||||
|
|
@ -162,5 +165,62 @@ class AdapterContractTests(unittest.TestCase):
|
|||
self.assertEqual(["timeline"], mismatch["changed"])
|
||||
|
||||
|
||||
class AdapterReadOnlyMcpTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_adapter_project_exposes_only_read_tools_and_custom_context(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory).resolve()
|
||||
fixture = AdapterContractTests()
|
||||
project = AdapterProject(
|
||||
Loader(fixture.projection(root)),
|
||||
cache_root=root / ".cache" / "adapter-read-only",
|
||||
)
|
||||
index = ProjectIndex(project)
|
||||
index.build()
|
||||
calls: list[tuple[str, int | None]] = []
|
||||
|
||||
def context_provider(
|
||||
current: ProjectIndex, profile: str, budget: int | None
|
||||
) -> dict[str, object]:
|
||||
checked = current.check()
|
||||
calls.append((profile, budget))
|
||||
return {
|
||||
"status": "ok",
|
||||
"project_id": checked["project_id"],
|
||||
"project_root_fingerprint": checked["project_root_fingerprint"],
|
||||
"revision": checked["revision"],
|
||||
"source_hash": checked["source_hash"],
|
||||
"adapter": checked["adapter"],
|
||||
"profile": profile,
|
||||
"budget": budget,
|
||||
"estimated_tokens": 1,
|
||||
"entries": [],
|
||||
"omissions": [],
|
||||
}
|
||||
|
||||
server = create_read_only_server(project, context_provider=context_provider)
|
||||
async with create_connected_server_and_client_session(
|
||||
server, raise_exceptions=True
|
||||
) as session:
|
||||
tools = await session.list_tools()
|
||||
info = await session.call_tool("docforge_project_info", {})
|
||||
contract = await session.call_tool("docforge_get_contract", {})
|
||||
context = await session.call_tool(
|
||||
"docforge_get_context", {"profile": "fixture", "budget": 321}
|
||||
)
|
||||
|
||||
self.assertEqual(READ_TOOLS, tuple(tool.name for tool in tools.tools))
|
||||
self.assertEqual("adapter-fixture", info.structuredContent["project_id"])
|
||||
self.assertEqual(list(READ_TOOLS), contract.structuredContent["allowed_tools"])
|
||||
self.assertIn(
|
||||
"isolated_changeset_writes",
|
||||
contract.structuredContent["excluded_operations"],
|
||||
)
|
||||
self.assertFalse(contract.structuredContent["proposal_access"]["enabled"])
|
||||
self.assertFalse(contract.structuredContent["isolated_changeset_writes_allowed"])
|
||||
self.assertEqual("fixture", context.structuredContent["profile"])
|
||||
self.assertEqual([("fixture", 321)], calls)
|
||||
self.assertFalse(project.descriptor.changeset_root.exists())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue