Authorization decides whether a tool call is allowed. Audit decides whether anyone can explain it afterwards. In a vendor security review those are separate questions, and the second one is where otherwise sound MCP servers run out of answers.
The usual state of a first server: an HTTP access log with a method, a path, a status code, and a latency. Every MCP tool call is a POST to the same path, so the log says a request happened and nothing else. It cannot answer who, on whose behalf, through which client, against which tenant's data, and with what result. This is a guide to the event that can.
What the reviewer is actually asking
The question behind the questionnaire item is an incident question. An employee's agent did something the customer did not expect. The customer's security team wants to reconstruct it from their own tooling, not from a support ticket to you. So they will ask, in some wording:
- Can we get per-action logs for our tenant, in our SIEM, without asking you?
- Do the records name the human, not just the agent or the API client?
- Do you record denied and failed attempts, or only successes?
- How long are they retained, and can they be tampered with?
A server that answers those four with a schema, a sample, and a retention number moves through that section of the review quickly. A server that answers "we have logging" does not.
Design the event before the first tool
The event shape is a constraint on the server's internals, which is why it belongs in the design week rather than the hardening week. If the handler for search_invoices does not carry the caller's identity down to the data layer, no logging library will add it later. Deciding the fields first forces the identity to flow.
A workable minimum for one tool call:
Envelope
event_id— unique per event, for deduplicationevent_time— RFC 3339, UTC, with at least millisecond precisionschema_version— you will change this schema; say which one a record usesevent_type— for examplemcp.tool.call,mcp.tool.denied,mcp.session.start,mcp.auth.failed
Subject
user.id— the stable subject identifier from the validated access token, not a display nameuser.emailoruser.external_id— whatever the customer's SIEM correlates on, if you are permitted to hold ittenant.id— the tenant the call executed against, taken from the token contextclient.id— the registered OAuth client, so the customer can tell one agent host from anotherclient.nameandclient.version— from the MCP initialize handshake, useful and not trustworthy; treat as self-reported
Action
tool.nameandtool.versiontool.arguments_summary— redacted, bounded, structured; see belowscopes— the scopes present on the token at the time of the callauthz.decision—allowordeny, withauthz.reasonon a deny
Outcome
outcome—success,denied,errorerror.code— your code, stable across versionsrecords_affectedorrecords_returned— a count, not the dataduration_msdownstream— the systems the call touched, so a customer can trace a change into their own systems
Correlation
session.id— the MCP sessiontrace_idandspan_id— if you emit traces, use the same identifiers so an engineer can move between the audit trail and the telemetryrequest_id— returned to the caller in an error, so a support ticket maps to an event
That is roughly twenty fields. It is enough to reconstruct an incident and small enough that nobody argues about it.
Log the denials
Successes show what the product does. Denials show what someone tried. An expired token, a tool call outside the granted scopes, a cross-tenant identifier that the server rejected, an argument that failed validation — each of those is a record a security team will want, and each is the kind of record that is missing when a server logs only at the point of a successful response.
Emit an event for every authorization failure at the transport boundary as well: rejected audience, unknown client, missing token. Those events are what let a customer notice a credential being used from somewhere it should not be.
Arguments: summarize, do not copy
Tool arguments are user content and sometimes third-party content. Copying them wholesale into a log turns your audit pipeline into a second copy of customer data with a different retention policy, which reviewers will notice.
A usable rule: record the shape, not the payload. Argument names present, types, lengths, and enumerated values in full; free text hashed or truncated to a short prefix; identifiers recorded because they are the point; anything classified as a secret, a credential, or regulated data replaced with a redaction marker. Redact by allowlist, so a new tool argument is redacted until someone decides otherwise. Never log the access token, and never log a refresh token or an authorization code, including in error paths where a framework may serialize the whole request.
Outputs get the same treatment. records_returned: 42 is auditable. Forty-two invoices in a log line are a data-handling finding.
Use a format that already exists
Inventing an event format costs your customers an ingestion project, and an ingestion project is a reason to defer your server.
The practical options, in rough order of how often enterprise buyers ask for them:
- Newline-delimited JSON with a documented schema. The lowest common denominator, and adequate if the field names are stable and documented.
- OCSF, the Open Cybersecurity Schema Framework. Map tool calls to an activity class and keep your MCP-specific fields in the extension object. Several SIEM vendors ingest it natively.
- CloudEvents as the envelope when the events go over a message bus rather than a file or an HTTP endpoint.
- RFC 8417 security event tokens when the requirement is signed, verifiable events, usually alongside RFC 8935 or RFC 8936 delivery. This comes up with identity-heavy buyers who already consume shared signals from their identity provider.
For engineering telemetry, OpenTelemetry is the right pipe and its GenAI semantic conventions are worth following for the model-facing spans. Keep the two separate in your head: traces are for your engineers and are sampled; audit events are for the customer and are not.
Getting the events to the customer
A schema the customer cannot reach is a document, not a control. Pick at least one delivery path and write it down:
- A per-tenant export endpoint, paginated, filtered by time range, authenticated with the customer's own credential
- Delivery to an object store bucket the customer owns, written on a schedule
- A streaming subscription to a bus, for customers with the appetite
Whatever the path, scope it to the tenant, log the export itself, and state the retention period in the same document. Say what you keep, for how long, and whether the store is append-only. "Retained ninety days, write-once, exported hourly to a bucket you own" is an answer. "Logs are retained per policy" is not.
Prove it works
Before handover, test the audit trail the way you test the authorization. Take one tool call and follow it end to end: does an event appear, with the right subject, in the export, within the stated latency? Now do the negative cases. Revoke a token mid-session and confirm the rejection is recorded. Call a tool outside your scopes and confirm the deny event names the missing scope. Attempt a cross-tenant read and confirm the record lands in the target tenant's stream, not only the caller's.
Then keep a sample. A short, redacted set of real event records, attached to the security questionnaire, ends this section of a review faster than any amount of prose about your logging practices.
Where this fits
Audit is one of the areas we assess in an MCP security audit, and a structured event stream is a standard deliverable on an MCP server build. Where several servers sit behind one gateway, the audit design moves up to the platform, so a customer gets one stream rather than one per server. If you already have events with a subject, a tenant, a tool, and a decision on every call, you are in better shape than most servers we see, and you probably do not need us for this part.