Skip to content

Guide

MCP

The read-only query_ga4_sessions tool, and why it returns the same object as REST.

SignalBraid exposes its query as a Model Context Protocol tool, so an AI client can answer a question from your data and cite the same provenance the API returns.

The point is not that an assistant can reach the data. It is that it reaches the same published snapshot the API and your spreadsheet read, so three surfaces agree on more than a total. The MCP tool calls the same query service as POST /v1/query and returns the identical MetricResult object — not a summary of it, and not a re-computation.

There is no public MCP endpoint. The route is POST /mcp on the private gateway origin, reachable only over a Cloudflare service binding from inside the account. Every POST must send Accept: application/json, text/event-stream; both media types must be explicitly acceptable, for both legacy and current protocol requests. Both are parameterless server representations: a parameterized range does not stand in for either one, and an explicit quality of zero is not acceptable.

Two gates, in this order. Private-origin gate first: the request URL must be https://signalbraid-gateway.internal/mcp; the Host header must be absent, that hostname, or the pinned loopback bridge form 127.0.0.1:<port> (1–65535, no leading zero); and an Origin header, if present, must equal that origin. This gate runs before the authorization store is touched, as required by the MCP transport’s DNS-rebinding defense. Authentication second: requests that reach that boundary need a current query:read key.

The order matters when you are reading a status. A disallowed URL, Host or Origin returns 403 without checking whether the key exists. With the private origin accepted, a missing or invalid key returns 401; a current key without query:read returns 403.

An operator-run acceptance passed on 7 September 2026 using MCP Inspector 2.5.0 against the private staging deployment, with all nine of its verification checks green. The acceptance record names the client and version only; the transport (Streamable HTTP) and protocol version (2026-07-28) are recorded in an earlier run’s client block rather than in that record. Claude, ChatGPT and Cursor are the intended clients and are not yet tested; setup guides will be written when they are, not before.

One tool, query_ga4_sessions:

{
"workspaceId": "your_workspace",
"connectionId": "your_connection",
"propertyId": "123456789",
"startDate": "2026-08-31",
"endDate": "2026-09-04"
}

Exactly those five fields — the same window as REST. metric and dimensions are fixed by the route to ga4.sessions and date; a client cannot vary them.

The result is the full MetricResult as structuredContent, including provenance.snapshots, so an assistant can state which extraction it read. See Reading data for how to interpret it.

Annotated readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false — a client that respects annotations can treat it as safe to call without confirmation.

The tool surface is deliberately tiny, because an MCP endpoint is an execution surface handed to a model.

  • No SQL, no filters, no free-form parameters. The input schema is a strict object; an extra property is rejected rather than ignored.
  • No writes. No sync trigger, pilot setup, schedule mutation or export. sync:write is not reachable over MCP at all — the route authenticates query:read and exposes one read-only tool.
  • No cross-workspace access. workspaceId is checked against the key’s own workspace, and the query service binds the authorised workspace independently of whatever the input says.
  • No other tools. tools/list returns one tool, and the list is fixed — tools.listChanged is advertised as false, so a client is told not to subscribe to changes that cannot happen.

MCP does not reuse REST’s error contract. Most statuses match, some codes do not, and MCP also uses 406 for its transport negotiation — so use these:

StatusCodeCause
401unauthorizedMissing, unknown, expired or revoked key after the private-origin gate.
403forbiddenA disallowed URL, Host or Origin (checked first), or a key lacking query:read.
405method_not_allowedNot POST. Allow: POST.
406JSON-RPC -32000Accept did not explicitly allow both application/json and text/event-stream.
415json_requiredNot application/json, or a Content-Encoding other than identity. REST splits these into unsupported_media_type and unsupported_encoding.
413request_too_largeBody over 16 KiB.
400invalid_queryA URL query delimiter was present, including a bare ?.
400invalid_requestThe body is not bounded, valid JSON. REST returns invalid_json.
503query_unavailableThe exchange failed or exceeded its deadline.
503auth_unavailableThe authorization store could not be reached.

A 200 does not mean success. Protocol failures come back with HTTP 200 and a JSON-RPC error object, because that is what the protocol requires: -32600 invalid request, -32601 unsupported method, -32602 unsupported tool or arguments, -32603 internal. A client that checks only the HTTP status will read every one of these as success.

Tool failures are different again: a 200, a valid result, and isError: true carrying the same fixed error envelope REST uses. data_not_ready and data_incomplete also carry their bounded, non-empty missingDates list; malformed internal error fields collapse to query_unavailable.

The named queryForExport service-binding RPC used by the pilot runtime is private Worker-to-Worker infrastructure, not an MCP tool or an HTTP route. MCP still exposes only query_ga4_sessions. A mixed-timezone query returns isError: true with the fixed actionable timezone_conflict message, with no analytics read.

The route is stateless. Each request builds its own server instance and no session id is issued, so nothing carries between exchanges and there is no session to hijack or replay.

Authorization is rechecked before the buffered response is released, so a key revoked mid-call yields 401 and no tool result — the same rule as REST.

Limits: requests capped at 16 KiB, complete responses at 128 KiB, exchanges at 20 seconds.

There is a third cap worth knowing because its failure is currently mislabelled: a single serialised tool result is capped at 43,690 bytes (a third of the response cap). A result over that is returned as isError: true with code query_unavailable — indistinguishable from a backend outage, even though it is a size limit. A 31-day MetricResult is around 8 KB, so this is not reachable with today’s one metric.

Protocol-level diagnostics are sanitised before they leave the boundary: the JSON-RPC error code is preserved, and only the message is replaced, so an SDK error never carries exception text or reflected header values out of the gateway.

A syntactically valid JSON-RPC notification has no id and therefore gets no JSON-RPC response. This includes an unknown notification method and an id-less tools/call: the transport returns an empty HTTP 202 and performs no tool dispatch. Unsupported requests that carry an id still return -32601.

Caveat worth passing to whoever reads the answer

Section titled “Caveat worth passing to whoever reads the answer”

An assistant citing these numbers is citing a snapshot carrying provider_may_revise_recent_data and not_independently_reconciled. If your client surfaces tool output verbatim, the flags travel with it. If it summarises, they may not — and a confident sentence with the caveats stripped is exactly the failure mode this API’s provenance exists to prevent.