— The WOWHOW Least-Privilege MCP Governance Layer (LPML) is a reference design that constrains MCP tool dispatch to only the authority each call actually needs. It layers four components — static allow-lists, per-call scope resolution, a tamper-evident audit trail, and rollback hooks — over any MCP server with no changes to existing tool handlers. The biggest payoff: runaway agent loops and prompt-injected exfiltration become detectable and stoppable before they complete.
Every MCP tool your agent calls runs with the same ambient authority as the process that spawned it — unless you explicitly constrain it. That is the default behavior of the Model Context Protocol as shipped, and it is a security posture that most teams have not thought through. A filesystem read tool, a database write tool, and a cloud deployment tool sit side by side in the same server context, all equally callable, all equally trusted. The WOWHOW Least-Privilege MCP Governance Layer (LPML, pronounced “lempel”) is an original reference design for layering auditable, scope-limited control over MCP tool dispatch. It introduces four components: a static tool allow-list with per-tool capability scopes, a dynamic per-call scope resolver, a tamper-evident audit trail schema, and deterministic rollback hooks. Together they bring MCP deployments closer to the principle of minimal necessary authority per operation.
This article documents the framework architecture, the governance rubric, and concrete implementation patterns. No third-party product is required — all components can be implemented with standard Node.js or TypeScript in front of any MCP server.
Why MCP’s Default Authority Model Is a Problem
MCP tools are registered with a name, an input schema, and a handler. When a model requests a tool call, the MCP server dispatches it. There is no built-in concept of “this tool is allowed to touch only these paths” or “this tool requires explicit human approval before writing”. The protocol leaves all of that to the implementer.
Three failure modes come up repeatedly in production agentic systems:
- Scope bleed: A tool designed to read files is implemented to accept arbitrary paths. The model, reasoning about a task, constructs a path to
/etc/passwdor a cloud credentials file. The tool executes because nothing in the dispatch layer blocked it. - Write-without-witness: A database mutation tool runs, produces a side effect, and the only record is whatever the model logged to its own context. There is no independent audit trail. If something breaks three steps later, reconstruction is guesswork.
- Silent rollback impossibility: A deployment tool runs, fails mid-way, leaves infrastructure in a partial state. There is no rollback hook wired to the governance layer, so recovery requires manual intervention.
None of these are MCP bugs. They are architecture gaps that the framework does not address by design, leaving them to implementers. The LPML framework fills those gaps.
The LPML Framework: Four Components
Component 1 — Static Tool Allow-List with Capability Scopes
The allow-list is a configuration artifact, not code. It lives in a versioned file (JSON or YAML) that is committed alongside the MCP server definition. Each entry maps a tool name to a set of capability scopes. Scopes are strings from a fixed vocabulary — the vocabulary is the first governance decision your team makes.
A minimal LPML capability vocabulary has five primitive scope types:
- READ: tool may read data from a resource; no writes permitted
- WRITE: tool may write or mutate data
- EXECUTE: tool may trigger a process, subprocess, or shell command
- NETWORK: tool may make outbound network requests
- ESCALATE: tool may request human approval before proceeding
Scopes are additive and explicit. A tool not present in the allow-list is automatically blocked — deny-by-default, not allow-by-default. A tool present in the list but missing a scope cannot perform operations requiring that scope.
Example allow-list entry in JSON (no backticks in this representation):
{
"tools": {
"read_file": {
"scopes": ["READ"],
"path_constraint": "^/workspace/",
"max_bytes": 1048576
},
"write_file": {
"scopes": ["READ", "WRITE", "ESCALATE"],
"path_constraint": "^/workspace/outputs/",
"require_approval_for_paths": ["^/workspace/outputs/config"]
},
"run_shell": {
"scopes": ["EXECUTE"],
"command_allowlist": ["npm run build", "npm test", "git status"],
"blocked": true,
"block_reason": "EXECUTE tools require security review before enabling"
},
"fetch_url": {
"scopes": ["NETWORK", "READ"],
"domain_allowlist": ["api.github.com", "registry.npmjs.org"]
}
}
}
The blocked: true pattern is deliberate. It lets you declare a tool’s existence and intended scopes in the allow-list without enabling it, so the governance record is complete even for tools under review.
Component 2 — Dynamic Per-Call Scope Resolver
The static allow-list defines the maximum possible scopes for each tool. The per-call scope resolver narrows those scopes at dispatch time based on the calling context. Calling context is the combination of: the session identity, the current task description, and the accumulated call history in this session.
The resolver sits between the MCP protocol handler and the tool dispatch logic. It receives the raw tool-call request, looks up the tool in the allow-list, then applies three resolver rules in order:
- Session floor rule: If the session was started with restricted scopes (e.g., a read-only audit session), no call in this session can exceed those scopes regardless of what the allow-list permits.
- Task-context rule: If the active task description does not semantically match the tool’s declared purpose, flag the call for human review rather than blocking it outright. This is the “suspicious but not definitely wrong” case.
- Call-history escalation rule: If the same tool has been called N times within a session window (configurable, default 10 calls per 5 minutes for WRITE-scoped tools), require human approval before the next call. This catches runaway agent loops.
The resolver emits a ScopeResolution object: the resolved scopes, the rule that fired (if any), the disposition (ALLOW, BLOCK, ESCALATE), and a trace ID. That object goes directly into the audit trail before the tool runs.
Component 3 — Tamper-Evident Audit Trail Schema
The audit trail is append-only. Each entry is a structured record written to a log sink before the tool executes (pre-record) and after it completes (post-record). The pre-record creates a commitment; the post-record closes it. If a post-record is missing for a pre-record, the gap is itself evidence of an incomplete or interrupted operation.
LPML Audit Record Schema:
| Field | Type | Phase | Description |
|---|---|---|---|
trace_id |
UUID v4 | Pre + Post | Links pre and post records; also appears in resolver output |
session_id |
string | Pre + Post | Identifies the agent session; opaque to the tool itself |
tool_name |
string | Pre + Post | Exact name from the allow-list; never user-supplied |
resolved_scopes |
string[] | Pre only | Scopes actually granted for this call after resolver |
disposition |
ALLOW/BLOCK/ESCALATE | Pre only | Resolver decision |
input_hash |
SHA-256 hex | Pre only | Hash of the JSON-serialized tool input; detects tampering |
input_summary |
string | Pre only | Human-readable summary (first 256 chars of input), not the full payload |
ts_pre |
ISO 8601 | Pre only | Timestamp before tool execution |
outcome |
SUCCESS/ERROR/TIMEOUT | Post only | Tool execution result |
error_code |
string or null | Post only | Structured error code if outcome is ERROR |
output_hash |
SHA-256 hex | Post only | Hash of the JSON-serialized tool output |
duration_ms |
number | Post only | Wall-clock execution time in milliseconds |
ts_post |
ISO 8601 | Post only | Timestamp after tool execution completes or fails |
rollback_id |
string or null | Post only | Non-null if a rollback hook is registered for this call; references the hook’s artifact ID |
Tamper-evidence comes from three properties: the hash chain, the append-only sink, and the pre/post pairing. The input_hash and output_hash fields make it possible to verify, after the fact, that a specific input produced a specific output. If your log sink supports hash-chain anchoring (e.g., writing each entry’s hash as the prev_hash of the next entry), you get a lightweight ledger that detects insertion or modification.
In practice, writing to an append-only structured log (Cloud Logging, Loki, or even a file opened with O_APPEND) plus shipping records to an S3-compatible bucket with Object Lock enabled is sufficient for most production threat models. You do not need a blockchain.
Component 4 — Rollback Hooks
A rollback hook is a function registered alongside a tool definition that the governance layer can invoke to undo the tool’s side effects. Not every tool can have a rollback hook — some operations are genuinely irreversible. The framework requires you to be explicit about that.
LPML defines three rollback statuses for every WRITE-scoped or EXECUTE-scoped tool:
- REVERSIBLE: A rollback hook is registered and has been tested. The audit record gets a
rollback_idpointing to the saved artifact (snapshot, backup reference, inverse operation descriptor). - PARTIAL: The tool can be partially reversed. The rollback hook undoes what it can and documents what it cannot. Audit record includes a
partial_rollback_manifest. - IRREVERSIBLE: No rollback is possible. Calling this tool requires
ESCALATEscope, meaning human approval is mandatory before dispatch. The audit record includes airreversible_ackfield that must be populated by the approver.
Rollback hooks are invoked by session ID, not by individual call. When a session is flagged for rollback (manually by an operator, or automatically when an anomaly detector fires), the governance layer replays the session’s audit trail in reverse order, calling each registered rollback hook with the saved artifact. PARTIAL tools emit warnings. IRREVERSIBLE tools emit hard stops that require operator confirmation before the rollback sequence continues past them.
Comments · 0
Beta: comments are stored locally on your device and not visible to other readers.
No comments yet. Be the first to share your thoughts.