Skip to content

Repository files navigation

MCP Visor

Deterministic authorization for MCP tool calls.

MCP Visor is a self-hosted policy enforcement proxy for AI agents. It evaluates valid JSON-RPC tools/call requests that include an id before relay and applies allow, deny, approval, redaction, chain, and session-taint rules without an LLM. Notification-form tools/call (no id), duplicate method keys, and JSON-RPC batches containing tools/call are blocked at the proxy; recognizable malformed tools/call attempts with an id fail closed without relay.

MCP Visor is not a model guardrail. It is an action boundary. Models can request actions. MCP Visor decides whether those actions are allowed.

Model guardrails try to shape what the model says or thinks inside the context window. MCP Visor controls what the agent is allowed to do after a tool call is requested. System prompts can describe intended behavior; MCP Visor enforces policy outside the model.

Go Version License CI Release

Paper: MCP Visor: Deterministic Runtime Enforcement · Writing: Spec engineering · Runtime policy enforcement


The Visor Trust Plane research program

MCP Visor is the production enforcement layer of a broader research program on governing autonomous agents at the boundary.

Repo Question it answers Status
mcp-visor What may an agent do? (runtime policy at the MCP tools/call boundary) Production
agent-identity-plane Who is acting, for whom, through which chain? (identity + provenance) v0.1.0
authority-graph-simulator What authority can an agent reach? (counterfactual delegation analysis) Prototype
capability-delta-receipts What capability can an agent acquire? (trajectory-level capability accounting) Prototype

This binary is the tools/call action boundary. Agent Identity Plane authenticates callers and carries actor-chain provenance via a visor-gateway; it is not yet an in-proxy token gate. The two prototypes are standalone proofs. Capability-delta semantics also exist here as an opt-in evaluator (-capability-eval or settings.capability_accounting); the authority-graph is not in the proxy.

The Trust Plane governs not just what authority an agent is given, but what new authority its discoveries make possible — authorization must be re-evaluated when an agent materially increases its effective capabilities, even when nominal permissions are unchanged.


Why MCP Visor exists

AI agents can now read files, call APIs, run commands, query databases, and modify infrastructure through MCP tools. Prompt-only controls are useful guidance, but they are not an execution boundary.

MCP Visor adds that boundary at the MCP tools/call layer:

AI agent → MCP Visor → policy decision → MCP server

Valid tools/call requests with IDs are evaluated before relay. Notification-form tools/call is dropped without response, including if sent in the post-initialize handshake slot. Duplicate method keys and JSON-RPC batches containing tools/call are blocked before relay. Non-tools MCP notifications and batches still forward unchanged.

Install

go install github.com/themayursinha/mcp-visor/cmd/mcp-visor@latest

Pre-built binaries and checksums are available on the Releases page.

Quick start

# Run the built-in demo proxy
mcp-visor serve --demo

# Supplemental lint. Do not combine --strict with --no-warnings.
# This is not yet a complete fail-closed policy gate; see the threat model.
mcp-visor lint --strict examples/policies/session-taint-egress.yaml

# Proxy a real MCP server through a policy boundary.
# Allows fail closed unless -audit-log points at a writable JSONL file.
mcp-visor serve -server <your-mcp-server> -policy policy.yaml -audit-log ./audit.jsonl

Two-minute action-boundary demo: go run ./examples/demo-runner

Monitor vs delayed detection (same http_post to evil.example; unmediated server observes it, Visor denies before relay): go run ./examples/monitor-vs-proof

Local Proof Console (not a product dashboard): go run ./examples/demo-runner -ui then open the printed URL. Default bind is loopback. To reach it from another machine on a tailnet, bind a CGNAT address in 100.64.0.0/10 (not 0.0.0.0):

go run ./examples/demo-runner -ui -ui-addr "${TAILSCALE_BIND_ADDRESS}:9092"

