Skip to main content

StackQL v0.11 Turns Every Agent Cloud Query Into an OpenTelemetry Record: What That Buys Your Fleet's Audit Trail

11 min readDora NodaDora Noda
Share
On this page

Your deploy agent just ran 40 cloud queries across three providers. Quick: which ones mutated state, which ones were refused by the policy gate, and how long the slow one took? If your answer involves grepping a bespoke JSONL file with a custom parser, StackQL's September 2026 release was made for you. StackQL v0.11 brings its MCP server onto the current Model Context Protocol revision, 2026-07-28, and — the part that matters for anyone running agents against real infrastructure — lets the audit log that records what an agent did be emitted as OpenTelemetry log records instead of StackQL-specific JSON.

The one-paragraph version for the impatient:

  • What changed: one flag, --mcp.log.format=otel, turns every tool call into an OTLP/JSON log record carrying GenAI and MCP semantic-convention attributes plus the verbatim SQL, the gate decision, and the duration.
  • Why it matters: the line format is exactly what the OpenTelemetry Collector's otlp_json_file receiver reads, verified against otelcol-contrib 0.160.0. Agent activity lands in the same pipeline as every other fleet event with no sidecar exporter and no transform processor.
  • The catch: protocol sessions are gone over HTTP in the new revision, so serving 2026-07-28 natively over HTTP requires a stateless switch with a real tradeoff for older clients. And an audit stream is not a policy layer — per-tenant scoping and spend ceilings are still yours to build.

The rest of this post works each of those claims end to end: the record format, the collector wiring, the stateless decision, and the remaining gap between inventory-query tools and deploy-authority tools.

What v0.11 actually ships: two changes, one server

StackQL is the SQL-native engine that exposes cloud and SaaS APIs — AWS, Azure, Google, GitHub, Databricks, and 40-plus other providers — as queryable tables, so an agent inventories infrastructure with SELECT instead of learning forty SDKs. Its MCP server wraps that engine in discovery, validation, and execution tools (run_select_query for reads, mutation tools behind a policy gate), governed by server modes: read_only, safe, delete_safe, and full_access, with read_only the default. The September 5, 2026 update ships two changes on top of that foundation.

Change one: MCP protocol revision 2026-07-28, negotiated per client. The 2026-07-28 revision is the largest MCP spec release since launch: the initialize handshake is gone (version and capabilities ride inline in _meta on every request, with a single server/discover call for discovery), protocol-level sessions and the Mcp-Session-Id header are gone from Streamable HTTP, and mid-call server-to-client elicitation is replaced by input_required results the client retries with answers attached. The StackQL server speaks the new revision alongside every earlier one it already supported, negotiating per client so a mixed fleet works against one server:

RevisionLifecycleApproval prompt in safe / delete_safe mode
2026-07-28No handshake; version and capabilities in _metainput_required result, retried with inputResponses
2025-11-25, 2025-06-18initialize handshakeServer-initiated elicitation/create request
2025-03-26, 2024-11-05initialize handshakeServer-initiated elicitation/create request

The acceptance test for the work was the server's headline feature, the gated-write flow: a mutation in safe mode still stops for approval on both revisions over both transports, and the audit record still says whether the user accepted, declined, or dismissed the prompt. Nothing changes in how you run the server or in the SQL an agent writes.

Change two: the audit log speaks OpenTelemetry. Every tool call the server handles already wrote one audit record — the tool, the server mode, the gate decision, the verbatim SQL, the duration, and any error. Until now that record was a line of StackQL-specific JSON, which meant a custom parser between the file and anything you wanted to do with it. The new format option removes the parser:

bash
stackql mcp --mcp.server.type=stdio --mcp.log.format=otel

The default jsonl is unchanged byte for byte, the format can also be set in mcp.config as "audit": {"format": "otel"}, and the destination is the same rotating file either way. Under the hood the encoding is a decorator in the generic pkg/sink package, not MCP-specific plumbing — the audit log is the first consumer, and other StackQL log channels can adopt the same option later.

