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
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
```

View file

@ -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.

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:
```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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)
)

View file

@ -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,

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:
"""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(