v2.27.5 · open source · MIT

Every MCP tool call ends in a verdict.

Hangar is the runtime security and governance layer between your agents and your MCP servers: identity, policy, approvals and an audit record for every call. Runs on a laptop, a VM, or Kubernetes.

Deterministic — no ML. Self-hosted — no SaaS. MIT.

Read a verdict
Four calls and their verdicts
timeverdictcall · reasonaudit record
15:15:02allowgithub.get_issuetenant_id=acme
15:15:04holdgithub.create_issuegate=approval state=pending
15:15:07denygithub.delete_repositorygate=tool_access reason=tool_not_in_access_policy
15:15:09denyfetch.getl7_verdict=deny l7_mode=enforce policy_id=sha256:9f2c41e0
Allowed, held for a person, refused by the tool policy, refused by the egress policy: each with the gate and the code that decided it.

01 · the path

How a call is decided

A tool call passes 13 gates, in an order core fixes, before it reaches your server. The first one that says no is the verdict. Nothing on the verdict path is scored or learned.

  1. CancelledruntimeWas the batch cancelled before this call ran?cancelled
  2. TimeoutruntimeIs the batch's time budget already spent?batch_timeout
  3. RouteruntimeWhich server answers: this one, or a member of its group?server_not_found · no_available_member
  4. Tool accessIs this tool exposed to this caller at all?tool_not_in_access_policy
  5. WithdrawalHas the tool been withdrawn for this tenant?tool_withdrawn
  6. Digest pinIs the tool still the one you approved, byte for byte?digest_mismatch
  7. HealthruntimeIs the server failing often enough to stop sending it calls?circuit_open
  8. ValidatorsDo the interceptor validators pass? They fail closed.validator_denied
  9. Tenant budgetDoes the caller's tenant have a budget, a free slot, and rate left?no_budget · concurrency · rate
  10. ApprovalDoes a person have to say yes first? The call holds until they do.approval_denied · approval_timeout
  11. Cold startruntimeStart a stopped server once, however many calls are waiting.start_failed
  12. Pin, after startThe pin check a cold start made possible.digest_mismatch
  13. CancelledruntimeWas the batch cancelled while the server started?cancelled

then, and not gates

  • Rate limitHas this caller spent its command-bus budget?RateLimitExceeded
  • Egress policy (L7)Inside the invocation, just before any outbound I/O: may this call, with these arguments and headers, leave?l7_verdict=deny · l7_verdict=require_approval
  • Response sizeIs the result over the cap? It is cut, and the cut is marked, with a continuation to fetch the rest.truncated

Generated from _GATES in core v2.24.0. The path with the cluster plane around it is on the request path.

this config

config.yaml
mcp_servers:
  github:
    tools:
      allow_list: ["get_*", "list_*"]
      approval_list: ["create_issue"]

produces this record

What the config above decides, call by call
timeverdictcall · reason
15:15:02allowgithub.get_issuetenant_id=acme
15:15:03allowgithub.list_issuestenant_id=acme
15:15:04holdgithub.create_issuegate=approval state=pending
15:15:07denygithub.delete_repositorygate=tool_access reason=tool_not_in_access_policy
15:15:11denygithub.list_issuesgate=tenant_budget reason=rate
Reads pass, the write waits for a person, anything else is never exposed, and a tenant over its rate is refused before anyone is asked.

02 · the record

What you can prove afterwards

Every call is written down with the caller attached — allowed and denied alike. A verdict nobody can attribute proves nothing, so identity travels with the record rather than being reconstructed afterwards.

Audit trail
Each allow, deny and hold, with the caller, the tenant, the gate and the bounded reason. Codes only, never the text a gate was handed.
SIEM export
CEF, LEEF 2.0, JSON lines and RFC 5424 syslog, the same refusal fields in all four.
Traces
OTLP spans per call, a hangar.gate.decision event per gate, and hangar.refusal.gate on the call that stopped.

the refused delete_repository, as your SIEM receives it

LEEF 2.0

LEEF:2.0|MCP Hangar|MCP Hangar|0.15.0|103|	devTime=Oct 05 2026 15:15:07	proto=tool	usrName=svc:ci	sessID=sess-4f1a	tenantID=acme	action=delete_repository	duration=0.41	gate=tool_access	gateReason=tool_not_in_access_policy	src=github	routeBackend=github

JSON lines

{"timestamp": "2026-10-05T15:15:07.204000+00:00", "event_type": "ToolInvocationDenied", "provider_id": "github", "route_backend": "github", "tool_name": "delete_repository", "status": "denied", "duration_ms": 0.41, "user_id": "svc:ci", "session_id": "sess-4f1a", "tenant_id": "acme", "gate": "tool_access", "gate_reason": "tool_not_in_access_policy"}
Written by core v2.24.0's own audit handler and exporters from one ToolCallRefused event, not typed for this page. Configure the exporters.