What it protects against

  • Secret reads followed by outbound exfiltration
  • Access to configured sensitive paths such as /project/.env, SSH keys, kubeconfigs, credentials, and key files; basename-only and policy-uri gaps are documented in the threat model
  • Unsafe shell commands and command-injection patterns
  • Unknown or newly introduced tools under default-deny policy
  • High-risk actions without human approval
  • Tool sequences that look safe individually but become dangerous together
  • Tool-output-induced recipient expansion: allow_recipient is an exact, fail-closed mailbox slot. Session history does not promote a denied recipient into authority.
  • PATH→SHELL amplification: require_path_literal denies path-class arguments that contain shell grammar before relay. Schema-valid, tool-authorized calls still fail closed when a path would interpolate as a command fragment.
  • Cross-principal writes: allow_resource_owner is an exact, fail-closed principal slot. A mandate to act for alice does not authorize cancelling bob's reservation. Session history does not promote a denied owner into authority.
  • Destructive path outside mandate: allow_path_slot denies PATH/TARGET-class arguments that are not under the mandate glob. A schema-valid cleanup of /tmp that names $HOME is denied before relay.
  • Authority-expanding destination: allow_destination is an exact, fail-closed host slot. A mandate to reach docs.internal does not authorize http_post or web_fetch to evil.example. Session history does not promote a denied host into authority.
  • Untrusted execution environment: allow_working_directory denies CWD-class arguments that are not under the mandate glob. A schema-valid decoder run whose cwd is an attacker extract directory is denied before relay.
  • Untrusted credential custody: deny_secret denies SECRET-class arguments before relay. A schema-valid configure_secret whose api_key is a valid replacement key is still denied. Credential validity is not custody.
  • Authority-expanding application: allow_application is an exact, fail-closed application slot. A mandate to sync staging-orders does not authorize argocd_sync of production-payments. A tool-provider token is not caller authority.
  • Unauthorized skill promotion: allow_skill is an exact, fail-closed skill-identity slot. A mandate to install workspace-lint does not authorize install_skill of attacker-registry. Experience cannot manufacture a new skill name.
  • Permission-bypass delegation: deny_permission_bypass is a fail-closed spawn-flag slot. A mandate to create a worker does not authorize spawn_agent with skip-permissions. Explicitly-off values are not a bypass. Delegation graphs are out of model.
  • Unauthorized configuration activation: allow_activation is an exact, fail-closed executable-or-host slot. A mandate to register /usr/bin/node or mcp.internal does not authorize register_mcp of /bin/sh or 169.254.169.254. Describing infrastructure is not instantiating it.

Session-taint egress control

MCP Visor tracks session state, not just individual calls.

file_read("/customer-secrets/tokens.csv")
  → session tainted: sensitive_file_accessed
  → http_post(...) denied by block_sensitive_egress
  → audit log records source, taint, policy rule, sink, and decision

Example policy:

taints:
  - name: "sensitive_file_accessed"
    source_tools: ["file_read"]
    source_patterns: ["**/customer-secrets/**"]

egress_controls:
  - name: "block_sensitive_egress"
    when_tainted: "sensitive_file_accessed"
    sink_tools: ["http_post", "slack_send_message"]
    action: deny

Full example: examples/policies/session-taint-egress.yaml

Policy example

version: "1.0"
default_action: deny

servers:
  - name: "filesystem"
    allowed: true
    tools:
      - name: "file_read"
        allowed: true
        rules:
          - type: deny_path
            patterns: ["**/.env", "**/*.pem", "/etc/passwd"]
          - type: allow_path
            patterns: ["/home/**", "/tmp/**"]

  - name: "shell_exec"
    allowed: true
    risk: critical
    approval_required: true
    rules:
      - type: deny_command_pattern
        patterns: ["bash\\s+-i\\s+>&", "rm\\s+-rf\\s+/"]

tool_chains:
  - name: "prevent_exfiltration"
    sources:
      - server: "*"
        tool_pattern: "file_read"
    sinks:
      - server: "*"
        tool_pattern: "(http_post|slack_send_message)"
    action: deny
    within_calls: 3

