Add --host, --port to ingester cli, update docker compose example

This commit is contained in:
Yiorgis Gozadinos 2026-05-25 10:45:16 +03:00
parent ec94426556
commit f4468b65ee
No known key found for this signature in database
9 changed files with 89 additions and 26 deletions

View file

@ -409,6 +409,9 @@ haiku-rag mcp --stdio
# Custom port # Custom port
haiku-rag mcp --port 9000 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) # Read-only mode (no write tools)
haiku-rag --read-only mcp haiku-rag --read-only mcp
``` ```

View file

@ -217,8 +217,14 @@ ingester:
haiku-ingester serve # workers + pollers + API haiku-ingester serve # workers + pollers + API
haiku-ingester serve --no-api # workers + pollers only haiku-ingester serve --no-api # workers + pollers only
haiku-ingester serve --db /path.lancedb # explicit DB 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 The service blocks until SIGINT or SIGTERM. Shutdown drains the API
server, then pollers, then in-flight workers. server, then pollers, then in-flight workers.

View file

@ -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: The MCP server supports Streamable HTTP and stdio transports:
```bash ```bash
# Default streamable HTTP transport on port 8001 # Default streamable HTTP transport on 127.0.0.1:8001
haiku-rag mcp haiku-rag mcp
# Custom port # Custom port
haiku-rag mcp --port 9000 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) # stdio transport (for Claude Desktop)
haiku-rag mcp --stdio haiku-rag mcp --stdio
@ -20,6 +23,10 @@ haiku-rag mcp --stdio
haiku-rag --read-only 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. **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 ## Claude Desktop Integration

View file

@ -16,7 +16,9 @@ services:
start_interval: 5s start_interval: 5s
# LanceDB allows one writer + N readers per database URI; the ingester is # 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: haiku-ingester:
build: build:
context: ../.. context: ../..
@ -44,6 +46,19 @@ services:
depends_on: depends_on:
docling-serve: docling-serve:
condition: service_healthy 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 restart: unless-stopped
haiku-rag: haiku-rag:
@ -56,6 +71,8 @@ services:
"/app/haiku.rag.yaml", "/app/haiku.rag.yaml",
"--read-only", "--read-only",
"mcp", "mcp",
"--host",
"0.0.0.0",
"--port", "--port",
"8001", "8001",
] ]
@ -70,5 +87,6 @@ services:
- VOYAGE_API_KEY=${VOYAGE_API_KEY} - VOYAGE_API_KEY=${VOYAGE_API_KEY}
- CO_API_KEY=${CO_API_KEY} - CO_API_KEY=${CO_API_KEY}
depends_on: depends_on:
- haiku-ingester haiku-ingester:
condition: service_healthy
restart: unless-stopped restart: unless-stopped

View file

@ -10,6 +10,9 @@ ingester:
# Queue lives next to the LanceDB so both persist in the data volume. # Queue lives next to the LanceDB so both persist in the data volume.
queue: queue:
path: /data/ingester.db path: /data/ingester.db
api:
# Bind to all interfaces inside the container so docker port-mapping works.
host: 0.0.0.0
sources: sources:
- type: fs - type: fs
id: docs id: docs

View file

@ -728,6 +728,7 @@ class HaikuRAGApp: # pragma: no cover
async def run_mcp( async def run_mcp(
self, self,
transport: str | None = None, transport: str | None = None,
host: str = "127.0.0.1",
port: int = 8001, port: int = 8001,
): ):
"""Run the MCP server until interrupted.""" """Run the MCP server until interrupted."""
@ -744,7 +745,9 @@ class HaikuRAGApp: # pragma: no cover
if transport == "stdio": if transport == "stdio":
await server.run_stdio_async() await server.run_stdio_async()
else: else:
logger.info(f"Starting MCP server on port {port}") logger.info(f"Starting MCP server on {host}:{port}")
await server.run_http_async(transport="streamable-http", port=port) await server.run_http_async(
transport="streamable-http", host=host, port=port
)
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass

View file