The audit record, field by field

A SELECT run through the server produces one OTLP/JSON export payload per line. Here is what matters inside it, grouped by what you would actually query or alert on:

AttributeValue (example)What it tells you
gen_ai.operation.nameexecute_toolStandard GenAI span operation — joinable with every other agent tool call in your backend
gen_ai.tool.namerun_select_queryWhich MCP tool ran
gen_ai.tool.call.id3f1c9a6b2e8d4a70Correlates the records of one call — including the second record an approval-gate decision emits
mcp.method.nametools/callThe protocol exchange, per the OTel MCP conventions
mcp.protocol.version2026-07-28Which revision the calling client negotiated — your mixed-fleet census, for free
stackql.queryVerbatim SELECT ...The exact statement the agent ran
stackql.query_classselectRead vs. mutation at a glance
stackql.modesafeThe server mode that governed the call
stackql.decisionallowThe gate verdict: allowed, refused, or failed
stackql.duration_ms412How long the provider round trip took — your slow-query signal
stackql.rows_returned5Result size without result contents

Three design decisions in this record deserve emphasis because they are the ones a homegrown exporter usually gets wrong.

First, the attribute set is a versioned interface. The upstream GenAI conventions are still Development status, so StackQL pins what it emits: the instrumentation scope version (1.0.0) names the schema, the schemaUrl pins the stable conventions, and a repository test asserts the exact attribute list. When the set changes, the scope version changes with it — your dashboards and alerts key off a contract, not a hope.

Second, redaction is identical in both formats. The audit answers "what did the agent do," not "what did the agent see." Result rows are never written to either format; only the statement that produced them is, and the regression suite checks that a value returned to the client appears in neither log. This is the correct default for a fleet audit trail: the record of a query against a secrets-adjacent table must not itself become a secrets leak.

Third, trace correlation works with or without agent cooperation. If the calling agent propagates W3C trace context in params._meta (a traceparent key, as the MCP conventions describe), the records carry that trace and span — the tool call joins the agent's distributed trace. When the agent does not propagate context, the server generates one trace id per MCP session, so every record from one agent session still correlates in your backend. A refused or failed call is an ERROR record with error.type and error.message, so the alert query is just "severity ERROR on this scope."

The collector wiring: no transform processor

The line format is exactly what the OpenTelemetry Collector's otlp_json_file receiver reads, so the stream reaches an OTLP pipeline with no transform processor. This configuration was verified against otelcol-contrib 0.160.0:

yaml
receivers:
  otlp_json_file:
    include: [/var/log/stackql-mcp.log]
exporters:
  debug: {}
service:
  pipelines:
    logs:
      receivers: [otlp_json_file]
      exporters: [debug]

Swap the debug exporter for otlphttp or whichever backend you run, and the agent's activity lands next to the rest of your telemetry with the same resource, scope, and trace semantics as everything else.

Why does "no transform" matter enough to be the headline? Because the tax it removes is the one every platform team has paid: the bespoke parser, the sidecar exporter, the field-mapping config that rots one release behind the tool that emits the logs. A Prometheus-plus-collector fleet already runs the collector on every node; pointing a file receiver at the audit log is a config change, not a project. The moment agent actions share resource, scope, and trace semantics with application telemetry, three things become ordinary queries instead of forensic exercises: which agent touched which provider API and when, whether the gate allowed or refused it, and how the slow provider calls correlate with the rest of the request path. Upstream is explicit that sinks and dashboards on top of the stream are the next step — this release makes the stream standards-shaped, and standards-shaped is what lets your existing backend do the rest.

The stateless decision: one listener cannot serve both models

The protocol half of v0.11 comes with a deployment decision that deserves care, because the wrong choice fails in a way that looks like an approval bug rather than a config bug.

