Skip to main content

StackQL update: MCP protocol revision 2026-07-28 and OpenTelemetry output

· 8 min read
Technologist and Cloud Consultant

StackQL v0.11 is out. The StackQL MCP server: now includes the current Model Context Protocol revision, 2026-07-28, alongside every earlier revision it already supported, and the audit log that records what an agent did can be written as OpenTelemetry log records instead of the bespoke JSONL format. Both are available today through every install channel.

MCP protocol revision 2026-07-28

2026-07-28 is the largest revision of the protocol since it launched. Relevant features for the StackQL MCP server incldue:

  • The initialize / notifications/initialized handshake is gone. Every request carries the protocol version and the client's capabilities in its _meta block, and a client discovers a server with a single server/discover call.
  • Protocol-level sessions are gone from Streamable HTTP. There is no Mcp-Session-Id header, and tools/list, prompts/list and resources/list must return the same answer on every connection.
  • Server-to-client requests such as elicitation no longer happen mid-call. A server that needs the user's input returns an input_required result, and the client retries the call with the answer attached (multi round-trip requests, SEP-2322).

The StackQL MCP server negotiates 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 _meta on every requestinput_required result, retried with inputResponses
2025-11-25, 2025-06-18initialize handshakeServer-initiated elicitation/create request
2025-03-26, 2024-11-05as aboveas above

The gated-write flow is the headline feature of the server, so it was the acceptance test for this work: 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.

stdio

The stdio transport serves every revision on one process. A current client's first request is served without a handshake, and an older client's initialize still works. If you use Claude Desktop, the npm or PyPI launchers, or the Docker image, there is nothing to configure.

Streamable HTTP and the stateless option

Over HTTP the protocol removed sessions, and the two models cannot share one listener. By default the server 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 their approval prompts unchanged.

To serve 2026-07-28 natively over HTTP, set stateless on the server:

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 the approval round trip through input_required. 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, so an older client cannot approve gated writes on a sessionless server. Pick stateless for current-revision hosts, and leave the default for a fleet that still includes older clients. The protocol revision support section of the docs has the full matrix, alongside the server modes it interacts with.

OpenTelemetry output for the audit log

Every tool call the server handles writes 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 whatever you wanted to do with it.

This release adds a second format. --mcp.log.format=otel writes the same records as OpenTelemetry log records, in the OTLP/JSON encoding, one export payload per line:

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

The default, jsonl, is unchanged byte for byte, and the format can also be set in mcp.config as "audit": {"format": "otel"}. The destination is the same rotating file either way. The log format reference lists every option and attribute.

What a record looks like

A SELECT run through the server produces a record like this (pretty-printed; the file holds it on one line):

{
"resourceLogs": [{
"resource": {"attributes": [
{"key": "service.name", "value": {"stringValue": "stackql"}},
{"key": "service.version", "value": {"stringValue": "0.10.623"}}
]},
"scopeLogs": [{
"scope": {"name": "github.com/stackql/stackql/pkg/mcp_server/audit", "version": "1.0.0"},
"logRecords": [{
"timeUnixNano": "1788652800123456789",
"observedTimeUnixNano": "1788652800123987654",
"severityNumber": 9,
"severityText": "INFO",
"body": {"stringValue": "execute_tool run_select_query"},
"attributes": [
{"key": "gen_ai.operation.name", "value": {"stringValue": "execute_tool"}},
{"key": "mcp.method.name", "value": {"stringValue": "tools/call"}},
{"key": "gen_ai.tool.name", "value": {"stringValue": "run_select_query"}},
{"key": "gen_ai.tool.call.id", "value": {"stringValue": "3f1c9a6b2e8d4a70"}},
{"key": "mcp.protocol.version", "value": {"stringValue": "2026-07-28"}},
{"key": "stackql.mode", "value": {"stringValue": "safe"}},
{"key": "stackql.decision", "value": {"stringValue": "allow"}},
{"key": "stackql.query", "value": {"stringValue": "select name, id from google.storage.buckets where project = 'stackql-demo'"}},
{"key": "stackql.query_class", "value": {"stringValue": "select"}},
{"key": "stackql.duration_ms", "value": {"intValue": "412"}},
{"key": "stackql.rows_returned", "value": {"intValue": "5"}}
],
"traceId": "16c62eeb8bb8c6e30917626336d61829",
"spanId": "ff6b2aa55252ac0e"
}],
"schemaUrl": "https://opentelemetry.io/schemas/1.44.0"
}],
"schemaUrl": "https://opentelemetry.io/schemas/1.44.0"
}]
}

The attribute names follow the OpenTelemetry GenAI and MCP semantic conventions: gen_ai.operation.name is execute_tool, gen_ai.tool.name is the MCP tool, gen_ai.tool.call.id ties records of one call together, and mcp.method.name, mcp.protocol.version and mcp.session.id describe the protocol exchange. StackQL's own facts sit under a stackql. prefix: the verbatim stackql.query, its stackql.query_class, the stackql.mode and stackql.decision of the gate, stackql.duration_ms, stackql.rows_returned, and stackql.provider for metadata tools. A refused or failed call is an ERROR record with error.type and error.message.

A call that went through the approval gate produces a second record for the elicitation decision, sharing the same gen_ai.tool.call.id and trace id, so an accepted or declined write is visible as two correlated events.

The design inlcudes:

  • The attribute set is a versioned interface. The GenAI conventions are still Development status upstream, so the set StackQL emits is pinned: the instrumentation scope version (1.0.0) names the schema, the schemaUrl pins the stable conventions, and a test in the repository asserts the exact attribute list. When it changes, the scope version changes with it.
  • 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. The regression suite checks that a value returned to the client appears in neither log.

Trace correlation

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. When it does not, the server generates one trace id per MCP session, so every record from one agent session still correlates in your backend.

Feeding a collector

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:

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 for 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. Sinks and dashboards on top of the stream are the next step; this release makes the stream standards-shaped.

For developers: a general output option

The OpenTelemetry encoding is not tied to the MCP server. It is implemented as a decorator in the generic pkg/sink package that wraps any record sink: a payload can describe its own log records, and any other JSON-shaped record is decorated automatically, with its top-level fields as typed attributes and the record as the body. The MCP audit is the first consumer; other StackQL log and result channels can adopt the same option without new plumbing.

Get it

⭐ Star us on GitHub and let us know what your agents build.