Development Choices

Discover MCP Servers and Cache Results Safely

Author
Joseph TrasattiMember of technical staff
Published
Section
MCP
Length
10 min read3 sources cited

Call `server/discover` before normal requests when you need revisions and capabilities up front, cache only complete results under their method, parameters, TTL, and scope, and invalidate on subscribed change notifications. Keep private entries partitioned by authorization context; an expired TTL means stale, not proof that the underlying data remained unchanged.

Prerequisites

Implement the 2026-07-28 request metadata before adding discovery. Each request identifies its protocol revision, client, and client capabilities; the revision no longer uses the initialize/initialized exchange or a protocol-level session. If the client still supports the previous handshake, handle that compatibility path separately through the migration from MCP 2025-11-25 to 2026-07-28.

Decide how the cache will distinguish authorization contexts before storing any response. A private entry may be reused only within the authorization context that produced it. If two requests use different access tokens, they require different private cache partitions even when their methods and parameters match.

The normative reference is the server discovery specification for revision 2026-07-28. It defines server/discover as a request that servers must implement but clients may choose whether to call.

Discovery is cacheable, not permanent

An MCP client discovers capabilities, caches them within a TTL, invalidates or expires them, and discovers again
Cache hints reduce round trips without turning capabilities into permanent configuration.
  1. Call server/discover when the client needs server information before its first operation.

    Send server/discover with the standard request _meta, including io.modelcontextprotocol/protocolVersion, io.modelcontextprotocol/clientInfo, and io.modelcontextprotocol/clientCapabilities. The method has no additional body parameters.

    This call does not initialize a connection or create a session. The 2026-07-28 release announcement published July 28, 2026 describes the protocol core as stateless: every request carries the context needed to process it. Discovery is therefore a read of server metadata, not the first state transition in a handshake.

    A complete discovery result can provide all of the following in one response:

    • supportedVersions, listing the protocol revisions the server accepts;
    • capabilities, such as tools, resources, or prompts;
    • optional instructions, giving natural-language guidance on using the server;
    • server identity in _meta when the server supplies it;
    • ttlMs and cacheScope, telling the client how the result may be cached.

    The cost is one extra RPC before the operation the user actually requested. Pay that cost when the client must present server information, choose among supported revisions, or know capabilities before exposing an action. Skip discovery when the client can send its intended RPC directly and handle an unsupported-protocol response. Discovery is optional for clients precisely because a single-purpose caller may learn nothing useful from the extra round trip.

    On stdio, discovery also serves as the compatibility probe for clients that support both the 2026 request metadata and the legacy initialization exchange. That is a migration condition, not a reason for a new 2026-only client to recreate session state.

  2. Use the discovery result to choose a revision and gate optional behavior.

    Select a value from supportedVersions for subsequent requests. Treat the returned capabilities as the server’s declaration of which optional protocol surfaces it supports. The detailed decision flow belongs in MCP capability negotiation; the important caching boundary is that discovery records what the server reported at one point in time.

    Keep instructions as usage guidance. Keep the server name and version for display, logs, or debugging. The specification says that server identity is self-reported and unverified, so it must not determine authorization or another security decision.

    Do not infer permanence from any of these fields. A server deployment can change its supported revisions, capabilities, instructions, or identity after the response. The cache hints describe when reuse is reasonable; they do not turn discovery into durable configuration.

  3. Admit only complete, explicitly cacheable results.

    Apply the caching rules in the 2026-07-28 specification to results whose resultType is complete. The server must attach caching hints to complete results from:

    • server/discover;
    • tools/list;
    • prompts/list;
    • resources/list;
    • resources/templates/list;
    • resources/read.

    This means both list results and resource-read results carry ttlMs freshness guidance plus a cacheScope of public or private.

    Do not cache an interim input_required result. It represents an unfinished multi-round-trip request and carries no cache hints. Also reject results produced by retries containing inputResponses or requestState; those outputs depend on supplied inputs that are not represented by the ordinary cache key.

    This admission check costs a branch at the cache boundary and requires the client to retain resultType. Omitting it risks treating an unfinished interaction as a reusable answer. If an implementation cannot distinguish complete from interim results, it should bypass this cache rather than guess.

  4. Construct the key from the method and every result-affecting parameter.

    Start the key with the request method. Add all request parameters that can change the result. For resources/read, that includes the uri. For a paginated list request, it includes the cursor. Two requests may share an entry only when their methods and result-affecting parameters match.

    Do not key a resource solely by URI across different methods. Do not reuse the first page of a list for a request carrying a cursor. A method-only key is attractive because it produces fewer entries, but it is the wrong answer whenever the method can return different results for different parameters.

    Authorization partitioning is additional to this protocol cache key. The same method and parameters can identify a public entry shared across callers or several private entries isolated by authorization context. Keep that distinction in cache metadata rather than assuming parameter equality grants permission to reuse a result.

    The practical cost is more entries and more invalidation paths. That cost buys correctness: each cached value remains tied to the request that produced it. If cache pressure forces eviction, evicting an entry merely causes a later fetch; collapsing distinct keys can return the wrong result.

  5. Calculate freshness from receipt time and ttlMs.

    Record the local time when the complete response arrives. Consider the response fresh only while now is earlier than the receipt time plus ttlMs.

    Apply the field conditions directly:

    • A positive ttlMs permits the client to consider the result fresh for that number of milliseconds after receipt.
    • ttlMs: 0 makes it immediately stale, so the client may fetch again whenever the result is needed.
    • An absent ttlMs defaults to zero. The specification says absence should occur only with older server versions.
    • A negative value is invalid for a server to send; a client should ignore it and treat it as zero.

    Check freshness when the application asks for the data. TTL is not a polling schedule and does not require a background fetch at every expiry. Once the entry becomes stale, fetch it on the next access. A client that nevertheless polls must add jitter and backoff.

    Cache expiry is not proof that the underlying data stayed unchanged during the TTL. It says only that the client may no longer treat the stored result as fresh. The inverse matters too: an unexpired TTL is not a guarantee that the data remains unchanged. The server may change the underlying data before the deadline, and the client may refetch early when another signal suggests a change.

    This approach trades some freshness for fewer fetches. It is wrong when the application must react promptly to changes and no notification path exists; use a zero or suitably conservative server-provided TTL in that case. Do not manufacture a longer TTL than the server supplied and present the result as fresh.

  6. Enforce cacheScope before returning a hit.

    Treat public and private as sharing rules, not descriptions for display:

    • A public result contains no user-specific data according to the server’s declaration. A client, shared gateway, or caching proxy may store it and serve it across callers.
    • A private result may be reused only for the same authorization context. It must not cross authorization contexts. A different access token therefore requires a different private cache entry, even if every other part of the key matches.

    Check the scope on every hit, not only when writing the entry. This prevents a later caller from reaching private data through a shared method-and-parameters key. It also makes changes in the caller’s authorization context explicit at the read boundary.

    Do not treat cacheScope as access control. A server must still enforce authorization for each primitive. Conversely, a result from an authenticated endpoint can be marked public; that declaration permits sharing outside the authorization context that made the original request. Servers should use public only when the result truly is identical for all callers.

    Private partitioning costs memory because identical-looking responses cannot be collapsed across users or tokens. That is the required trade-off. A shared private cache is the wrong optimization because it can disclose one caller’s tools, filtered lists, or resource contents to another caller. Token validation remains a separate concern from binding an OAuth access token to its intended MCP server.

  7. Cache paginated lists page by page.

    Treat each page as an independent response. Each page has its own ttlMs, and its freshness clock starts when that page arrives. A server may assign different TTLs to different pages, but it must use the same cacheScope across every page belonging to a given list request.

    When one page expires, refetch that page with its cursor. If the cursor is no longer valid, discard all cached pages for that list and restart without a cursor. The protocol does not promise a consistent snapshot across pages, so underlying changes between page fetches can produce gaps or duplicates.

    If the caller requires a consistent view of the full list, refetch from the beginning rather than mixing independently refreshed pages. Page-level caching is the right answer for incremental browsing where an occasional cross-page inconsistency is acceptable. It is the wrong answer for work that requires one coherent snapshot.

  8. Subscribe to changes when freshness matters.

    Combine TTL checks with change notifications through subscriptions/listen when the server advertises the relevant notification capability. TTL avoids unnecessary refetches between notifications; a matching notification invalidates the cached result immediately, even when its TTL has not expired.

    A server may provide TTLs without advertising list-change notifications. In that condition, TTL is the client’s only protocol freshness signal. A server may also provide both. Prefer both when stale catalogs or resources would affect a decision, because each mechanism covers a different gap: TTL bounds how long the client ordinarily reuses a response, while a notification reports a known change before that bound expires.

    Resource-specific consumers should connect the same invalidation rule to MCP resource subscriptions. When a relevant event arrives, mark the matching entry stale; fetch the new value when the application next needs it unless the product requires immediate refresh.

    The cost is subscription and reconnection logic plus an invalidation map from notification types to cache keys. Notifications alone are the wrong answer if the listener can disconnect or miss an event. TTL alone is the wrong answer when the application cannot tolerate waiting until the next expiry. Using both avoids treating either signal as stronger than it is.

  9. Test the boundaries that can return a wrong or unauthorized result.

    Exercise at least these cases before enabling the cache:

    • repeat an identical complete request before expiry and confirm it uses the matching entry;
    • change a result-affecting parameter and confirm the old entry is not used;
    • pass the expiry boundary and confirm the next access refetches;
    • send a relevant change notification before expiry and confirm immediate invalidation;
    • switch authorization contexts and confirm a private entry is not reused;
    • confirm a deliberately public entry can be reused under the sharing policy;
    • invalidate a paginated cursor and confirm the client discards the affected list pages and restarts;
    • return input_required and confirm no cache entry is written.

    Also allow early refetch when behavior contradicts cached discovery, such as a tool call failing because a method is missing or its parameters are no longer accepted. Such an error is evidence that the cached description may be stale, even if its TTL has time remaining.

Expected result

The finished client can call server/discover without creating a session, select a supported revision, and inspect capabilities and instructions before normal requests. It caches complete discovery, list, and resource-read results under exact request keys, honors ttlMs, keeps private entries inside their authorization context, and invalidates relevant entries on subscribed change notifications. Expired entries are treated as stale and refetched on access, never as evidence that the underlying data remained unchanged.

Sources

  1. server discovery specification for revision 2026-07-28modelcontextprotocol.io
  2. 2026-07-28 release announcement published July 28, 2026blog.modelcontextprotocol.io
  3. caching rules in the 2026-07-28 specificationmodelcontextprotocol.io

See also