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.
Availability
Section titled “Availability”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.
The tool
Section titled “The tool”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.
What it cannot do
Section titled “What it cannot do”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:writeis not reachable over MCP at all — the route authenticatesquery:readand exposes one read-only tool. - No cross-workspace access.
workspaceIdis 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/listreturns one tool, and the list is fixed —tools.listChangedis advertised asfalse, so a client is told not to subscribe to changes that cannot happen.
Statuses and codes
Section titled “Statuses and codes”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:
| Status | Code | Cause |
|---|---|---|
| 401 | unauthorized | Missing, unknown, expired or revoked key after the private-origin gate. |
| 403 | forbidden | A disallowed URL, Host or Origin (checked first), or a key lacking query:read. |
| 405 | method_not_allowed | Not POST. Allow: POST. |
| 406 | JSON-RPC -32000 | Accept did not explicitly allow both application/json and text/event-stream. |
| 415 | json_required | Not application/json, or a Content-Encoding other than identity. REST splits these into unsupported_media_type and unsupported_encoding. |
| 413 | request_too_large | Body over 16 KiB. |
| 400 | invalid_query | A URL query delimiter was present, including a bare ?. |
| 400 | invalid_request | The body is not bounded, valid JSON. REST returns invalid_json. |
| 503 | query_unavailable | The exchange failed or exceeded its deadline. |
| 503 | auth_unavailable | The 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.
Session and transport behaviour
Section titled “Session and transport behaviour”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.