More policies: examples/policies/ · Interop policies: examples/policies/interop/ · Schema reference: docs/policy-model.md

Core capabilities

Capability Purpose
Default-deny policy Unknown servers and tools fail closed
Argument rules Restrict paths, commands, queries, recipients, sizes, and repos
Pattern redaction Replace configured matches in arguments and textual Content[].Text output; this is not full structured-payload or complete private-key sanitization
Tool-chain detection Block dangerous sequences such as read → exfiltrate
Session taints Change later authorization decisions after sensitive context is touched
Human approval Gate critical tools before execution
Durable allow commit Terminal allows are appended and fsync'd to the JSONL sink before relay (H19)
Stdio identity pin Optional stdio_invocation_sha256_v1 binds a local invocation; registry runners are unpinnable
Audit log Hash-linked JSONL; allows are a durable commit; denies and other selected events are written without the same fsync
Policy linting Validate YAML policy before deployment

Advanced capabilities include signed decision receipts, Vault Transit signing, webhooks, and experimental remote transport, SIEM, metrics/OTLP, and local dashboard surfaces. The --trace formatters are not yet connected to runtime message paths. See docs/complexity-budget.md and docs/threat-model.md.

Security model

  • Deterministic: no LLM in the allow/deny path
  • Fail closed: unknown tools are denied by default; a missing or non-durable -audit-log denies every allow
  • Layered: optional stdio identity → redaction → policy → taint-aware egress → chain detection → approval → durable allow commit → post-allow taint marking → relay
  • Observable: every terminal allow is a standalone JSONL tool_call_allowed record, fully appended and Sync()'d before relay. Denies, approvals-required, taints, and session events are also JSONL but are not equivalently fsync'd. The hash chain recovers when reopening a healthy file; incomplete or corrupt tails fail closed. This is not a signed repudiation control, and it does not survive an untrusted audit directory.
  • Self-hosted: single Go binary; no SaaS dependency required
  • Operator-controlled: optional telemetry exports to your Prometheus, OTLP, webhook, or SIEM stack
  • Not a host sandbox: Visor authorizes MCP tools/call. It does not confine arbitrary filesystem, network, or subprocesses.

CLI

mcp-visor serve [flags]    Run the proxy
mcp-visor lint [--strict] <policy>  Supplemental policy validation; not a complete fail-closed gate
mcp-visor version          Print version

Common flags: -server, -policy, -audit-log, -approval-dir, -approval-cli, -demo

Advanced flags: -server-url, -webhook-url, -siem-target, -vault-addr, -metrics-addr, -otel-endpoint, -dashboard, -trace, -capability-eval

Full reference: mcp-visor serve -h

Documentation

Architecture · Policy model · Threat model · Complexity budget · Interoperability · Qwen-MM-Plugins demo · Harness

Development

go build ./cmd/mcp-visor/      # build
go test ./...                  # test
harness/check.sh               # fmt + vet + test + evidence manifest
make bench                     # benchmarks

Roadmap

  • v1.0: Proxy, policy engine, redaction, approval, audit, chain detection
  • v1.1: Identity/time policies, partial engine hot-reload, CLI approval, experimental remote transport
  • v1.2: Session taints and egress controls
  • v1.3: Documentation truth, security verification, interoperability evidence, and release hardening
  • v1.4: Durable allow-commit before relay, optional stdio identity attestation, Qwen third-party demo, documentation truth
  • Future: sandboxing, richer telemetry, optional policy engines — only if deployment evidence demands them

Contributing

See CONTRIBUTING.md. Run harness/check.sh before PRs.

License

MIT — see LICENSE

About

Runtime policy enforcement and audit control plane for MCP tool execution. Deterministic, non-AI policy engine that intercepts MCP tools/call requests before execution.

Topics

Resources

Contributing

Security policy

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages