A filter that sits between an LLM agent and its human user, enforcing communication rules before output reaches them.
The name is a wink at the Model Context Protocol, nothing more. MOP is not a protocol in that sense and has nothing to do with MCP beyond the pun. It is a filtering layer: a gate every agent message passes through before it reaches the user.
LLM agents drift. Voice rules in the system prompt get crowded out by task instructions. Reminders in CLAUDE.md decay across long sessions. The result: messages that are too long, too cheerleady, ask permission instead of acting, delegate work back to the user, narrate process instead of stating outcomes.
Stuffing more rules in the system prompt does not fix this — the agent's context is already saturated with the task. MOP moves voice enforcement out of the agent and into a thin layer that inspects each message.
MOP is one core (mop.rules, mop.evaluators, mop.types) with two ways to call it:
mop check— a stateless CLI (the primary, shipped path). Pipe a message in, get a verdict and exit code out. No agent runtime, no MCP server. This is how a host gates outbound text.- In-process MCP gate (
mop/protocol.py,mop/mcp.py) — a stateful adapter exposingsubmit_message/submit_justificationas MCP tools, with a justification loop and failed-open. Parked pending an Agent-SDK host (e.g. Claude Code onclaude_agent_sdk): it is not deterministic-authoritative and no live integration uses it. When one arrives it will be rebuilt as a thin state wrapper aroundmop check, so verdict semantics live in one engine. See ADR-0005 anddocs/mop-enforce-decision.md(Issue 2).
Rules carry one of four detectors:
| Detector | Fires when | Authority |
|---|---|---|
llm |
the evaluator model judges it does (carries a prompt) |
model call |
regex |
a declarative pattern matches (patterns) |
deterministic — authoritative |
script |
an external command exits non-zero (command, message on stdin) |
deterministic — authoritative |
length |
a char/word cap is exceeded (max_chars/max_words) |
deterministic — authoritative |
A single evaluation runs the deterministic rules first (a match is a hard violation the model cannot wave away), then one LLM call judges the llm rules and produces a best-effort rewrite that also repairs the deterministic hits. The deterministic rules are re-checked against the rewrite.
The verdict is derived, never declared by the model — computed from (did the text change? is unresolved empty?):
Accepted— nothing changed, nothing unresolved.Rewritten(rewritten, unresolved)— the evaluator rewrote it;unresolvedlists anything it couldn't fix (the "partial" case).Rejected(unresolved)— nothing was fixable; the residual violations are the agent's to handle.AcceptedFailedOpen(system_note)— MCP-gate-only escape hatch after the justification budget is exhausted.
See docs/adr/ for the decisions behind all of this, and CONTEXT.md for the glossary.
echo "Sounds great, shipping it!" | mop check --json # verdict + exit code
mop check --file draft.md --builtins # include packaged rules
mop check --no-rewrite < draft.md # judge only (CI/lint)
mop rules list --builtins # inspect the resolved rule set- Exit codes:
0accepted,1rewritten,2rejected,3usage/runtime error. - Built-ins are opt-in:
mop checkruns only your local.mop/rules unless you pass--builtins. With no rules at all it warnsno active rules — MOP enforced nothingand accepts (exit 0) — it never imposes defaults or hard-fails a fresh repo. .mop/discovery: walks up from the cwd to the first.gitancestor. Local rules layer over the (opt-in) built-ins — same-name replaces,active: falsesilences.- Observability: set
MOP_AUDIT_LOG=<dir>to append every verdict to a daily-rotated JSONL flight recorder.
See mop/cli.py.
MOP calls litellm in-process and owns its own tier aliases (mop.evaluators.MODEL_ALIASES): small (default) = deepseek/deepseek-v4-flash, with provisional medium/large. Select with --model <tier|provider/model> or MOP_EVALUATOR_MODEL. Structured output degrades gracefully across providers (json_schema → json_object → prompt-only), validated by pydantic. MOP is evaluator-agnostic — a host can inject any callable matching the Evaluator signature.
For the stateful gate, hosts inject an evaluator (built via mop.build_evaluator(rules=...)) and a deliver(text, system_note?) callable for whatever channel they own (Telegram, Slack, web socket). mop.protocol_prompt(rules) returns a system-prompt fragment so the agent knows the protocol exists. See mop/protocol.py and mop/mcp.py.
Rules are validated against a counterexample corpus in evals/ — positive and negative example messages each rule should (or should not) flag. Run uv run python evals/harness.py for the deterministic rules, add --llm (and an API key) for llm rules, or --rule <name> to target one.
Alpha. Core + mop check CLI shipped and tested. First-rule integration into patchbay-relay is in progress (Relay is pi-only now; the earlier in-process cc-sdk-mop harness was retired, so integration is a mop check call in the send path).
HOP — Human Output Protocol. The decoder side: helps humans compose more productive messages to high-context agents from low-bandwidth interfaces (mobile, voice). Together, MOP and HOP form the I/O contract for the agent-human interface.
MIT