skip to content

Why must an MCP server's tool set not vary per connection, and what may it vary by?

level: seniorimportance: should knowfreq 38%

answer

  1. Statelessness makes the connection meaningless
  2. One permitted axis of variation
  3. Credentials, not connections
  4. Any node must answer identically
  5. Hiding is not enforcing

basics

~20 s

MCP 2026-07-28 is stateless, so a server MUST expose the same tool set regardless of which connection a request arrives on. The one permitted variation is the authorization presented with the request: different credentials may legitimately see different tools.

solid answer

~50 s

Revision 2026-07-28 made MCP stateless: every request is self-contained, and a server MUST NOT rely on prior requests over the same connection to establish context — "an open connection, such as a STDIO process, is not a conversation." The tool rule follows directly: the tool set MUST NOT vary per connection. You cannot reveal a tool only after the client has called some other tool, and you cannot let one node behind a load balancer answer `tools/list` differently from another. What the set MAY vary by is the **authorization presented with the request** — a token with admin scope may see tools a read-only token does not, and the same is true for prompts and resources. Because that variation is per-credential rather than per-connection, any node can serve any request, and `tools/list` marks such a result `cacheScope: "private"` so a shared cache never crosses users.

code

json · 9 lines
json
{
  "resultType": "complete",
  "ttlMs": 60000,
  "cacheScope": "private",
  "tools": [
    { "name": "list_incidents", "inputSchema": { "type": "object" } },
    { "name": "close_incident", "inputSchema": { "type": "object" } }
  ]
}

go deeper

for a junior

Remember the rule as stated: the tool set must be the same on every connection, and the only thing that may change it is the authorization presented with the request.

for a middle

Explain why — the protocol is stateless, a connection is not a session, and servers must not infer anything from earlier requests on it.

for a senior

Ground it operationally: any node behind a load balancer must answer identically, authorization-varying lists are cacheScope private, and hiding a tool from the list never replaces enforcing it in the call handler.

for a principal

Own the multi-tenant design: how entitlements map to tool surfaces, how surface changes are rolled out and communicated, and how a stable advertised surface keeps user consent meaningful.

## The rule MCP revision 2026-07-28 states plainly that a server's tool set MUST NOT vary per connection, though it MAY vary by the authorization presented. The same rule applies to prompts and resources. It is one of the new MUSTs introduced alongside the move to a stateless protocol. ## Why it exists The rule is a corollary of statelessness. Before 2026-07-28, a client ran `initialize`, and the connection carried negotiated context for its lifetime; a server could plausibly say "this connection has been initialized with capability X, so I will expose tool Y on it." That model is gone. In 2026-07-28 every request carries its own protocol version and capabilities in `_meta`, servers MUST NOT infer capabilities from prior requests, and an open connection is explicitly not a session. Once a connection means nothing, letting the tool set depend on it produces incoherence. Three concrete failures: **Load balancing.** A remote MCP server behind a load balancer may have request N answered by one node and request N+1 by another. If the tool set were per-connection state, node B would not know what node A had exposed, and a `tools/call` for a tool listed by A would fail on B. With the invariance rule, any node can serve any request from configuration plus the presented credential. **Caching.** `tools/list` is a cacheable result with `ttlMs` and `cacheScope`. A cache is only sound if the same inputs give the same answer. Per-connection variation has no cache key — there is no connection identifier in the protocol to key on any more. **Consent.** The host shows the user which tools a server exposes and gets approval. If the surface could morph after approval based on which requests had already been made, the approval would mean nothing. Keeping the set stable makes "what does this server expose?" an answerable question. ## What the rule forbids - Hiding a tool until the client has called an unlock or login tool first. The unlock pattern is exactly the per-connection state the rule bans. - Exposing extra tools on a connection where the client happened to advertise a particular capability in an earlier request. - Adding a tool only to the connection that just requested something related. ## What the rule permits - **Authorization-based variation.** Two callers presenting different credentials may see different tool sets. This is per-request and derived from the token, so it survives load balancing and reconnection: the same token always yields the same list from any node. - **Change over time.** The set may evolve — deployments add tools, a tenant's plan changes. That is server-wide change, not per-connection divergence, and it is announced with `notifications/tools/list_changed` to clients that opted in with `toolsListChanged` on a `subscriptions/listen` stream, and bounded by the `ttlMs` on the list. - **Per-user surfaces on stdio.** A stdio server launched with different configuration or environment for different users is a different server instance, not one server varying per connection. ## Getting authorization-varying lists right If your list depends on the credential, mark it `cacheScope: "private"` so no shared cache serves one caller's list to another. And enforce for real: hiding a tool from `tools/list` is a UX affordance, never a security control. A caller who learned a tool's name elsewhere can still send `tools/call` for it, so the authorization check must live in the call handler too, answering an unauthorized attempt at the HTTP/OAuth layer — `403` with `error="insufficient_scope"` when a step-up would help. ## The migration answer If you are porting a server that used the unlock pattern, the replacement is to make the gate an *argument* rather than a *phase*: expose the tool always, and have the call itself carry whatever identifies the caller's entitlement, with the server deciding per request. Anything that must persist between calls is handled by the server minting an explicit identifier the client passes back as an ordinary tool argument — never by the connection remembering. ## Interview framing A good answer states the MUST, names the one permitted axis (authorization), and grounds it in a concrete operational consequence — load balancing, cache soundness or consent stability — rather than reciting the sentence. The weak answer treats a connection as a session, which is precisely the pre-2026-07-28 mental model this rule exists to kill.

  • Is omitting a tool from tools/list enough to stop an unauthorized caller from using it?
    No. `tools/list` shapes what the model and user see; it is not an access control. A caller who knows the name can send `tools/call` directly, so the authorization check must be enforced in the call handler as well. On a remote server that means answering at the HTTP/OAuth layer — 403 with `error="insufficient_scope"` where a step-up flow would resolve it.
  • How do you replace a server that revealed extra tools after the client called an unlock tool?
    Expose the tools unconditionally and decide per request from the credential presented, or move the unlock outcome into arguments the caller passes on every call. The phase-based design relies on connection state, which 2026-07-28 forbids; a load-balanced deployment would break anyway once a later request landed on a node that never saw the unlock.
  • Does the invariance rule mean a server's tool set can never change?
    No — it forbids divergence *between connections at the same moment*, not change over time. Deployments, configuration edits and plan changes all legitimately alter the set. Clients learn about it through the `ttlMs` on the cached list expiring, or promptly via `notifications/tools/list_changed` if they opened a `subscriptions/listen` stream with `toolsListChanged`.

saying these in an interview costs you the question

  • Treating an open connection as a session that can hold tool state
  • Revealing tools only after the client calls an unlock tool
  • Believing omission from tools/list is an authorization control
  • Marking an authorization-dependent tool list cacheScope public
  • Assuming the rule means the tool set can never change at all

context