Over stdio there is no decision: one process serves every revision, a current client's first request is served without a handshake, and an older client's initialize still works. If you run the server via Claude Desktop, the npm or PyPI launchers, or the Docker image, there is nothing to configure.

Over Streamable HTTP the two models cannot share one listener, because the protocol removed sessions. The default keeps the stateful, session-per-client model, which serves revisions up to 2025-11-25 — a 2026-07-28 client learns that from server/discover and negotiates down, so existing HTTP integrations keep their sessions and approval prompts unchanged. To serve 2026-07-28 natively over HTTP, you set stateless on the server:

bash
stackql mcp --mcp.server.type=http \
  --mcp.config '{"server": {"transport": "http", "address": "127.0.0.1:9992", "stateless": true} }'

A sessionless server issues no Mcp-Session-Id, keeps the list endpoints connection-invariant, and runs approvals through input_required. The tradeoff: it still accepts an older client's initialize and serves reads to it, but it cannot retain the elicitation capability that client declared at handshake time — an older client cannot approve gated writes on a sessionless server. Reads keep working, which is exactly why this bites: everything looks fine until a safe-mode write needs approval and the approval round trip cannot complete.

The decision rule is therefore simple: pick stateless for current-revision hosts, and leave the default for any fleet that still includes older clients. If you are rolling a fleet forward, the mcp.protocol.version attribute in the new audit stream is your migration census — watch the old revisions drain to zero, then flip the switch.

What v0.11 does not give you: the deploy-authority gap

An OTel-native audit stream answers "what did the agent do." It does not answer "what is the agent allowed to do next," and the distance between those two questions is the remaining work before inventory-query tools become deploy-authority tools. Concretely, a self-hosted fleet still needs to build:

  • Per-tenant tool scoping. Server modes (read_only through full_access) govern what one server instance permits, but they are not per-tenant policy. The moment two tenants share a fleet, "which tools may this tenant's agent call, against which providers, with whose credentials" needs an enforcement point in front of or around the server — the mode flag alone does not get you there.
  • Spend ceilings. Cloud inventory queries are read-cheap until an agent decides to enumerate every resource in every region on a loop, and mutations are spend-real by definition. Rate limits and budget guards on tool calls are still yours to build; the audit stream tells you what it cost after the fact.
  • Sinks and dashboards. Upstream says this plainly: the release makes the stream standards-shaped, and the views on top of it come next. The alert on stackql.decision != allow, the per-agent activity dashboard, the weekly "what did the fleet change" report — those queries are now easy, but they are still yours to write.

None of this diminishes the release. The correct layering is: the server owns the protocol surface and the audit facts, the collector owns the pipeline, and the platform owns policy. v0.11 completes the first two links of that chain for a standards-shaped pipeline. The policy link — scoping, ceilings, approvals that survive the stateless transition — is where a self-hosted PaaS earns its keep, because it is the link no upstream can build for your tenants.

The pattern to copy

Step back from StackQL specifically and v0.11 is a template for how MCP servers should grow up. Speak the current protocol revision while negotiating down for the installed base, so one server serves a mixed fleet. Emit audit facts in the semantic conventions your operators' backends already understand, with a pinned, tested attribute set instead of bespoke JSON. Log statements, never result rows. Correlate with the caller's trace when it cooperates and with a session trace when it does not. And say out loud what the release does not do, so the platform layer knows where its job starts.

Every MCP server with deploy authority will eventually need all five of those properties. StackQL just shipped the full set for the inventory-query case — which makes it the reference implementation to measure your own deploy endpoint against.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Agent-operated deploys need audit trails with the same rigor as the deploy path itself, which is why deploy-from-chat on infrastructure you own starts with owning the endpoint, the credential scope, and the log. Star the repo on GitHub or deploy your first app today.

Related articles

Give your agents a chain backend

Autonomous agents hit RPC endpoints very differently than people do. See what bex router handles on their behalf.

Read the agents guide