On this page
- Discovery is not authorization
- Tool identity is server-scoped
- A discovered tool is a contract snapshot
- Do not treat a description as executable policy
- Tool schemas should be validated before use
- Providers do not all accept the same schema subset
- Discovery can be lazy
- Eager
- Lazy
- Cached with refresh
- Cache lifetime should come from protocol metadata when available
- Cache scope matters for multi-account clients
- Tools can change while the app is open
- Modern change notifications are opt-in
- Notifications can be missed
- Tool removal must revoke exposure immediately
- Schema changes are more dangerous than tool removal
- Snapshot identity helps race handling
- Hash normalized schemas, not raw formatting
- Discovery should produce a model-facing subset
- Do not let model-facing aliases break audit identity
- Tool descriptions can consume significant context
- Discovery failures need classification
- Preserve the last known snapshot carefully
- Reauthentication can change capabilities
- Discovery metadata is not a security identity
- server/discover and tools/list solve different questions
- Tool annotations should be advisory unless your policy says otherwise
- A useful discovery state machine
- Refresh before sensitive execution
- Tool discovery should be testable without a real server
- Example normalized flow
- Where BYOKchat fits
- Further reading
MCP tool discovery answers a simple question:
Which tools does this server currently offer, and what contract does each tool expose?
The difficult part is everything around that answer:
- server identity;
- schema freshness;
- cache invalidation;
- tool-name collisions;
- permission policy;
- model-provider translation;
- changes while the app is running.
A production client should treat discovery as dynamic metadata, not as a one-time setup step.
Discovery is not authorization
If a server returns:
search
create_issue
delete_issue
that means the server advertises those operations.
It does not mean:
the user approved all three
or:
the model may invoke them automatically
The client still decides:
- whether the server is enabled for this chat;
- which tools are exposed to the model;
- which require approval;
- which are disabled entirely.
Keep discovery and permission as separate layers.
Tool identity is server-scoped
Tool names are not globally unique.
Two servers can both return:
search
or:
send_message
The host therefore needs an identity such as:
(server_connection_id, tool_name)
not just:
tool_name
This matters for:
- permission persistence;
- analytics;
- debugging;
- UI labels;
- result provenance.
A permission for Work GitHub / create_issue must not silently authorize Personal Tracker / create_issue.
A discovered tool is a contract snapshot
A useful normalized record might look like:
interface DiscoveredTool {
serverID: string
name: string
description?: string
inputSchema: JSONSchema
outputSchema?: JSONSchema
annotations?: Record<string, unknown>
discoveredAt: Date
sourceRevision: string
}
The exact fields vary by implementation.
The important idea is that the application knows:
- where the tool came from;
- which schema version/snapshot it saw;
- when that metadata was obtained.
Do not treat a description as executable policy
Descriptions are primarily model-facing and human-facing metadata.
A description might say:
Safely deletes old temporary files.
That sentence does not prove the operation is safe.
Authorization should use application policy and normalized arguments.
Descriptions can also be malicious or misleading if the server is untrusted.
Treat them as external content.
Tool schemas should be validated before use
A server can return malformed or unexpectedly complex schemas.
A defensive client should validate what it can support before passing a tool to a provider.
Checks can include:
schema parses
supported schema dialect/features only
reasonable depth
reasonable property count
reasonable description size
no unsupported recursive structure
valid required-property references
The goal is not to rewrite arbitrary JSON Schema.
The goal is to avoid feeding invalid contracts into a model provider and then blaming the provider for the failure.
Providers do not all accept the same schema subset
This is one of the most important reasons to normalize discovery separately from provider translation.
The MCP server may expose a schema that is valid for MCP but includes constructs a specific model API does not support.
The client should have a provider-adaptation step:
If translation would change semantics, fail clearly rather than silently weakening the schema.
See How to Design Good AI Tool Schemas.
Discovery can be lazy
A client does not necessarily need to query every configured server every time the app launches.
Possible strategies:
Eager
Discover all enabled servers at startup.
Pros:
- complete tool list immediately;
- capability errors appear early.
Cons:
- network cost;
- auth prompts;
- startup latency;
- unnecessary traffic for unused servers.
Lazy
Discover when a server becomes relevant to a chat.
Pros:
- less startup work;
- fewer needless auth/network operations.
Cons:
- first-use latency;
- capability changes surface later.
Cached with refresh
Use a previous snapshot, then refresh according to freshness rules.
This often gives the best UX if stale metadata is handled carefully.
Cache lifetime should come from protocol metadata when available
Modern MCP revisions can carry freshness metadata for list/read results.
The 2026-07-28 generation introduced cache-oriented metadata such as ttlMs and cacheScope for relevant list/resource results.
A client should respect server-provided freshness when it understands it.
Conceptually:
cache entry
server = github-work
operation = tools/list
expires = discoveredAt + ttlMs
scope = per-user / broader scope as declared
Do not invent a long-lived universal TTL if the server provides one.
Cache scope matters for multi-account clients
Suppose one MCP endpoint supports two authenticated accounts.
Account A may expose:
read_repo
create_issue
while Account B may expose:
read_repo
If the cache is keyed only by endpoint URL, the client can leak capability metadata across identities.
Cache keys may need:
server connection
credential/account identity
protocol revision
relevant server-declared cache scope
Do not use raw secrets as cache keys.
Use stable internal credential-profile identifiers.
Tools can change while the app is open
A server can add, remove, or modify tools.
Reasons include:
- server deployment;
- account permission changes;
- user configuration;
- installed integration changes;
- feature flags;
- organization policy.
A client should not assume discovery is immutable until restart.
Modern change notifications are opt-in
With the 2026-07-28 protocol generation, clients can use subscriptions/listen for supported change notifications instead of relying on unsolicited session notifications.
For tools, this can include tool-list changes when both sides support the relevant capability.
Conceptually:
client opens subscriptions/listen
client asks for tools-list changes
server acknowledges supported subset
server later emits tool-list-changed notification
client refreshes tools/list
The notification should normally trigger authoritative re-discovery.
Do not mutate a cached list based only on a vague “changed” signal.
Notifications can be missed
A subscription stream can drop.
The app can be suspended.
The server can restart.
Therefore:
Notifications are hints that state changed, not proof that unchanged state stayed unchanged forever.
After reconnect, revalidate authoritative discovery state according to your freshness policy.
Tool removal must revoke exposure immediately
Suppose a model turn was prepared with:
create_invoice
Then the server changes and the tool disappears.
Possible race:
T0 discover tool
T1 send tool schema to model
T2 server removes tool
T3 model proposes create_invoice
T4 client calls server
The call may fail because the tool no longer exists.
The client should surface a capability-change error, refresh discovery, and decide whether to retry the model turn.
Do not silently redirect to another similarly named tool.
Schema changes are more dangerous than tool removal
Suppose a tool changes from:
{
"amount": 10
}
to:
{
"amount_cents": 1000,
"currency": "USD"
}
A stale client might send old arguments that are now invalid.
Always validate model-generated arguments against the current tool contract immediately before execution.
See How to Validate AI Tool Arguments Safely.
Snapshot identity helps race handling
A robust design can associate a tool proposal with the discovery snapshot used to expose it.
For example:
ToolProposal {
serverID
toolName
discoveryRevision
arguments
}
Before execution:
if cached/current revision differs
revalidate against current schema
You do not need a globally standardized revision token for this; a local hash of the normalized contract can work as application state.
Hash normalized schemas, not raw formatting
If you want local change detection, raw JSON bytes may change because of formatting or property order.
Normalize first.
For example:
canonicalized schema
+ normalized description
+ annotations relevant to execution
Then hash that representation.
This avoids treating harmless serialization differences as semantic tool changes.
Discovery should produce a model-facing subset
Do not dump every tool from every server into every model request.
Problems include:
- huge context overhead;
- worse tool selection;
- privacy exposure;
- accidental access to irrelevant capabilities;
- provider tool-count limits.
A useful pipeline is:
Selection can be explicit or context-driven, but security filtering should happen before provider serialization.
Do not let model-facing aliases break audit identity
You may need to rename tools for provider compatibility or to avoid collisions.
Example:
MCP identities:
github-work / search
linear-work / search
Provider-facing names:
github_work_search
linear_work_search
That is fine as long as the host preserves the reverse mapping.
Audit records should still point to the real server/tool identity.
Tool descriptions can consume significant context
A server with many verbose tools can add thousands of tokens before the user message is even counted.
Track tool-schema cost as part of context budgeting.
Possible optimizations:
- expose only relevant tools;
- trim redundant descriptions only if semantics are preserved;
- cache token estimates;
- group rarely used servers behind explicit enablement.
Do not truncate schemas arbitrarily.
A broken contract is worse than a larger prompt.
Discovery failures need classification
tools/list can fail for different reasons:
network unreachable
auth expired
permission denied
protocol mismatch
malformed response
server bug
rate limit
unsupported operation
The UI should not reduce everything to:
No tools found
An empty list and a failed discovery request are not the same state.
Preserve the last known snapshot carefully
A stale snapshot can still be useful for UI continuity.
But distinguish:
last known tools
from:
currently verified tools
For example:
GitHub Work
Last checked: 2 hours ago
Unable to refresh: authentication expired
Do not let stale tools execute just because they are still displayed.
Reauthentication can change capabilities
After OAuth refresh or account switching, re-run discovery if capabilities can differ by authorization context.
This is especially important when a server conditionally exposes tools based on scopes or roles.
A changed access token can mean a changed tool surface.
Discovery metadata is not a security identity
Server-reported names and version strings are self-reported metadata.
Do not use a display name like:
"Official GitHub MCP"
as proof of server identity.
Trust should come from configured endpoint/authentication/TLS/application policy, not a string the remote endpoint tells you.
server/discover and tools/list solve different questions
At a high level:
server/discover -> server capabilities / metadata
while:
tools/list -> actual available tool contracts
A client may use discovery metadata to decide whether a capability is supported, but still needs the operation-specific list to know the current tools.
Do not treat one as a substitute for the other.
Tool annotations should be advisory unless your policy says otherwise
A protocol/server may expose annotations about a tool.
These can help UX and planning.
But a dangerous pattern is:
annotation says readOnly=true
therefore bypass approval
Only do that if your client explicitly trusts that annotation source for that policy and you understand the consequences.
For remote/untrusted servers, conservative defaults are safer.
A useful discovery state machine
unknown
-> loading
-> ready(fresh snapshot)
-> stale(last known snapshot)
-> refreshing
-> authRequired
-> incompatible
-> failed
This gives the UI and tool coordinator more information than a boolean hasTools.
Refresh before sensitive execution
For high-impact operations, it can be reasonable to require a fresh contract before execution.
Example:
if tool is destructive
and discovery snapshot is expired
refresh
revalidate arguments
then request approval/execute
Whether to do this for every call depends on latency and threat model.
Tool discovery should be testable without a real server
Build fixtures for:
empty list
one valid tool
same name on two servers
invalid schema
schema changes between refreshes
tool removed
very large tool list
auth-dependent list
expired cache
list_changed notification
subscription disconnect
provider cannot represent schema
A fake MCP server is far more useful than depending on public services in unit tests.
Example normalized flow
Notice that discovery is only one step in a larger controlled path.
Where BYOKchat fits
A multi-provider client can use MCP discovery to create one provider-neutral tool catalog, then adapt selected tools to whichever model is active in the conversation.
The client still owns:
- per-chat server enablement;
- per-tool permission policy;
- collision-free tool identity;
- discovery freshness;
- provider compatibility filtering;
- audit history.
That keeps tool availability dynamic without giving discovery metadata authority over execution.