Core concepts
A short glossary of the terms the rest of these docs lean on. Each one is a real thing in the code, not a metaphor.
Server
Section titled “Server”An upstream toolset strad brokers. A server is one of four
kinds — remote-http, local-tunnel,
supplementary-image, or builtin — but downstream of the adapter layer they
are indistinguishable: each exposes listTools / callTool / close.
A server’s stable identifier, matching ^[a-z0-9][a-z0-9-]{0,62}$. It is what you
put in ?servers=, and it is the first half of every namespaced tool name. Slugs
cannot contain underscores — that restriction is what makes namespacing
unambiguous.
Namespacing
Section titled “Namespacing”Every tool is exposed as <server>__<tool> — for example
granola__list_meetings. The separator is __ (two underscores). Denamespacing
splits on the first __ only, because tool names may themselves contain
underscores while slugs may not. Namespacing is what lets strad merge many
servers into one toolset without collisions.
Selection (?servers=)
Section titled “Selection (?servers=)”The ?servers= query parameter names which servers a client wants for this
request. An empty or absent ?servers= means “everything this token is
entitled to” (sorted). A non-empty list narrows to those servers.
Selection is a hint, never a grant. This is the one rule:
The query string proposes. The token disposes.
?servers= can only narrow the token-derived entitled set — it can never widen
it. See Auth for why (RFC 8707: a query string is not a
resource, so it cannot scope a token).
A named server whose upstream cannot be listed fails the request rather than
quietly dropping out of the toolset. The single exception is a
local-tunnel whose machine is not
answering, because for that kind that is expected rather than a fault: it keeps
its place in the listing and serves one tool, <slug>__troubleshoot_host, saying
which machine is down and what to run on it. Named on its own it returns that
tool rather than an error.
Entitlement
Section titled “Entitlement”The set of servers a token may reach, computed server-side by intersecting two directions that must both agree:
- A role the token holds must name the server under
roles.<role>.servers(where["*"]means every server), and - The server must list that role under its
entitlements.
Neither side grants access alone. A server with entitlements: [] (the default)
is reachable by nobody — strad fails closed. The final selection for a request is
entitled ∩ requested.
The 424 contract
Section titled “The 424 contract”When a requested server is not in the entitled set, strad returns 424 Failed Dependency rather than dropping it silently or returning 401. The body names
each unavailable server with a status (not_configured, not_entitled, or
disabled) and an authorize_url — that server’s console page,
${publicUrl}/ui/<slug>. A slug that names nothing lands on the console’s own
not-found rather than on a stock 404, with the server list one click away. This
is the fail-closed contract: a client always learns which server it couldn’t
get and where to fix it.
Statelessness
Section titled “Statelessness”strad keeps no session state. A fresh MCP Server + transport is constructed
per request (sessionIdGenerator: undefined) and destroyed when the response
closes. The toolset is a pure function of (token, URL). This is both an
operational property (any replica can serve any request) and the reason
?servers= is spec-conformant under the 2026-07-28 revision, which forbids
per-connection toolset variation but allows per-request variation.
Bundle
Section titled “Bundle”A deployment group. core is the implicit, public bundle (the gateway process
itself); every other bundle becomes an internal-only App Platform component on
the private network. Bundles are how strad scales out heavy servers without
exposing them. See Architecture.
One bundle runs one image, but any number of servers may share it — each on its
own path, and capability variants of one upstream on the same path. strad’s
own deployment is three components: core; one bundle whose image mounts
twenty-two MCP servers in a single process; and bundle-zimmer-secrets, which runs that same
image for the one thing a bundle cannot share — its process.env, and so the
parameter store the secrets server fronts.
Capability
Section titled “Capability”Which of a server’s tools a slug may use, set by tools: { allow: [...] } /
{ deny: [...] } and enforced by the gateway on tools/list and
tools/call — a withheld tool is refused with JSON-RPC -32600 before it reaches
the upstream. The capability is baked into the slug (onepassword-ro vs
onepassword-rw), so ?servers= selects it. Two capability variants are the same
container.
An allow list is exhaustive, so an entry naming a tool the upstream does not
serve costs that slug the tool it meant to grant, silently. CI matches every entry
against the booted bundle’s real tool names
(check-tool-policy);
the gateway reports what CI could not enumerate, at runtime, as
mcp.policy_unmatched_tools.
Capability is therefore policy — a gateway that declines — and not physics —
a process that lacks the tool. It is a real boundary and a weaker one than the
container-level tool-group env vars it replaced, and strad says so rather than
letting you assume the stronger guarantee. See HACKS.md #22.
Parameter
Section titled “Parameter”A named value strad manages for one server — a bucket name, a service-account email, a private key. A parameter is either non-secret (safe to read back) or secret (write-and-inject-only), and it is addressed by one canonical path:
/strad/{env}/mcp/{server-slug}/{kind}/{VARIABLE_NAME}kind is static today; an oauth kind is a future ticket (issue
#32). A namespace is a prefix of
that path (e.g. /strad/staging/mcp/) — the secrets server is scoped to one and
refuses to act outside it. Parameters live behind a store-agnostic interface; GCP
(Parameter Manager indexing everything, Secret Manager backing the secret parts) is
the first backend. See Architecture.
/strad/{env}/gateway/static/ sits beside mcp/ and holds what belongs to the
deployment rather than to one server — the ${NAME} references a config makes,
and the gateway’s own keys. It is what gateway.secrets.provider: gcp-parameter-store reads at runtime. A supplementary server’s env: reference
reads its own mcp/{slug}/static/ first and falls back to this one, so the same
${NAME} in two servers can mean two values; see two servers, one variable
name and
Secrets.
Secret provider
Section titled “Secret provider”Where a running strad resolves ${NAME} from, and when. env (the default)
reads the process environment, so a secret is fixed for the life of the container
and changing one needs a deploy. gcp-parameter-store reads the parameter store
while the gateway runs — once per process, held for ttlSeconds — so a change
made through strad’s console lands at once, a rotation made outside it within
ttlSeconds, and an addition within negativeTtlSeconds. No deploy. One
provider per deployment, because two ways to manage a credential means one is
stale and nobody knows which.
consoleEnv
Section titled “consoleEnv”Env for the core gateway only — the human console’s half of a
supplementary-image server. env: lands on the component that answers /mcp, so
anything in it is a credential the agent path holds. consoleEnv: lands on core,
which serves the SSO-gated /ui and nothing an agent can reach. The asymmetry is
the point: the secrets server’s viewer key (which cannot read a secret value) is
in env:, and its admin key (which can) is in consoleEnv:, so the two never meet.
The deploy layer keeps consoleEnv: off every other component — a broadcast SECRET
would have reached the bundle too. See Auth.
params
Section titled “params”A boolean opt-in on a builtin or supplementary-image server. When true,
everything under /strad/{env}/mcp/{slug}/static/* is resolved from the parameters
system and injected as that server’s environment — for a core builtin in-process
on every call, so a changed parameter reaches it without a restart; for a
supplementary image at deploy time onto its bundle, because that is a container
strad does not run. It is the modern alternative to hand-writing ${REF} entries in
env:. The resolved values reach the child process only, never a tool result.
Which role a running container plays: core (public gateway, proxies to bundles)
or bundle (internal, serves its assigned builtins to core). Selected by
STRAD_MODE; the same image runs both.