@ -685,6 +685,11 @@ def mcp(
"--stdio", "--stdio",
help="Run MCP server on stdio Transport", 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( port: int = typer.Option(
8001, 8001,
"--port", "--port",
@ -697,7 +702,7 @@ def mcp(
transport = "stdio" if stdio else None # pragma: no cover transport = "stdio" if stdio else None # pragma: no cover
asyncio.run( # pragma: no cover asyncio.run( # pragma: no cover
app.run_mcp(transport=transport, port=port) app.run_mcp(transport=transport, host=host, port=port)
) )

View file

@ -54,7 +54,12 @@ class IngesterApp:
jitter=ingester_cfg.workers.retry.jitter, 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._client = client
self._pool = WorkerPool( self._pool = WorkerPool(
client=client, client=client,

View file

@ -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: def _configure_logfire() -> None:
"""Logfire emits spans only when LOGFIRE_TOKEN is set; otherwise it """Logfire emits spans only when LOGFIRE_TOKEN is set; otherwise it
stays silent. Console output is disabled in either case so span lines 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") @queue_cli.command("init")
def queue_init( def queue_init(
config: Path | None = typer.Option(
None, "--config", "-c", help="Path to haiku.rag.yaml."
),
queue: Path | None = typer.Option( queue: Path | None = typer.Option(
None, None,
"--queue", "--queue",
@ -109,17 +120,13 @@ def queue_init(
), ),
) -> None: ) -> None:
"""Create the queue DB and apply the current schema. Idempotent.""" """Create the queue DB and apply the current schema. Idempotent."""
app_config = _load_config_with_override(config) path = _resolve_queue_path(get_config(), queue)
path = _resolve_queue_path(app_config, queue)
asyncio.run(_ensure_schema(path)) asyncio.run(_ensure_schema(path))
typer.echo(f"Queue initialized at {path}") typer.echo(f"Queue initialized at {path}")
@queue_cli.command("migrate") @queue_cli.command("migrate")
def queue_migrate( def queue_migrate(
config: Path | None = typer.Option(
None, "--config", "-c", help="Path to haiku.rag.yaml."
),
queue: Path | None = typer.Option( queue: Path | None = typer.Option(
None, None,
"--queue", "--queue",
@ -128,8 +135,7 @@ def queue_migrate(
), ),
) -> None: ) -> None:
"""Apply any pending schema migrations to an existing queue DB. Idempotent.""" """Apply any pending schema migrations to an existing queue DB. Idempotent."""
app_config = _load_config_with_override(config) path = _resolve_queue_path(get_config(), queue)
path = _resolve_queue_path(app_config, queue)
asyncio.run(_ensure_schema(path)) asyncio.run(_ensure_schema(path))
typer.echo(f"Queue at {path} is up to date") 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") @_cli.command("serve")
def serve( def serve(
config: Path | None = typer.Option(
None, "--config", "-c", help="Path to haiku.rag.yaml."
),
db: Path | None = typer.Option( db: Path | None = typer.Option(
None, None,
"--db", "--db",
help="LanceDB path (overrides config.storage.data_dir).", 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( no_api: bool = typer.Option(
False, False,
"--no-api", "--no-api",
@ -156,7 +169,11 @@ def serve(
) -> None: ) -> None:
"""Run the production ingester: pollers + workers (and the HTTP API """Run the production ingester: pollers + workers (and the HTTP API
unless --no-api is set). Blocks until SIGINT/SIGTERM.""" 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) db_path = _resolve_db_path(app_config, db)
app = IngesterApp(config=app_config, db_path=db_path) app = IngesterApp(config=app_config, db_path=db_path)
asyncio.run(app.serve(api=not no_api)) asyncio.run(app.serve(api=not no_api))
@ -165,9 +182,6 @@ def serve(
@_cli.command("run-once") @_cli.command("run-once")
def run_once( def run_once(
uri: str = typer.Argument(..., help="URI to ingest (file://, http(s)://, s3://)."), 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( db: Path | None = typer.Option(
None, None,
"--db", "--db",
@ -182,8 +196,7 @@ def run_once(
Bypasses the queue does NOT enqueue. Useful for smoke-testing the Bypasses the queue does NOT enqueue. Useful for smoke-testing the
Source adapter + pipeline path without spinning up the full pool. Source adapter + pipeline path without spinning up the full pool.
""" """
app_config = _load_config_with_override(config) asyncio.run(_run_once(get_config(), uri, db, delete))
asyncio.run(_run_once(app_config, uri, db, delete))
async def _run_once( async def _run_once(