What strad is
strad is a self-hosted MCP platform. It puts one endpoint in front of many MCP servers. A client points at that endpoint, names the servers it wants, presents a token, and gets back a single merged toolset drawn from exactly those servers — proxied, namespaced, and access-controlled server-side.
The core ships with zero MCP servers baked in. strad is a platform, not a bundle of integrations. Servers are attached through one config interface, and that config is the entire product surface.
The shape of the thing
Section titled “The shape of the thing”Point any MCP client at https://your-strad.example.com/mcp?servers=granola,deepwiki,
present a token, and strad returns the union of those servers’ tools — every tool
namespaced as server__tool (e.g. granola__list_meetings) so nothing collides.
The one rule: the query proposes, the token disposes
Section titled “The one rule: the query proposes, the token disposes”There is a single invariant that everything else follows from:
The query string proposes. The token disposes.
The ?servers= parameter can only ever narrow what a caller is already
entitled to. It is never an authorization boundary. A token minted for /mcp
is valid at ?servers=anything; the query string cannot widen access, only
select within it. Entitlement is computed server-side from the caller’s roles and
the config — nothing the caller sends can grow it.
The justification is RFC 8707: resource indicators bind a token to a resource,
and a query string is not one. So ?servers= is treated as a selection hint,
not a credential scope. See Auth for how the entitled set is
actually computed.
It fails closed
Section titled “It fails closed”Ask for a server you are not entitled to, and strad does not silently drop it.
It returns a 424 Failed Dependency with a machine-readable body naming exactly
what’s missing and where to authorize it:
{ "error": "downstream_not_authenticated", "message": "One or more requested servers are unavailable to this token.", "servers": [ { "slug": "slack", "status": "not_entitled", "authorize_url": "https://…/ui/slack" } ]}The per-server status is one of not_configured, not_entitled, or
disabled. strad deliberately avoids returning a 401 here — a 401 would march
the client into a pointless OAuth loop against the gateway, when the real problem
is downstream authorization. And it avoids silently omitting the server, because
“a client that asked for slack and got a toolset without it looks like a broken
Slack integration.” Fail closed, loudly, with a pointer to the fix.
Capability is a policy the gateway enforces
Section titled “Capability is a policy the gateway enforces”Entitlement decides which servers a token can reach. A second, narrower decision
is which tools it can use on a server it has already reached — read-only versus
read-write. That is a tools: { allow: [...] } list on the server entry, and the
gateway enforces it on tools/list and on tools/call: a withheld tool is
refused with JSON-RPC -32600 before the request reaches the upstream.
Because the decision lives at the gateway, gmail-ro and gmail-rw are two config
entries pointing at the same container on the same path. They differ only in the
policy. That is what makes the deployment small, and it is worth naming what it
costs: capability is now policy, not physics. A gateway that declines is not the
same guarantee as a process that lacks the tool. See
Known limitations.
Stateless, and spec-conformant because of it
Section titled “Stateless, and spec-conformant because of it”strad is stateless. It builds a fresh MCP Server and transport for every
request (StreamableHTTPServerTransport({ sessionIdGenerator: undefined })) and
tears it down when the response closes. The toolset a client sees is a pure
function of (token, URL) — it is never derived from session state.
That is exactly what keeps ?servers= conformant with the 2026-07-28 MCP spec
revision, which removes sessions and forbids a server from varying its toolset
per connection — while explicitly allowing variation by per-request input.
Because ?servers= is request input and there are no sessions, selecting servers
per request is squarely within spec.
Auth is the only gate
Section titled “Auth is the only gate”strad is built to run on the open internet — there is no tailnet, VPN, or IP allowlist in front of it. That posture is deliberate, and it means auth is the only thing standing between a caller and the tools:
/mcpalways requires a static system token. No token, no tools — including the CI smoke test that asserts an unauthenticated/mcpreturns401./console(the human surface) uses Google Workspace OAuth.
See Auth for the full model.
There is no hosted strad. You deploy it yourself on DigitalOcean App Platform (see Deploy model), with your own config, your own servers, and your own credentials.
The access model is roles and tokens, and that is the whole of it. There are no accounts, no organizations, and no per-user gating: a role names the servers it can reach, a token holds roles, and a server names the roles it admits. If you need to mediate distrust between the people using it, strad is the wrong tool.