docs← Back to article

Markdown for LLMs

Configuration reference

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

Download this articlePlain text ↗
# Configuration reference

## Goal

Give every setting of the two network executables in one place: each
setting's type, default, source (flag or environment), precedence,
validation, whether a restart is needed, and whether it is secret —
with links to the scenario articles that use it.

## Scope

Components: `law-mcp-server` (binary name in
`law-mcp/Cargo.toml`; its help text prints `law-mcp`)
and `law serve` (`law-bin-core/src/serve.rs`,
`law-serve-core/`). Scenario: any deployment that
starts these processes. All values below are synthetic examples. Words
like default, implementation-limit, reference-setting, and
operator-policy-example are used with distinct meanings (see the last
table); implemented / described / verified / supported follow the
[section vocabulary](/operate/verification-support-matrix/#section-vocabulary-binding).

## Prerequisites

- A checkout or release tree containing the binaries.
- For `law-mcp-server`: a corpus profile name
  (one JSON file in the checkout's profiles directory, or `all`).
- For `law serve`: a world directory and a journal directory (see
  [Deploy a private HTTP service](/operate/private-http-service/)).

## `law-mcp-server` settings

Source rule (verified in `src/main.rs`, `Args::parse`): every setting
first reads its environment variable, then a command-line flag
overwrites it. Precedence is therefore **flag > environment >
built-in default** for settings that have both; settings with only an
environment source use **environment > built-in default**. All settings
take effect only after a process restart: **every change needs a
restart**. The four external-leg variables are read from the
process environment on every call, but that is not live reload —
editing the parent shell or the systemd `EnvironmentFile` does not
change the environment of the already-running process, so a new
value still needs a restart (or relaunch) to take effect. There is
no configuration file and no reload signal.

| Setting | Type | Default | Source | Validation | Restart | Secret |
|---|---|---|---|---|---|---|
| Root tree | path | auto-detected (`find_root` from cwd) | `LAW_MCP_ROOT`, `--root` | must hold the profiles directory for named profiles | yes | no |
| Profile | string | `all` (empty env counts as unset) | `LAW_MCP_PROFILE`, `--profile` | `all`, or an existing profile file whose facets name each known facet exactly once | yes | no |
| HTTP mode | bool | off (stdio JSON-RPC on stdin/stdout) | `--http` only | — | yes | no |
| Host | string | `127.0.0.1` | `LAW_MCP_HOST`, `--host` | non-loopback without a token is refused unless public (exact error names `LAW_MCP_TOKEN` and `LAW_MCP_PUBLIC=1`) | yes | no |
| Port | u16 | `8722` | `LAW_MCP_PORT`, `--port` | must parse as u16 | yes | no |
| Token | string | none | `LAW_MCP_TOKEN`, `--token` | exact `Authorization: Bearer <token>` match per request; flag wins over env; empty env counts as unset but empty `--token ""` is a real empty token (quirk: it also permits a public bind while demanding `Authorization: Bearer ` with a trailing space) | yes | **yes** |
| Public | bool | off | `LAW_MCP_PUBLIC=1`, `--public` | explicit consent to serve without a token | yes | no |
| Allowed origins | comma list / repeatable | empty (any `Origin` header is refused with 403) | `LAW_MCP_ALLOWED_ORIGINS`, `--allowed-origin` | exact string match against the `Origin` header | yes | no |
| Call timeout | seconds, fractions allowed | none (no limit) | `LAW_MCP_CALL_TIMEOUT` only | non-negative number; empty or `0` means no limit | yes | no |
| Journal on/off | bool | on | `LAW_MCP_LOG=0` disables | — | yes | no |
| Journal body cap | bytes per side | `65536` (64 KiB) | `LAW_MCP_LOG_MAX_BODY` only | must parse as usize; `0` means no cap | yes | no |
| Journal socket | path | `/run/systemd/journal/socket` | `LAW_MCP_LOG_SOCKET` only | if the socket is missing, records fall back to one JSON line on stderr | yes | no |
| OTLP receiver | HTTP(S) URL | none (disabled) | `LAW_MCP_OTLP_URL` only | non-empty string; spans POST as JSON to exactly this URL | yes | no |
| Answers service URL | HTTPS URL | none (the 4 publication tools answer "unavailable") | `LAW_ANSWERS_URL` only; stdio + HTTP; read per call | non-empty; http loopback-only; credentials-in-URL rejected | yes | no |
| Answers service token | string | none (same unavailability) | `LAW_ANSWERS_TOKEN` only; stdio + HTTP; read per call | required non-empty together with the URL; sent as `Authorization: Bearer` | yes | **yes** |
| Neural service URL | HTTPS URL | none (lexical-only answers) | `LAW_NEURAL_URL` only; stdio + HTTP; read per call | same URL rules as above | yes | no |
| Neural service token | string | `""` (appendix inactive) | `LAW_NEURAL_TOKEN` only; stdio + HTTP; read per call | empty refused by the client (`SERVICE_CONFIG` → inactive appendix); sent as `Authorization: Bearer` | yes | **yes** |
| Artifact dir | path | `$HOME` (required at call time) | `LAW_MCP_ARTIFACT_DIR` only | missing dir and missing `$HOME` fail the call with `ANSWER_RESOURCE_UNAVAILABLE` | yes | no |

Two unit templates (reference-settings, not code defaults) —
never mix them:

- Private (the model for every closed deployment in this
  section): `kit/secure-deploy/law-mcp-private@.service` sets
  `LAW_MCP_PORT=%i`, `LAW_MCP_HOST=127.0.0.1`,
  `LAW_MCP_CALL_TIMEOUT=110`, `LAW_MCP_LOG=1`, `MemoryMax=6G`,
  `User=arxo-mcp`, `NoNewPrivileges=true`, `RestartSec=3`, and
  reads the token from `EnvironmentFile=/etc/arxo/mcp-%i.env`.
  It contains no `PUBLIC` line: without a usable token the
  process refuses to serve instead of opening up.
- Public (deliberate open bind only): the shipped
  `arxo-ops/deploy/mcp/law-mcp@.service` adds `LAW_MCP_PUBLIC=1`.
  Copying that line into a private instance silently converts a
  missing token from a startup refusal into an open evaluator —
  keep it out of every closed recipe.

Flags that are commands, not settings: `--healthcheck` (probes
`127.0.0.1:<port>/healthz` with a 4 s timeout, exit 0/2; honors
`--port`), `--export-neural-input PATH` (writes the search index
document and exits), `-h/--help`.

Implementation-limits (constants, not settings): request body cap
`MAX_BODY` = 1 MiB (`src/http.rs`); no request batching; no SSE
(`GET /mcp` → 405); no sessions (`DELETE` → 405).

## `law serve` settings

Source rule (verified in `serve.rs` `parse` + `service.rs`
`Config::default`): **flags only** — `law serve` reads no environment
variable for its own configuration. All flags are parsed once at
startup: **every change needs a process restart**.

| Setting | Flag | Type | Default | Validation | Restart | Secret |
|---|---|---|---|---|---|---|
| World | `--world` (required) | dir or pinned `profile.json` | none | dir, or a file named `profile.json` under `deps/profiles/<id>/<version>/` matching the lock; a `.arxo` bundle is refused — restore the world dir from the bundle first | yes | no |
| Journal | `--journal` / `--no-journal` | dir / bool | none — one of the two is required | `--no-journal` is development only (unrecorded decisions never replay) | yes | no |
| Watch world | `--watch-world` | bool | off | watches the `--world` symlink target, warms the new world; the journal stays shared, new records carry the new pin | yes | no |
| Host | `--host` | string | `127.0.0.1` | non-loopback without a token is refused unless `--public` | yes | no |
| Port | `--port` | u16 | `8480` | must parse | yes | no |
| Token | `--token` / `--token-file` (mutually exclusive) | string / path | none | exact `Authorization: Bearer <token>` match; empty file token refused | yes | **yes** |
| Public | `--public` | bool | off | explicit consent to bind without a token on any address; requires the process-worker path (the CLI always sets it to its own executable, `serve.rs`) | yes | no |
| Workers | `--workers` | usize | `2` | at least 1 | yes | no |
| Queue | `--queue` | usize | `64` | at least 1 | yes | no |
| Max body | `--max-body` | bytes | `1048576` (1 MiB) | must parse | yes | no |
| Max assertions | `--max-assertions` | usize | `10000` | must parse | yes | no |
| Max response | `--max-response` | bytes | `16777216` (16 MiB) | must parse | yes | no |
| Call timeout | `--call-timeout-ms` | ms | `30000` (30 s) | positive number | yes | no |
| Journald socket | `--log-socket` | path | none (JSON to stderr) | call fields to native journald when set | yes | no |
| OTLP receiver | `--otlp-url` | HTTP URL | none (disabled) | must start with `http://` or `https://` | yes | no |

Implementation-limits (no flag): socket I/O timeout 10 s
(`io_timeout`), journal segment cap 64 MiB (`max_segment`),
`Idempotency-Key` 1–255 printable ASCII characters without spaces.

## The `law` launcher

The `./law` shell script reads one variable: `LAW_PYTHON` — path to a
Python ≥ 3.12 with `jsonschema` and `fastjsonschema` for `law dev`
subcommands. It is launcher-only: neither Rust executable reads it.

## Steps

1. Pick the scenario article first ([Deploy a private HTTP service](/operate/private-http-service/) for
   `law serve`; [HTTP, TLS and reverse proxies](/operate/http-tls-reverse-proxies/) for the MCP edge):
   copy its exact command, not the tables above.
2. Set secrets through the file/env channel the scenario names
   (`--token-file`, `LAW_MCP_TOKEN` via a systemd `EnvironmentFile`),
   never on a shared command line.
3. Start the process and read the stderr launch line: `law-mcp-server`
   prints address, profile, journal channel, call limit, and OTLP
   receiver; that line is the effective configuration.
4. Change any setting by editing the unit/flags and restarting the
   process.

## Expected result

- `law-mcp-server --http --port 8722` on loopback without a token
  starts and answers `GET /healthz` with
  `{"status":"ok",...}` (HTTP 200).
- `law serve --world <dir> --journal <dir>` starts after all workers
  warm up; `GET /readyz` returns 200.
- A non-loopback bind without a token exits before listening with the
  refusal quoted above (exit code 2).

## Result check

```bash
law-mcp-server --healthcheck --port 8722; echo "exit=$?"
curl -s http://127.0.0.1:8722/healthz
curl -s http://127.0.0.1:8480/readyz -o /dev/null -w "%{http_code}\n"
```

Success: exit `0`, a body containing `"status":"ok"`, and `200` for
`/readyz`. These commands are created-examples (paths and ports are
synthetic); the flags, routes, and fields they use exist in code.

## Failures and diagnostics

| Symptom | Cause | Fix |
|---|---|---|
| `bind on "0.0.0.0" without LAW_MCP_TOKEN is refused` | non-loopback HTTP without token/public | bind loopback, or set a token, or set `LAW_MCP_PUBLIC=1` deliberately |
| `address "0.0.0.0" without a token is refused` | same for `law serve` | `--token`/`--token-file`, or deliberate `--public` |
| `LAW_MCP_PORT: ...` / `--port: ...` | unparsable port | pass a 0–65535 integer |
| `facets.allow contains duplicates` / `each known facet must be named exactly once` | profile names a facet twice or misses one | fix the profile file (see [Tool profiles](/operate/tool-profiles-input-policy/)) |
| `OTLP URL: expected http:// or https://` | bad `law serve --otlp-url` | pass a full HTTP(S) URL |
| `--token and --token-file cannot be combined` | both passed | keep one |
| `direct launch requires worker_exe` | library misuse, not CLI | start via the `law serve` command |

## Support boundaries

- Changing any setting without restarting the process is not
  supported. The four external-leg variables are re-read per call
  from the process environment, but a changed shell or
  `EnvironmentFile` value still reaches the process only through a
  restart (or relaunch); everything else binds at startup.
- There is no configuration file format: any `.toml`/`.yaml` snippet
  for these executables is an operator-policy-example, not a
  server-read file.
- The verified constants with no setting are the 1 MiB body cap, the
  10 s I/O timeout, and the 64 MiB segment: tuning them requires a
  code change. Anything else absent from these tables is either a
  constant the author did not verify (treat as unsupported until
  checked) or a new setting — not a hidden option you can guess.

## Vocabulary used in this series

| Word | Meaning | Example |
|---|---|---|
| Default | value the code uses when the operator sets nothing | port `8722` |
| Implementation-limit | constant behavior with no setting | 1 MiB body cap |
| Reference-setting | value from a shipped deploy file, still overrideable | `LAW_MCP_CALL_TIMEOUT=110` in `law-mcp-private@.service` |
| Operator-policy-example | this documentation's suggestion, not read by code | run as a dedicated OS user |
| Created-example | synthetic value (path, token, host) safe to copy structurally | `LAW_MCP_TOKEN=REDACTED_EXAMPLE_TOKEN` |

## Next step

- To expose MCP over HTTPS: [HTTP, TLS, reverse proxies](/operate/http-tls-reverse-proxies/).
- To choose token vs public: [Authentication and access control](/operate/authentication-access-control/).
- To narrow the tool surface per profile:
  [Tool profiles and input policy](/operate/tool-profiles-input-policy/).