diff --git a/haiku_rag_slim/haiku/rag/app.py b/haiku_rag_slim/haiku/rag/app.py index 8a73919d..95e99fc5 100644 --- a/haiku_rag_slim/haiku/rag/app.py +++ b/haiku_rag_slim/haiku/rag/app.py @@ -918,20 +918,25 @@ class HaikuRAGApp: host: str = "127.0.0.1", port: int = 8001, ): - """Run the MCP server until interrupted.""" - async with HaikuRAG._covering( - self.scope, self.config, read_only=self.read_only - ): - server = create_mcp_server( - self._path, config=self._store_config, read_only=self.read_only - ) - try: - if transport == "stdio": - await server.run_stdio_async() - else: - logger.info(f"Starting MCP server on {host}:{port}") - await server.run_http_async( - transport="streamable-http", host=host, port=port - ) - except KeyboardInterrupt: - pass + """Run the MCP server until interrupted. + + The server opens its own client and validates it on startup, so nothing + is opened here first. + """ + # The ref's own path, not `_path`: a URI-backed database has none, and + # the local stand-in would override the URI in `_store_config`. + server = create_mcp_server( + self._one.db_path, + config=self._store_config, + read_only=self.read_only, + ) + try: + if transport == "stdio": + await server.run_stdio_async() + else: + logger.info(f"Starting MCP server on {host}:{port}") + await server.run_http_async( + transport="streamable-http", host=host, port=port + ) + except KeyboardInterrupt: + pass diff --git a/haiku_rag_slim/haiku/rag/mcp.py b/haiku_rag_slim/haiku/rag/mcp.py index 16e97d29..929b2d62 100644 --- a/haiku_rag_slim/haiku/rag/mcp.py +++ b/haiku_rag_slim/haiku/rag/mcp.py @@ -22,12 +22,16 @@ def _decode_images(images_base64: list[str] | None) -> list[bytes] | None: def create_mcp_server( - db_path: Path, config: AppConfig | None = None, read_only: bool = False + db_path: Path | None = None, + config: AppConfig | None = None, + read_only: bool = False, ) -> FastMCP: """Create an MCP server with the specified database path. Args: - db_path: Path to the database file. + db_path: Path to the database file, or None to let `config` place it. A + path overrides a configured `lancedb.uri`, so a URI-backed database + must pass None rather than a local stand-in. config: Configuration to use. read_only: If True, write tools (add_document_*, delete_document) are not registered. """ diff --git a/tests/test_mcp.py b/tests/test_mcp.py index 191549f9..6835d920 100644 --- a/tests/test_mcp.py +++ b/tests/test_mcp.py @@ -603,6 +603,44 @@ class TestMCPClientLifetime: assert opens == 1 + @pytest.mark.asyncio + async def test_a_uri_backed_database_is_not_replaced_by_a_local_path( + self, monkeypatch + ): + """A path overrides `lancedb.uri`, so the server gets the database's own + path — None where a URI placed it — not the local stand-in a URI-backed + ref resolves to for display.""" + from haiku.rag.app import HaikuRAGApp + from haiku.rag.client.scope import DatabaseScope + from haiku.rag.config.models import AppConfig, LanceDBConfig + + config = AppConfig( + lancedb=LanceDBConfig(databases={"prod": "s3://bucket/prod.lancedb"}) + ) + seen: dict = {} + + class _Server: + async def run_stdio_async(self): + return None + + def fake_create(db_path=None, config=None, read_only=False): + seen["db_path"] = db_path + seen["config"] = config + return _Server() + + monkeypatch.setattr("haiku.rag.app.create_mcp_server", fake_create) + app = HaikuRAGApp( + scope=DatabaseScope.resolve(config, database_name="prod"), config=config + ) + + await app.run_mcp(transport="stdio") + + assert seen["db_path"] is None + [ref] = DatabaseScope.resolve( + seen["config"], database_path=seen["db_path"] + ).databases + assert ref.uri == "s3://bucket/prod.lancedb" + @pytest.mark.asyncio async def test_startup_fails_when_the_database_cannot_open(self, tmp_path): mcp = create_mcp_server(tmp_path / "does-not-exist.lancedb", read_only=True)