Instrumenting an MCP Server: Audit Logging, Activation Funnels, and Per-User Attribution
By CorpusIQ LLC
Every MCP server that ships into production eventually hits the same question. Who called what tool, when, with what parameters, producing what result?
On a demo, nobody asks. On day one of a real customer deployment, your procurement review asks. By month three, your own product team asks because they want to know which tools users actually call and which have been untouched since launch. By month twelve, your auditor asks.
If your server was not instrumented from the start, the answers are guesses. Here is the architecture we built at CorpusIQ to answer those questions, and the reasoning behind each decision.
The two events that matter
Most MCP deployments over-instrument by logging every HTTP request, or under-instrument by logging nothing structured. Both fail. The right baseline is exactly two event types.
user_authenticated. Emitted once per session, when a user's JWT is validated and a session begins. Contains the user's email, the OAuth subject ID, the authentication method (JWT, API key, delegated OAuth), and the session identifier.
tool_called. Emitted on every MCP tool invocation. Contains the user's OAuth subject ID, the tool name, the session ID the call belongs to, a unique request ID, and a timestamp.
Two events. That is it. Everything else derives from these.
Why so narrow? Because the alternative is log bloat. Teams that log HTTP requests, database queries, LLM inferences, and tool calls in the same stream end up with telemetry they cannot query. Two clean event types with consistent schemas are worth a hundred fields of unstructured logs.
Why per-user attribution is non-negotiable
The common anti-pattern: one service account behind the MCP server, authenticating on behalf of all users.
This is easier to ship. It also fails every regulated audit. SOC 2 CC6.1 wants attributable access. CMMC AC.L2-3.5.1 wants unique identifiers per user. HIPAA 164.312(a)(2)(i) wants unique user identification. Every framework has a variant of the same control: if you cannot prove which human triggered an action, the control fails.
Service account architectures force you to rebuild authentication later, under audit pressure, with existing customer data to migrate. Per-user OAuth from day one is cheaper than per-user OAuth retrofitted in month eighteen.
The implementation: each user authenticates with their own OAuth token. The MCP server validates the JWT, extracts the sub claim (the OAuth subject, a stable user identifier), and attaches it to every downstream tool call. Logs record the sub, not a shared service account. When compliance asks who called get_quickbooks_profit_loss on March 14 at 2:47 PM, you can answer with a specific human identity.
The schema that survives
Across Azure Container Apps, AWS ECS, and GCP Cloud Run, the same structured logging schema works.
2026-04-16 21:23:36,725 - AUDIT - INFO - AUDIT event=tool_called
user=745ed9a8-0028-443f-aac9-799d061cee6b
tool=list_drive_files
session=vDVvl_yWsG-Picw1OaZupKn8iXUVW8HQv5JNYY5IKfw
request_id=1
2026-04-16 21:23:36,725 - AUDIT - INFO - AUDIT event=user_authenticated
user=ted.m@corpusiq.io
sub=745ed9a8-0028-443f-aac9-799d061cee6b
method=jwtThree design choices embedded in that format.
ISO 8601 timestamp, not epoch. Queryable by humans and machines. Joins cleanly across log sources.
AUDIT prefix in the log message. Makes filtering trivial. One KQL or CloudWatch Insights predicate isolates audit events from application noise.
sub in tool_called, email in user_authenticated. The UUID is stable even if the user changes their email. The join between events lets you report by email when you need human-readable output, and by sub when you need stability. Do not emit email on every tool call. It bloats logs and leaks PII into every downstream consumer.
The activation funnel this unlocks
Once user_authenticated and tool_called exist, the product analytics fall out for free.
Time to first tool call. Min(tool_called timestamp) minus min(user_authenticated timestamp), grouped by sub. Segment by signup cohort. This is your core activation metric.
Session depth. Count of tool_called events grouped by session. One-tool sessions are exploratory. Six-plus tool sessions are workflow usage. The shift from one to six is where product-market fit shows up.
Tool usage leaderboard. tool_called grouped by tool name, 7-day and 30-day windows. Bottom quartile tools are candidates for deprecation or meta-tool consolidation. Top decile tools are the features your marketing should feature.
Cohort retention by first connector. Users whose first tool call was in QuickBooks versus Shopify versus Gmail. The retention curves diverge in ways that inform onboarding design.
None of these require new instrumentation. They all derive from the same two events.
What we chose for storage
CorpusIQ runs on Azure Container Apps, so audit events land in ContainerAppConsoleLogs_CL in Log Analytics. KQL queries extract fields via regex parsing, which is fine at low to moderate volume.
Three practical notes for anyone building the same.
Retention matters for compliance. Log Analytics default is 30 days. SOC 2 wants at least one year. FINRA wants seven years. Check your workspace retention setting before you cite longitudinal metrics or claim compliance posture. Shipping events to cheap cold storage (Azure Blob or Data Lake) nightly via Diagnostic Settings is usually the right compromise.
Structured logging libraries are worth the migration. Regex parsing works until a message contains a comma inside a value. Then it breaks silently. Emit JSON from the start if you can.
PII handling needs a written policy. user_authenticated contains email. If your platform operates in GDPR jurisdictions, the retention clock for that email is shorter than for tool_called (which contains only a UUID). Segment storage or age-out email fields separately.
What this does not give you
Audit logging is the foundation, not the whole building. Three adjacent concerns still need separate solutions.
Performance telemetry. Tool call latency, error rates, token costs. Goes in APM (Application Insights, Datadog, OpenTelemetry), not audit logs.
LLM inference logs. If your MCP server embeds an LLM for routing or synthesis, the prompts and completions are a separate logging concern. They contain much higher-PII-density data and often need redaction before storage.
Product analytics. Activation funnel, tool usage, retention. You can compute these from audit logs (as above) or ship them to a dedicated product analytics tool.
The bar for enterprise-ready
When an enterprise buyer asks what your observability posture looks like, the right answer has four parts. First, every tool call is logged with user attribution, tool name, session, and request ID. Exportable in machine-readable format. Second, logs are retained for at least twelve months, tamper-evident, or shipped to immutable cold storage. Third, access to logs is itself logged. Who queried the audit log, when. Fourth, a documented schema. If the buyer's security team wants to ingest your logs into their SIEM, they can without asking for help.
A platform that cannot answer all four is not enterprise-ready, regardless of what the marketing page says.
The operator summary
Two event types, clean schema, per-user attribution from day one. That is the entire foundation for audit readiness and product analytics in an MCP server.
Everything else, the dashboards, the funnel reports, the compliance evidence, the deprecation decisions, is a query against that foundation. Build the foundation before you need it. The retrofit is always more expensive.
Try CorpusIQ Free
Connect your first tool in under 2 minutes
30-day free trial. Cancel anytime. All 22+ connectors included.
Start free trial →