docs← Back to article

Markdown for LLMs

Deployment overview: SDK vs CLI vs MCP vs law serve

The source Markdown for this article. Copy it into your assistant or download it as a text file.

Download this articlePlain text ↗
# 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 <slice>|
                                     +--------+|||  +--------+---------+
                                              |||           ^
  HTTP clients (anywhere) reach the host      |||           | POST /mcp
  over the network with a Bearer <token>         |||           | + Bearer
                                              |||           |
  +----------+  POST /mcp or /v1/ask +--------+|||  +--------+---------+
  | remote   |  Bearer <token>    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 <dir> --journal <dir> --decision <id>`),
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 `<name>.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 [<case>] …`
  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/<id>/<version>/`.
- `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.