learntutorialcore5 minsince 2.19.0
Export MCP tool calls to your SIEM (CEF, LEEF, syslog, OTLP)
Updated October 14, 2026
The problem
The tool calls an agent makes are the part of its behaviour you can actually audit, and they pass through one place. A security team wants them where it already looks: in the SIEM, in a format the SIEM parses, with a refusal recorded as a refusal and not as a gap in the log.
The record
One refused call, as each exporter writes it. The call is the one from
the rug-pull walkthrough: the
tool’s description changed after it was pinned. Captured from mcp-hangar
2.27.3.
CEF:
CEF:0|MCP Hangar|MCP Hangar|2.27.3|103|Tool Invocation Denied|5|rt=1791926901180 dvchost=mcp-hangar act=ToolInvocationDenied cs1=demo cs1Label=ProviderID suser=local-user cs6=local cs6Label=TenantID flexString1=demo flexString1Label=RouteBackend cs5=echo cs5Label=ToolName cn1=2.08 cn1Label=DurationMs gate=digest_pin gateReason=digest_mismatch
LEEF 2.0 (fields are tab-separated):
LEEF:2.0|MCP Hangar|MCP Hangar|2.27.3|103| devTime=Oct 13 2026 21:28:28 proto=tool usrName=local-user tenantID=local action=echo duration=2.17 gate=digest_pin gateReason=digest_mismatch src=demo routeBackend=demo
RFC 5424 syslog:
<132>1 2026-10-13T21:28:35.641693Z host mcp-hangar 90512 103 [mcp@49152 provider="demo" routeBackend="demo" tool="echo" status="denied" duration="2.1" user="local-user" tenant="local" gate="digest_pin" gateReason="digest_mismatch"] Tool echo on provider demo denied
JSON lines:
{"timestamp": "2026-10-13T21:28:42.856724+00:00", "event_type": "ToolInvocationDenied", "provider_id": "demo", "route_backend": "demo", "tool_name": "echo", "status": "denied", "duration_ms": 2.13, "user_id": "local-user", "tenant_id": "local", "gate": "digest_pin", "gate_reason": "digest_mismatch"}
OTLP, as the collector’s debug exporter prints the log record:
Body: Str(tool_invocation)
-> mcp.server.id: Str(demo)
-> gen_ai.tool.name: Str(echo)
-> mcp.tool.status: Str(denied)
-> mcp.caller.id: Str(local-user)
-> mcp.caller.tenant_id: Str(local)
-> hangar.gate.name: Str(digest_pin)
-> hangar.gate.reason: Str(digest_mismatch)
Trace ID: 7dee0ec07e9b74c74f4c74234651b130
The same call that succeeds writes a Tool Invocation Completed record
(signature 101), and a server changing state writes one with the transition,
so a refusal sits in the log between the calls that were allowed.
The config that does it
The file formats are chosen with two environment variables:
MCP_COMPLIANCE_FORMAT=cef # cef, leef, syslog or jsonlines
MCP_COMPLIANCE_OUTPUT=/var/log/mcp-hangar/audit.cef
mcp-hangar serve --http
Set MCP_COMPLIANCE_FORMAT and leave MCP_COMPLIANCE_OUTPUT out, and the lines
go to standard error. An unknown format name refuses startup; before 2.25.0 it
logged a warning and served calls with no export.
OTLP goes to a collector you name, as logs:
observability:
audit:
enabled: true
OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4317 mcp-hangar serve --http
Audit records are sent only to an endpoint you set. Hangar never falls back to a default one.
What it does not do
- The CEF, LEEF, syslog and JSON exporters write a file or standard error.
They do not send anything over the network. A forwarder you already run (your
SIEM’s agent,
rsyslog, a log shipper) reads the file. Only OTLP is pushed. - Setting only
MCP_COMPLIANCE_OUTPUTdoes nothing. Without a format the variable is ignored, silently (#1723). - The refusal reasons are not in these records. A record carries the gate and
its reason code (
digest_pin,digest_mismatch), the tool, the server and the caller. The free-text reasons of an egress denial are on the event and in the gateway log, not on the audit line. - Caller type and cost fields are not written yet. The exporters accept them and do not emit them (#1705). The OTLP record does carry caller type, id and tenant.
- Before 2.27.3, CEF and LEEF headers said
0.15.0as the device version, and two CEF state changes of one server were identical lines. - A record shows what Hangar decided, not that the traffic stopped. For the network side, see egress control on Kubernetes.