MCP Runtime Commands¶
This page documents the current MCP runtime entry points in Assay.
1) assay mcp (CLI)¶
assay mcp is the canonical command family for MCP runtime work:
assay mcp wrapassay mcp preflightassay mcp manifest candidate|promoteassay mcp discoverassay mcp killassay mcp config-pathassay mcp tool keygen|sign|verify
The previous flat assay discover, assay kill, and assay tool ... spellings remain available as hidden compatibility shims and print a deprecation warning to stderr. Their stdout, exit codes, artifacts, and output formats remain unchanged.
2) assay mcp wrap (CLI)¶
Wrap a real MCP process and enforce policy decisions inline.
Each nonblank client line must be a single JSON object with unique members. Root arrays (including batches), scalars, malformed JSON and embedded line breaks are rejected locally, including in dry-run mode; they are never forwarded as opaque lines. Blank lines are ignored. CRLF terminators and JSON-escaped line breaks remain supported. Arrays inside parameters or response objects remain supported, and admitted frames retain their original bytes. Client response objects without a method member remain relayable for legacy upstream-initiated requests.
Synopsis¶
Common Usage¶
# Enforcing mode
assay mcp wrap --policy assay.yaml -- <real-mcp-command> [args...]
# Dry-run mode (log decisions, do not block)
assay mcp wrap --policy assay.yaml --dry-run -- <real-mcp-command> [args...]
Key Options¶
| Option | Description |
|---|---|
--policy <PATH> | Policy file (default: assay.yaml) |
--dry-run | Log decisions but do not block |
--verbose | Print decisions to stderr |
--label <LABEL> | Logical server label for identity tracking |
--audit-log <PATH> | Tool-call policy decision summaries in legacy NDJSON (requires --event-source) |
--decision-log <PATH> | Structured assay.tool.decision CloudEvents in NDJSON (requires --event-source) |
--decision-log-new | Require --decision-log to be a new file: refuse an existing path or an alias of another output, and create it with mode 0600 on Unix. Without it, an existing log is appended to. The file stays writable by its owner, so this is not tamper evidence |
--session-context-out <PATH> | One startup-context CloudEvent in a new, separate file (requires --event-source) |
--session-stop-out <PATH> | Experimental separate stop observation after returned execution and bounded cleanup; requires --event-source and a fresh explicit decision log. Missing closure remains unknown; see contract |
--event-source <URI> | CloudEvents source URI, e.g. assay://org/app |
-- <command> [args...] | Wrapped process (required) |
Reading dry-run logs¶
In dry-run mode, a tool call denied by policy is forwarded, and its assay.tool.decision event records data.decision: "allow". The policy result remains in data.reason_code and, when the typed policy path produced one, data.typed_decision. To count calls the policy would have blocked, use --audit-log: it records these calls with decision: "would_deny". A decision log alone does not declare the session's enforcement mode; --session-context-out records enforce or dry_run in the separate startup-context file described below. Malformed client frames are still rejected in dry-run mode, as described above.
Optional startup context¶
assay mcp wrap --policy assay.yaml --event-source assay://org/app \
--decision-log decisions.ndjson --session-context-out context.json \
-- <real-mcp-command> [args...]
context.json records enforce/dry_run and a SHA-256 digest of the JCS-encoded [command, ...args] array. It never adds records to decisions.ndjson. The existing quickstart, coverage, runner and incident decision-log formats remain unchanged.
The context path must be new and distinct from all other wrap outputs. Existing files and final symlinks are refused before spawn; use a fresh path per invocation. The file is created and written after successful spawn, just before the first client frame that is not a server/discover request is handled. An error is returned if output fails. A failed run may leave an empty or partial file; that is not a valid context record. Filesystem paths must remain stable while the command runs. The child may already have executed startup code before a write failure.
A client can probe a stdio server on a disposable process before opening its session, and can end that process with a signal at once, as the MCP TypeScript client's auto version negotiation does. So the one-run outputs (--decision-log-new, --request-receipts-out, --session-context-out, --session-stop-out) are checked before spawn but created only when the run becomes a session: at the first client frame that is not a server/discover request, or when the client closes without sending a frame. A run of only such requests creates none of them and writes no coverage or state window, however it ends, so the session can use the same paths. Until that first frame the checked paths are unoccupied, and a server or another process can take one. The run then refuses it, overwrites nothing, ends with an error, and does not handle or forward that frame; outputs it had already created stay, and the server has already been started. Nothing is removed once the server runs.
The digest covers command-line strings, not executable contents, PATH resolution, working directory, environment or remote identity. Argument plaintext is omitted from this artifact, but hashing does not hide guessable arguments; existing stderr behavior is unchanged. Context is neither tool-execution proof nor a completeness claim, and it does not bind individual decisions in shared logs. tool_pins still cover definitions and labels, not server implementations. See the payload contract.
3) assay mcp preflight (CLI)¶
Check whether assay-mcp-server on PATH matches this CLI and can start with a policy root. This command does not start an MCP session for a host.
Synopsis¶
Options¶
| Option | Description |
|---|---|
--policy-root <PATH> | Directory the host would pass as --policy-root (default: .). Must exist and be a directory. |
--format <terminal\|json> | Report format (default: terminal). JSON is one assay.mcp_preflight.v0 object. |
Exit 0 only for ready. Every other phase exits 2.
Both child probes use a 2-second wall-clock deadline and cap each of stdout and stderr at 8 KiB before materialization. Child output is never copied into the preflight JSON document. On timeout, cleanup kills and reaps only the direct probe child. Descendant or process-tree termination via Unix process groups or Windows Job Objects is not promised.
JSON document (assay.mcp_preflight.v0)¶
--format json prints one object. These fields are always present:
| Field | Meaning |
|---|---|
schema | Always assay.mcp_preflight.v0. |
phase | One of missing, unstartable, wrong_version, invalid_root, startup_refused, startup_timeout, ready. |
message | Stable diagnosis for that phase. |
next_step | Recovery string; empty only for ready. |
expected_version | This CLI's CARGO_PKG_VERSION. |
policy_root | The path that was checked. |
actual_version is present exactly when the identity probe parsed a version token. Identity-time missing and unstartable omit it. The same phases reached after the second spawn retain it, as do wrong_version, invalid_root, startup_refused, startup_timeout, and ready.
ready proves only a matching identity followed by an accepted startup attempt. It does not prove both resolutions selected one immutable executable.
On Windows, the bare assay-mcp-server command resolves an assay-mcp-server.exe on PATH even when .EXE is absent from PATHEXT. The preflight does not expand PATHEXT to run .cmd or .bat shims; a directory containing only one of those shell-discoverable shims is reported as missing.
This is process-level behavior for the preflight's environment. It does not establish that an editor or other GUI host sees the same PATH, add a published Windows MCP-server asset, or define an explicit-script invocation contract.
This is a command-local schema. It is not a ReasonCode, policy verdict, or host-discovery document.
4) assay mcp manifest (CLI)¶
Turn one complete observed MCP tool manifest into an operator-reviewable candidate, then explicitly promote that candidate into a new declared-v0 baseline.
assay mcp manifest candidate \
--from-observed observed.json \
--out candidate.json
# Review or diff candidate.json before running promotion.
assay mcp manifest promote \
--candidate candidate.json \
--source observed.json \
--out declared.json
Candidate export accepts only a complete, unambiguous assay.mcp_manifest_observed.v0 artifact with a reproducible manifest digest and at least one tool. The candidate records the SHA-256 of the exact source bytes and always carries status: candidate plus approval: not_approved.
Promotion rereads the source, reconstructs the candidate, compares every field, and mints a fresh assay.declared_mcp_manifest.v0 document. Byte-identical source at another path is accepted; any source or candidate content drift is refused. Outputs use create-new semantics and never overwrite an existing candidate or approval baseline.
Observed, candidate, and declared files share an inclusive 1 MiB ceiling for both reads and generated output. Exactly 1 MiB is accepted; one additional byte is refused before output creation.
A candidate is not an approval. A successful promotion proves an explicit operator action and byte parity, not that the operator understood the diff, that the observed server was honest, or that any tool is safe.
5) assay-mcp-server (separate binary)¶
Run the MCP server binary directly.
Synopsis¶
Key Options¶
| Option | Description |
|---|---|
--policy-root <PATH> | Policy root directory (default: policies) |
Outer failure contract¶
Tool dispatch failures and timeouts always fail closed. The returned tool payload has allowed: false; the MCP CallToolResult has isError: true. Dispatch failures use E_INTERNAL with Tool execution failed, and timeouts use E_TIMEOUT with Tool execution timed out.
arguments.on_error is not an operator control and is absent from the advertised input schemas. Remove it from clients that followed an older gateway example. settings.on_error remains available to assay run; it is not a server setting.
Unknown methods remain JSON-RPC -32601 with the fixed message Method not found. Unknown tool names currently return the fail-closed CallToolResult; this does not claim JSON-RPC -32602 routing for unknown tools.
Server identity tool¶
assay_server_identity takes no arguments and returns {"name": "assay-mcp-server", "version": "<crate version>"}. That is the same serverInfo object initialize returns, built by one function. Call it first in a session to record which server version answers that session's tools. A call with any argument is refused with E_INVALID_ARGS, and the argument is not echoed. The call takes no policy decision, so it emits no tool_decision record and no metered policy_check usage event, and neither its result (including the refusal) nor its tool_call_done log line carries an allowed field.
The result is the server's own self-report of what it was compiled as. It does not identify the binary file or its path, prove which bytes are loaded, say where the client configuration that started the server came from, or attest the build. It reads no policy file.
Policy-file ingest ceiling¶
The five advertised policy tools (assay_policy_decide, assay_check_args, assay_check_sequence, assay_check_coverage, assay_explain_trace) read local policy files through one inclusive byte ceiling before parse or cache insertion. The default is 1,000,000 bytes. Override it with ASSAY_MCP_MAX_POLICY_BYTES. Exactly the configured size is accepted; one extra byte returns E_LIMIT_EXCEEDED and is not parsed or cached.
This ceiling is independent of ASSAY_MCP_MAX_BYTES, which bounds inbound JSON-RPC messages. It does not limit YAML nesting, alias expansion, proxy startup policy, declared manifests, trust policy, or CLI policy readers.
Operators whose local policy files previously exceeded 1,000,000 bytes must either reduce the file or set an explicit bounded override.
Agent Integration Note¶
For agent-side runtime enforcement, prefer assay mcp wrap so the wrapped MCP process is mediated by Assay policy checks.
See also: - MCP Integration - Self-Correction Guide - Policies