# Deployment overview: SDK vs CLI vs MCP vs law serve ## Goal Choose the right deployment surface for private use: local CLI, MCP over stdio, MCP over HTTP, `law serve` HTTP, or the typed SDK — and understand which trust boundary each one crosses. ## Scope Components: `law` CLI (usages quoted from the [CLI reference](/cli/reference/), tool `0.1.1`), `law-mcp-server` binary (crate `law-mcp`), `@arxo/law-client` `0.2.0` (the npm client package). Scenario: a private, single-organization deployment; no public endpoint. All host names, tokens, and hashes below are synthetic examples. ## The five surfaces | Surface | Entry point | Transport | Auth in the tool | Corpus scope | |---|---|---|---|---| | CLI | `law ask`, `law case …` | local process | none (OS user boundary) | case package in cwd | | stdio MCP | `law-mcp-server` (default) | JSON-RPC lines on stdin/stdout | none (OS process boundary) | `--profile` slice | | HTTP MCP | `law-mcp-server --http` | `POST /mcp` | `Authorization: Bearer` when `--token` set | `--profile` slice | | `law serve` | `law serve --world … --journal …` | `POST /v1/ask` + service routes | `Authorization: Bearer` when `--token` set | one pinned world | | Remote SDK | `@arxo/law-client/mcp` | Streamable HTTP, one `POST /mcp` per message | caller-supplied header | whatever the server serves | | Embedded SDK | `@arxo/law` (evaluation via `@arxo/law-engine`) | in-process calls, no server | none (caller process boundary) | bundled engine + caller-supplied data | Facts behind the table, each read from its implementation body: - `law-mcp-server` serves stdio by default and HTTP only with `--http` (`law-mcp/src/main.rs`, `usage()`). - Both HTTP servers refuse a non-loopback bind without a token unless the operator passes the explicit opt-out (`--token`/`LAW_MCP_TOKEN`, or `--public` / `LAW_MCP_PUBLIC=1` as deliberate consent; `law-serve-core/src/service.rs` `Config::validate`, `law-mcp/src/http.rs` `Config::check`). - Neither HTTP server terminates TLS: there is no `--tls`/`--cert` flag in either usage string. Encryption to the outside world is an operator reverse proxy (operator-policy-example, see article 02). - The remote SDK computes nothing: it folds the envelope, parses the envelope, and validates the response shape (transport and shape validation only). The embedded SDK is the opposite choice — it evaluates inside the caller's process with no server at all, so case data never crosses a socket but the caller inherits engine versioning, resource use, and supply-chain trust directly. Pick the row that matches where evaluation must happen. ## Component and trust-boundary diagram Created-example layout; boxes are real components, arrows are real protocols, the `|||` column separates machines. stdio stays on the laptop (spawned child process); only HTTP crosses to the server host; the outbound legs leave whichever machine runs the server. ```text LAPTOP (local) ||| PRIVATE SERVER HOST ============== ||| ==================== MCP client spawns the server as a child ||| process; stdio never crosses the network: ||| ||| +----------+ stdio pipe, no auth +--------+||| +------------------+ | agent / | --------------------> | law- |||| | law-mcp-server | | editor | (OS process is the | mcp- |||| | --http, loopback | +----------+ trust boundary) | server |||| | --profile | +--------+||| +--------+---------+ ||| ^ HTTP clients (anywhere) reach the host ||| | POST /mcp over the network with a Bearer ||| | + Bearer ||| | +----------+ POST /mcp or /v1/ask +--------+||| +--------+---------+ | remote | Bearer through an ||| | law serve | | agent / | operator TLS edge ||| | --world (pinned) | | app (SDK)| -----------------------------> ||| | --journal (fsync | +----------+ ||| | before serving) | ||| +------------------+ Outbound legs (if set) leave whichever ||| machine runs the server: answers / neural ||| / OTLP over HTTPS. ||| ``` Boundary rules (implementation facts, not advice): 1. stdio surfaces (CLI, stdio MCP) have no in-tool authentication. Anything that can spawn the process or write to its stdin can ask. "Isolated" here means only: reachable solely through the OS process boundary. 2. HTTP surfaces (`--http` MCP, `law serve`) bind loopback by default (`127.0.0.1`; MCP port default `8722`, serve port default `8480`) and require a token for anything wider. "Secured" in these articles means only: loopback bind and/or Bearer token checked by the server, plus an operator TLS edge — never a claim about the strength of the law reasoning itself. 3. `law serve` answers only from the pinned world: a request whose `pin.{world,resolutionHash}` differs from the loaded `law.lock` is refused with `SERVE_PIN_MISMATCH` (HTTP 409, `law-serve-core/src/codes.rs`). ## Supported main route For a private service that other machines call, the supported main route is `law serve` with a journal, on loopback, behind an operator ingress: ```bash law serve --world /srv/cases/kompaniya --journal /srv/cases/kompaniya-journal \ --host 127.0.0.1 --port 8480 --token-file /srv/secrets/serve-token ``` Why this one: it is a public CLI command (see the [CLI reference](/cli/reference/)), every decision is recorded before it is served (replayable with `law replay-record --world --journal --decision `), and the pin check makes stale-client / rotated-world skew loud instead of silent. Full procedure is [Deploy a private HTTP service](/operate/private-http-service/); its acceptance branch is the serve branch of [Verification and support matrix](/operate/verification-support-matrix/). The section runs two explicit branches from here: - **Serve branch** (this main route): launch (02) → auth and TLS (06, 02-Step-3) → readiness and metrics (02, 12-serve-rows) → journal (02, 09-serve-rows) → upgrade, backup, rollback (15, 16, 17) → acceptance (20, checks S1–S10). - **MCP branch**: launch (03) → profile and token (07, 06) → edge (05) → call logs and artifacts (09, 07-matrix) → timeout and load (11, 18) → acceptance (20, checks M1–M8). Each shared article opens with an "Applies to" table naming which branch its recipe covers and where the other branch lives. Use stdio MCP instead when the only consumer is an agent or editor on the same machine (article 03). Use HTTP MCP instead when MCP-shaped clients must reach the corpus over the network but no pinned decision journal is needed. Use the SDK when calling from JavaScript and you want envelope folding plus response-shape validation without subprocesses. ## Requirements - The `law` binary installed by one route in [Install](/cli/install/); confirm with `law version`. - The `law-mcp-server` binary for MCP articles (crate `law-mcp`, `[[bin]] name = "law-mcp-server"` in its `Cargo.toml`). - For `law serve`: a case-package directory containing `law.toml` and `law.lock` (the world; `World::load` refuses without both). - For MCP: a checkout root holding the profiles directory and the CLIR tree (found by root discovery), or pass `--root` explicitly. Each profile is one `.json` file in the profiles directory. - Node.js only if you use the SDK (`@arxo/law-client`, ESM import). ## Transitions - CLI to `law serve`: same world directory. What `law ask [] …` answers locally, `POST /v1/ask` answers remotely; the envelope changes (`{pin, case, query|evaluate, …}`), the bytes of the decision do not. - stdio MCP to HTTP MCP: add `--http` (plus `--token` before any non-loopback `--host`). Clients switch from spawned-command config to an HTTP endpoint; the tool table is the same for the same `--profile`. - HTTP MCP to `law serve`: a real change of surface, not a flag flip. MCP tools (`law_ask`, `law_search`, …) become one `POST /v1/ask` envelope, the corpus slice becomes one pinned world, and answers gain journal records plus `Idempotency-Key` replay. - Any surface to a new world/slice: for serve, repoint `--world` (or rotate the symlink target under `--watch-world`); for MCP, restart with another `--profile`. Old pins keep failing loudly against the new world. ## Failures and diagnostics - `unknown flag …` (exit 2) from either server: the flag name is wrong; both usage strings are printed with `--help` (`-h` for MCP). - `SERVE_WORLD_INVALID` (HTTP 500 at startup, process exits): world directory lacks `law.toml`/`law.lock`, or a `profile.json` path sits outside `deps/profiles///`. - `address … without a token is refused` (startup refusal): a non-loopback `--host` needs `--token`/`--token-file`, or deliberate `--public`. - `--token and --token-file cannot be combined`; `--journal and --no-journal cannot be combined` (startup refusal, exit 2). ## Support boundaries - "Implemented", "described", "verified", and "supported" follow the binding definitions in [Verification and support matrix](/operate/verification-support-matrix/#section-vocabulary-binding): code existence alone never earns a stronger word. Anything labelled created-example (nginx/TLS configs, systemd units, client JSON, user accounts, token values) is an operator sketch, not a tested artifact. - No surface here performs TLS, rate limiting, or multi-tenant isolation by itself; those are operator layers outside the cited code. - `--no-journal` (serve) and `--public` / `LAW_MCP_PUBLIC=1` without a token are development/deliberate-consent modes (reference-settings, not defaults): unrecorded decisions never replay, and an unauthenticated evaluator answers anyone who reaches it. ## Result check You can name, for your consumer, the surface, its transport, where its trust boundary lies, and what the main-route command is. If the consumer is another machine and you need replayable decisions, proceed to article 02; if it is a local agent, to article 03. ## Next step - [02 Private HTTP service](/operate/private-http-service/): end-to-end `law serve`. - [03 Private MCP server](/operate/private-mcp-server/): stdio and HTTP MCP.