From f4468b65ee541a565be33f5f7373e23f29bfa7cd Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Mon, 25 May 2026 10:45:16 +0300 Subject: [PATCH] Add --host, --port to ingester cli, update docker compose example --- docs/cli.md | 3 ++ docs/ingester.md | 6 +++ docs/mcp.md | 9 ++++- examples/docker/docker-compose.yml | 22 +++++++++- examples/docker/haiku.rag.yaml.example | 3 ++ haiku_rag_slim/haiku/rag/app.py | 7 +++- haiku_rag_slim/haiku/rag/cli.py | 7 +++- haiku_rag_slim/haiku/rag/ingester/app.py | 7 +++- haiku_rag_slim/haiku/rag/ingester/cli.py | 51 +++++++++++++++--------- 9 files changed, 89 insertions(+), 26 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 4e98f7df..25426a2a 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -409,6 +409,9 @@ haiku-rag mcp --stdio # Custom port haiku-rag mcp --port 9000 +# Bind to all interfaces (containers, trusted LAN) +haiku-rag mcp --host 0.0.0.0 + # Read-only mode (no write tools) haiku-rag --read-only mcp ``` diff --git a/docs/ingester.md b/docs/ingester.md index d267333e..ca557412 100644 --- a/docs/ingester.md +++ b/docs/ingester.md @@ -217,8 +217,14 @@ ingester: haiku-ingester serve # workers + pollers + API haiku-ingester serve --no-api # workers + pollers only haiku-ingester serve --db /path.lancedb # explicit DB +haiku-ingester serve --host 0.0.0.0 # bind API on all interfaces +haiku-ingester serve --port 9000 # override API port ``` +`--host` and `--port` are CLI overrides for `ingester.api.host` and +`ingester.api.port` in `haiku.rag.yaml`. Both default to the YAML value +(which itself defaults to `127.0.0.1:8765` — loopback only). + The service blocks until SIGINT or SIGTERM. Shutdown drains the API server, then pollers, then in-flight workers. diff --git a/docs/mcp.md b/docs/mcp.md index 653e6f3d..8d0f74cb 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -7,12 +7,15 @@ The MCP server exposes `haiku.rag` as MCP tools for compatible MCP clients like The MCP server supports Streamable HTTP and stdio transports: ```bash -# Default streamable HTTP transport on port 8001 +# Default streamable HTTP transport on 127.0.0.1:8001 haiku-rag mcp # Custom port haiku-rag mcp --port 9000 +# Bind to all interfaces (e.g. inside a container) +haiku-rag mcp --host 0.0.0.0 --port 8001 + # stdio transport (for Claude Desktop) haiku-rag mcp --stdio @@ -20,6 +23,10 @@ haiku-rag mcp --stdio haiku-rag --read-only mcp --stdio ``` +`--host` defaults to `127.0.0.1` (loopback only). Bind to `0.0.0.0` only +when you want the MCP server reachable from outside the local machine — +e.g. inside a Docker container with port mapping, or on a trusted LAN. + **Read-only mode:** When `--read-only` is specified, write tools (`add_document_from_file`, `add_document_from_url`, `add_document_from_text`, `delete_document`) are not registered. Only search and query tools remain available. ## Claude Desktop Integration diff --git a/examples/docker/docker-compose.yml b/examples/docker/docker-compose.yml index d4e65246..43b29dac 100644 --- a/examples/docker/docker-compose.yml +++ b/examples/docker/docker-compose.yml @@ -16,7 +16,9 @@ services: start_interval: 5s # LanceDB allows one writer + N readers per database URI; the ingester is - # the writer, MCP runs read-only. + # the writer, MCP runs read-only. The ingester opens LanceDB on startup + # (creating it if missing), so its /health endpoint is a valid signal + # that the DB exists — MCP waits on it via condition: service_healthy. haiku-ingester: build: context: ../.. @@ -44,6 +46,19 @@ services: depends_on: docling-serve: condition: service_healthy + healthcheck: + # No curl in the slim image; use Python's stdlib instead. + test: + [ + "CMD", + "python", + "-c", + "import urllib.request, sys; sys.exit(0 if urllib.request.urlopen('http://localhost:8765/health', timeout=2).status == 200 else 1)", + ] + interval: 5s + timeout: 3s + retries: 12 + start_period: 30s restart: unless-stopped haiku-rag: @@ -56,6 +71,8 @@ services: "/app/haiku.rag.yaml", "--read-only", "mcp", + "--host", + "0.0.0.0", "--port", "8001", ] @@ -70,5 +87,6 @@ services: - VOYAGE_API_KEY=${VOYAGE_API_KEY} - CO_API_KEY=${CO_API_KEY} depends_on: - - haiku-ingester + haiku-ingester: + condition: service_healthy restart: unless-stopped diff --git a/examples/docker/haiku.rag.yaml.example b/examples/docker/haiku.rag.yaml.example index 215daf6d..ecc785f6 100644 --- a/examples/docker/haiku.rag.yaml.example +++ b/examples/docker/haiku.rag.yaml.example @@ -10,6 +10,9 @@ ingester: # Queue lives next to the LanceDB so both persist in the data volume. queue: path: /data/ingester.db + api: + # Bind to all interfaces inside the container so docker port-mapping works. + host: 0.0.0.0 sources: - type: fs id: docs diff --git a/haiku_rag_slim/haiku/rag/app.py b/haiku_rag_slim/haiku/rag/app.py index 46f8d67b..185ee07a 100644 --- a/haiku_rag_slim/haiku/rag/app.py +++ b/haiku_rag_slim/haiku/rag/app.py @@ -728,6 +728,7 @@ class HaikuRAGApp: # pragma: no cover async def run_mcp( self, transport: str | None = None, + host: str = "127.0.0.1", port: int = 8001, ): """Run the MCP server until interrupted.""" @@ -744,7 +745,9 @@ class HaikuRAGApp: # pragma: no cover if transport == "stdio": await server.run_stdio_async() else: - logger.info(f"Starting MCP server on port {port}") - await server.run_http_async(transport="streamable-http", port=port) + 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/cli.py b/haiku_rag_slim/haiku/rag/cli.py index 7a085ed5..8e0a2c66 100644 --- a/haiku_rag_slim/haiku/rag/cli.py +++ b/haiku_rag_slim/haiku/rag/cli.py @@ -685,6 +685,11 @@ def mcp( "--stdio", help="Run MCP server on stdio Transport", ), + host: str = typer.Option( + "127.0.0.1", + "--host", + help="Host to bind MCP server to (use 0.0.0.0 in containers; ignored with --stdio)", + ), port: int = typer.Option( 8001, "--port", @@ -697,7 +702,7 @@ def mcp( transport = "stdio" if stdio else None # pragma: no cover asyncio.run( # pragma: no cover - app.run_mcp(transport=transport, port=port) + app.run_mcp(transport=transport, host=host, port=port) ) diff --git a/haiku_rag_slim/haiku/rag/ingester/app.py b/haiku_rag_slim/haiku/rag/ingester/app.py index 03600207..62e31e91 100644 --- a/haiku_rag_slim/haiku/rag/ingester/app.py +++ b/haiku_rag_slim/haiku/rag/ingester/app.py @@ -54,7 +54,12 @@ class IngesterApp: jitter=ingester_cfg.workers.retry.jitter, ) - async with HaikuRAG(self._db_path, config=self._config) as client: + # The ingester is the sole writer for its LanceDB target; + # create on first start so docker-compose / fresh deployments + # don't require a manual `haiku-rag init`. + async with HaikuRAG( + self._db_path, config=self._config, create=True + ) as client: self._client = client self._pool = WorkerPool( client=client, diff --git a/haiku_rag_slim/haiku/rag/ingester/cli.py b/haiku_rag_slim/haiku/rag/ingester/cli.py index 395ecc17..3cb0b81e 100644 --- a/haiku_rag_slim/haiku/rag/ingester/cli.py +++ b/haiku_rag_slim/haiku/rag/ingester/cli.py @@ -36,6 +36,20 @@ _cli = typer.Typer( ) +@_cli.callback() +def main( + config: Path | None = typer.Option( + None, + "--config", + "-c", + help="Path to haiku.rag.yaml. Falls back to a discovered project YAML, then the process default.", + ), +) -> None: + """Top-level callback so every subcommand inherits --config without + each one redeclaring it. Mirrors haiku-rag's CLI shape.""" + _load_config_with_override(config) + + def _configure_logfire() -> None: """Logfire emits spans only when LOGFIRE_TOKEN is set; otherwise it stays silent. Console output is disabled in either case so span lines @@ -98,9 +112,6 @@ async def _ensure_schema(path: Path) -> None: @queue_cli.command("init") def queue_init( - config: Path | None = typer.Option( - None, "--config", "-c", help="Path to haiku.rag.yaml." - ), queue: Path | None = typer.Option( None, "--queue", @@ -109,17 +120,13 @@ def queue_init( ), ) -> None: """Create the queue DB and apply the current schema. Idempotent.""" - app_config = _load_config_with_override(config) - path = _resolve_queue_path(app_config, queue) + path = _resolve_queue_path(get_config(), queue) asyncio.run(_ensure_schema(path)) typer.echo(f"Queue initialized at {path}") @queue_cli.command("migrate") def queue_migrate( - config: Path | None = typer.Option( - None, "--config", "-c", help="Path to haiku.rag.yaml." - ), queue: Path | None = typer.Option( None, "--queue", @@ -128,8 +135,7 @@ def queue_migrate( ), ) -> None: """Apply any pending schema migrations to an existing queue DB. Idempotent.""" - app_config = _load_config_with_override(config) - path = _resolve_queue_path(app_config, queue) + path = _resolve_queue_path(get_config(), queue) asyncio.run(_ensure_schema(path)) typer.echo(f"Queue at {path} is up to date") @@ -140,14 +146,21 @@ def _resolve_db_path(config: AppConfig, override: Path | None) -> Path: @_cli.command("serve") def serve( - config: Path | None = typer.Option( - None, "--config", "-c", help="Path to haiku.rag.yaml." - ), db: Path | None = typer.Option( None, "--db", help="LanceDB path (overrides config.storage.data_dir).", ), + host: str | None = typer.Option( + None, + "--host", + help="Bind the HTTP control plane to HOST (overrides ingester.api.host; use 0.0.0.0 in containers).", + ), + port: int | None = typer.Option( + None, + "--port", + help="Bind the HTTP control plane to PORT (overrides ingester.api.port).", + ), no_api: bool = typer.Option( False, "--no-api", @@ -156,7 +169,11 @@ def serve( ) -> None: """Run the production ingester: pollers + workers (and the HTTP API unless --no-api is set). Blocks until SIGINT/SIGTERM.""" - app_config = _load_config_with_override(config) + app_config = get_config() + if host is not None: + app_config.ingester.api.host = host + if port is not None: + app_config.ingester.api.port = port db_path = _resolve_db_path(app_config, db) app = IngesterApp(config=app_config, db_path=db_path) asyncio.run(app.serve(api=not no_api)) @@ -165,9 +182,6 @@ def serve( @_cli.command("run-once") def run_once( uri: str = typer.Argument(..., help="URI to ingest (file://, http(s)://, s3://)."), - config: Path | None = typer.Option( - None, "--config", "-c", help="Path to haiku.rag.yaml." - ), db: Path | None = typer.Option( None, "--db", @@ -182,8 +196,7 @@ def run_once( Bypasses the queue — does NOT enqueue. Useful for smoke-testing the Source adapter + pipeline path without spinning up the full pool. """ - app_config = _load_config_with_override(config) - asyncio.run(_run_once(app_config, uri, db, delete)) + asyncio.run(_run_once(get_config(), uri, db, delete)) async def _run_once(