On this page
- The four layers to keep separate
- MCP is a capability protocol
- Tools
- Resources
- Prompts
- Extensions
- The current protocol is stateless at the core
- Stateless protocol does not mean stateless application
- Discovery is optional
- The host needs its own server registry
- Capability discovery needs provenance
- The model should receive a normalized tool view
- MCP does not define your permission policy
- Separate model proposal from tool execution
- Results are untrusted input too
- Multi-round interaction changed in modern MCP
- Do not trust requestState as application authority
- Long-running work belongs in Tasks
- Notifications are opt-in streams
- Transport and application lifecycle are different
- Authentication is not tool authorization
- Version support must be explicit
- Persist tool rounds semantically
- Keep secrets out of conversation history
- A practical client architecture
- Conversation orchestrator
- Tool coordinator
- Server registry
- Transport/client layer
- Failure modes to design for
- What the user should be able to see
- Test the boundaries, not just the happy path
- Where BYOKchat fits
- Further reading
Model Context Protocol is easiest to understand when you stop thinking of it as an “agent framework” and instead treat it as a protocol boundary between an AI application and external capabilities.
The model is not the MCP client.
The MCP server is not the model.
The protocol does not decide whether a tool is safe to run.
A production application sits in the middle and owns those decisions.
This separation matters because every security, UX, and reliability decision depends on knowing which layer owns which responsibility.
The four layers to keep separate
A useful architecture has four conceptual layers:
- Host application — conversation state, provider selection, permissions, UI, persistence, recovery.
- Model provider — produces text, reasoning, and tool-call proposals.
- MCP client implementation — speaks MCP to one or more servers.
- MCP server — exposes capabilities such as tools, resources, prompts, and extensions.
If these layers collapse into one abstraction, problems follow quickly:
model output accidentally becomes authorization
server metadata becomes trusted application state
protocol retries duplicate side effects
transport state leaks into conversation state
Keep them separate even if one SDK hides some of the boundaries.
MCP is a capability protocol
At a high level, an MCP server can expose several kinds of capabilities.
Tools
Tools are executable operations.
Examples:
search_issues
create_ticket
read_calendar
send_message
query_database
resize_image
A tool normally has:
- a name;
- a human-readable description;
- an input schema;
- optionally output metadata or annotations;
- a result returned after execution.
The host should treat a tool definition as a contract offered by the server, not as permission to invoke it automatically.
Resources
Resources expose readable data addressed by a URI-like identifier.
Examples can include:
file:///project/readme.md
repo://acme/mobile/issues/42
calendar://events/today
A resource is not necessarily a local file. The identifier is part of the server’s namespace.
The important client-side question is:
What data will be read, under which authorization context, and how will that content be presented to the model?
Prompts
Prompts expose reusable prompt templates or prompt-building operations.
A host can surface them as optional workflows, but should not confuse a server-provided prompt with a trusted system instruction.
Server content is still external input.
Extensions
Modern MCP also has an extension mechanism for behavior that should not live permanently in the core protocol.
Tasks are a key example in the 2026-07-28 revision.
A host should model extension support explicitly instead of assuming every server or SDK supports every extension.
The current protocol is stateless at the core
The 2026-07-28 MCP revision changed one of the most important architectural assumptions.
The protocol-level initialize / initialized handshake is gone from the modern wire format, and Mcp-Session-Id is gone as well.
A modern request carries the information needed to process that request.
Conceptually:
POST /mcp
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: search
with a JSON-RPC body describing the call.
That means two sequential requests may land on different server instances behind a normal load balancer.
The protocol no longer requires sticky routing for modern requests.
Stateless protocol does not mean stateless application
This distinction is critical.
A server may still need business state:
shopping basket
browser session
workflow draft
search cursor
job handle
transaction preview
The modern pattern is to make that state explicit.
For example:
{
"basket_id": "b_42"
}
The model or host then passes basket_id back to later tools.
That is different from hiding state in transport-session metadata.
Explicit handles are easier to:
- persist;
- inspect;
- audit;
- replay;
- route across server instances;
- include in permission decisions.
Discovery is optional
A modern client can ask a server for capabilities through server/discover.
But discovery is not a mandatory handshake that must happen before every request.
This changes client architecture.
Do not assume:
connect -> initialize -> hold session -> call tools
The modern mental model is closer to:
know endpoint
optionally discover
issue self-describing request
receive result
A client can still cache discovered metadata, but the cache needs freshness rules and invalidation behavior.
The host needs its own server registry
MCP does not replace application-level connection management.
A multi-server client still needs records such as:
interface MCPConnection {
id: string
displayName: string
endpoint: URL
authProfileID?: string
enabled: boolean
trustMode: "local" | "remote"
protocolPolicy: ProtocolPolicy
}
The application should not key everything by display name.
Use a stable local identity for each configured connection.
Two servers can expose a tool with the same name.
server A -> search
server B -> search
The real identity should include the server connection.
(connection_id, tool_name)
This becomes important for permissions, history, and debugging.
Capability discovery needs provenance
Suppose two servers both expose:
create_issue
They may have different schemas, behavior, auth, and side effects.
The host should persist or display provenance:
GitHub Work / create_issue
Internal Tracker / create_issue
A permission granted to one must not silently apply to the other.
See How MCP Tool Discovery Works.
The model should receive a normalized tool view
The model provider probably does not understand MCP directly.
The host usually translates MCP tool definitions into the provider’s tool-calling schema.
The reverse path also needs normalization:
provider tool proposal
-> host tool identity
-> MCP tools/call
-> MCP result
-> provider-specific tool-result message
That is why MCP support belongs in the application architecture, not in one provider adapter.
MCP does not define your permission policy
A server advertising a tool means:
This operation exists.
It does not mean:
The model may execute it without user approval.
A host can implement policies such as:
Ask
Always Allow
Disabled
or richer scopes:
Allow read-only tools
Ask for writes
Disable destructive tools
The permission decision should bind to:
- server identity;
- tool identity;
- normalized arguments;
- current user/account context;
- side-effect class.
See How to Build an MCP Client Permission System.
Separate model proposal from tool execution
The execution path should look like this:
The model never skips the host’s validation gate.
Results are untrusted input too
Tool results can contain:
- stale data;
- malicious instructions;
- user-generated content;
- HTML or Markdown;
- URLs;
- binary references;
- secrets the model should not receive.
Treat result content as data from an external source.
Do not grant it system-prompt authority.
See How to Prevent Prompt Injection From Tool Results.
Multi-round interaction changed in modern MCP
Older MCP designs used server-to-client requests such as elicitation during an active exchange.
The 2026-07-28 design introduces multi-round-trip requests for stateless operation.
A server can return an input-required result rather than relying on a persistent request channel.
Conceptually:
{
"resultType": "input_required",
"inputRequests": {
"confirm": {
"type": "elicitation",
"message": "Delete these files?"
}
},
"requestState": "opaque-state"
}
The client gathers input and retries the original operation with the responses.
This has two application consequences:
- interactive state must survive between attempts;
requestStatemust be treated as untrusted opaque data from the server.
See MCP Multi-Round-Trip Requests Explained.
Do not trust requestState as application authority
If the server sends:
requestState = ...
then the client may need to echo it back.
But the host should not interpret that opaque value as proof that:
- permission was granted;
- a user approved something;
- a previous side effect occurred;
- a particular account is active.
Application permissions belong in application-owned state.
If a server encodes sensitive state into such a token, it is the server’s responsibility to protect integrity and confidentiality appropriately.
Long-running work belongs in Tasks
Some tool calls cannot finish inside one request lifetime.
Examples:
large export
repository indexing
video processing
long deployment
bulk migration
The modern Tasks extension allows a server to return a task handle and let the client query progress later.
The important architecture rule is:
A task is durable application work, not merely a slow HTTP request.
The host should persist task identifiers and state if it wants recovery across app restarts.
See MCP Tasks Explained.
Notifications are opt-in streams
Modern MCP no longer assumes an unsolicited long-lived notification channel as part of every session.
For supported notification types, clients can open subscriptions/listen and request what they want to receive.
Examples include changes to tool/resource/prompt listings or resource updates.
This is operationally different from treating a transport connection as permanent application state.
A client should be prepared to:
- open a subscription deliberately;
- reconnect when it drops;
- reconcile authoritative state after reconnect;
- avoid assuming every notification was observed.
Transport and application lifecycle are different
A network request can end while the conversation continues.
A subscription stream can reconnect while the server configuration remains the same.
A task can outlive both.
Model them separately:
MCP connection configuration -> durable
HTTP request -> transient
subscription stream -> transient/reconnectable
task handle -> durable until terminal
conversation tool round -> durable app history
This separation makes crash recovery much easier.
Authentication is not tool authorization
A remote MCP server may use OAuth.
Successful OAuth means roughly:
The client has an access token accepted for this protected resource.
It does not answer:
Should the model invoke
delete_repositoryright now?
You still need host-side tool policy.
Keep these three layers distinct:
user authentication
OAuth/resource authorization
tool-execution approval
See OAuth for MCP Explained and How to Secure Remote MCP Servers.
Version support must be explicit
MCP has changed materially across revisions.
A robust client should know which behavior comes from which protocol generation.
Do not implement logic such as:
if endpoint contains /mcp, assume modern semantics
Instead, maintain explicit capability/version state and compatibility paths.
For example:
interface MCPProtocolProfile {
revision: string
statelessCore: boolean
supportsMRTR: boolean
supportsTasksExtension: boolean
supportsSubscriptionsListen: boolean
}
The exact representation is application-specific, but version-sensitive behavior must not be guessed from server names.
See How to Migrate MCP Clients Across Protocol Versions.
Persist tool rounds semantically
A conversation record should not save only rendered text.
A useful durable record can include:
model tool proposal
server identity
tool identity
normalized arguments
permission decision
MCP request ID
result status
sanitized result summary / durable result payload
next model response
This supports:
- crash recovery;
- auditing;
- debugging;
- export;
- model/provider switching.
Do not rely on one provider’s raw response object as the only history format.
Keep secrets out of conversation history
MCP authentication material belongs in secure credential storage.
Do not persist:
Authorization header
refresh token
client secret
session cookie
private custom header
inside normal conversation/tool records.
Store references to credential profiles instead.
A practical client architecture
Each component has a narrow responsibility.
Conversation orchestrator
Owns:
- current turn;
- model/provider selection;
- multi-round limits;
- cancellation;
- context rebuilding.
Tool coordinator
Owns:
- tool identity resolution;
- schema validation;
- permission checks;
- MCP execution;
- result normalization;
- retries only where safe.
Server registry
Owns:
- configured endpoints;
- protocol/capability profile;
- credential reference;
- enabled state;
- discovery cache.
Transport/client layer
Owns:
- JSON-RPC encoding;
- HTTP headers;
- protocol-version behavior;
- MRTR transport mechanics;
- subscription streams;
- extension messages.
This decomposition makes it possible to update MCP behavior without rewriting chat logic.
Failure modes to design for
A production MCP client should expect:
server unreachable
auth expired
tool disappeared
schema changed
malformed result
request timed out
subscription dropped
user denied approval
MRTR input cancelled
task failed
app restarted mid-task
provider asked for same tool again
Each needs a different recovery policy.
Do not turn them all into one generic “tool failed” state internally.
What the user should be able to see
For trust and debugging, expose at least:
- which MCP server is being used;
- which tool is requested;
- the arguments or a safe human-readable preview;
- whether approval is required;
- whether the operation is running, waiting for input, or a background task;
- the final status.
A model that says “I checked your calendar” is not enough evidence by itself.
Test the boundaries, not just the happy path
Useful tests include:
same tool name on two servers
server changes schema after discovery
OAuth expires before tools/call
client crashes after approval before execution
server returns input_required twice
requestState is malformed or unexpectedly large
task survives app restart
subscription drops and changes occur while offline
provider repeats a side-effecting call
legacy server requires older lifecycle behavior
The difficult bugs happen between layers.
Where BYOKchat fits
A provider-neutral BYOK client is a natural MCP host because the tool layer can remain independent from the selected model provider.
The model proposes an operation, but the application still owns:
- which MCP servers are enabled for the chat;
- which tools are exposed;
- per-tool permission policy;
- approval UI;
- tool-round persistence;
- multi-round orchestration.
That architecture keeps MCP useful without giving external servers or model output implicit authority over the user’s device or accounts.