MCP protocol versions, including 2026-07-28
The DVARA MCP Gateway serves three MCP protocol revisions on one endpoint,
/mcp:
| Revision | How a client connects |
|---|---|
2026-07-28 | No handshake and no session. Each request carries its own version and client details. |
2025-11-25 | initialize, then requests. |
2025-06-18 | initialize, then requests. |
Clients on all three can use the same Gateway at the same time. Every tool call passes the same governance checks and writes the same audit evidence, whatever revision the client speaks.
Not included in DVARA Open Source.
Why 2026-07-28 matters
MCP 2026-07-28 is stateless. A client does not open a session first, and the
server keeps nothing between requests. Each request names its protocol version,
its capabilities and, usually, the client's name and version. It also carries
three headers, MCP-Protocol-Version, Mcp-Method and Mcp-Name, so a load
balancer can route it without reading the body.
For a governed deployment, this means:
- Any Gateway replica can answer any request. You do not need sticky sessions in front of the Gateway.
- The audit record names the client on every call. Each record carries the protocol revision and the client's self-reported name and version. These are evidence, not identity: access is still decided by the API key or OAuth token.
- A header cannot hide a different call. If the headers and the body name different tools, methods or versions, or carry different tool arguments, the Gateway refuses the request before any upstream server sees it.
Connect a 2026-07-28 client
Point the client at https://<your-gateway>/mcp with a workspace API key, as
for any MCP client. A request looks like this:
curl -s https://<your-gateway>/mcp \
-H "Authorization: Bearer $DVARA_API_KEY" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: tools/list' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{
"io.modelcontextprotocol/protocolVersion":"2026-07-28",
"io.modelcontextprotocol/clientInfo":{"name":"my-agent","version":"1.0"},
"io.modelcontextprotocol/clientCapabilities":{}}}}'
To learn what the Gateway offers before calling anything, send
server/discover the same way. The answer lists the supported revisions,
newest first, and the capabilities your workspace can use.
Tool names keep the Gateway's form, serverId__toolName, on every revision.
From 1.9.0 a server's own address, /mcp/<serverId>, serves its tools under
their own names, on every revision; see
Connect to one server.
What a 2026-07-28 client sees
- Every result carries
resultTypeand the Gateway's name and version. - Tool, prompt and resource lists, resource reads and
server/discovercarry caching hints:ttlMs, how long the client may reuse the answer, andcacheScope: "private". See How long a client may reuse a list. - A request without the required headers, or without the protocol version and
client capabilities in
_meta, is refused with400and the error MCP defines for it. - A version the Gateway does not serve is refused with
400. The error lists the versions it does serve, so the client can retry with one of them. - Methods that
2026-07-28removed, such asinitializeandping, return404with "method not found".
Tool arguments in headers
A tool's input schema can mark a parameter with x-mcp-header. A 2026-07-28
client then sends that argument twice: in the body, and in an
Mcp-Param-{name} header, so a load balancer or policy in front of the Gateway
can route on it without reading the body. For example, if region is marked
"x-mcp-header": "Region", a call with "region": "us-west1" also sends
Mcp-Param-Region: us-west1. A value that is not plain ASCII, or has spaces at
either end, is sent Base64-encoded as =?base64?…?=.
The Gateway checks these headers against the body, so that a header can never
say something the call does not do. It refuses the call with 400 and MCP's
header-mismatch error (-32020) when:
- an argument the schema marks is in the body, but its header is missing;
- the header and the body disagree (numbers are compared as numbers, so
42.0and42agree); - the header is sent twice, is too long, contains characters a header may not carry, or is Base64 that does not decode;
- the header is sent, but the body has no value for that argument.
An argument that is absent or null needs no header. An Mcp-Param-… header
that no parameter is marked for is ignored, as the protocol says. Each refusal
is counted and written to the audit trail with the header's name, never its
value. To see which parameters are marked, look at the tool's inputSchema in
tools/list.
Be told when your tools, prompts or resources change
A 2026-07-28 client can ask the Gateway to tell it when its tools, prompts or
resources change, so it does not have to keep asking. It sends
subscriptions/listen and keeps the answer open:
{
"jsonrpc": "2.0",
"id": 1,
"method": "subscriptions/listen",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {}
},
"notifications": {
"toolsListChanged": true,
"promptsListChanged": true,
"resourcesListChanged": true,
"resourceSubscriptions": ["dvara-mcp://notes/file:///release.md"]
}
}
}
Ask only for what you need. The Gateway never sends a kind of notification you did not ask for.
The answer is a stream of events:
- First, an acknowledgment that names what the Gateway will send. It leaves
out what it cannot honour for you:
promptsListChangedorresourcesListChanged, when none of your workspace's servers offers prompts or resources, or when you chose a virtual server (it offers tools only);- any resource URI your key cannot read, or more than 16 URIs.
- Then
notifications/tools/list_changedeach time the tool list your key would get changes: a server is added, removed or quarantined, its tools are synced again, or a virtual server's allowed tools are edited. When you get one, calltools/listagain. notifications/prompts/list_changedornotifications/resources/list_changedwhen the prompt or resource list your key would get changes, andnotifications/resources/updatedwith the resource'suriwhen a resource you subscribed to changes. Read it again withresources/read.- Changes in other workspaces, and changes to anything your key may not see, are never sent to you.
- After 30 minutes the Gateway ends the stream with a final result, and your client opens a new one. If the Gateway restarts, the stream ends the same way.
Every 15 seconds the Gateway checks your tool list, your key and your workspace. If the key is revoked or has expired, or the workspace is suspended, the stream ends with an error.
Prompts and resources live on your upstream servers. About once a minute the
Gateway reads your prompt list, your resource list and each resource you
subscribed to, exactly as your own prompts/list, resources/list or
resources/read would, with the same policies. It compares the answer with the
last one and tells you when it differs. So a change can take up to a minute to
reach you, and works with every kind of upstream server. These reads count as
your key's own calls: they are recorded in the audit log and count toward its
rate limit. Streams that use the same key on the same Gateway instance share one
read. A read that fails is never reported as a change.
Each gateway instance allows a limited number of open streams, in total and per
workspace. Over the limit, the request is refused with 429. Every stream
opened, ended or refused is recorded in the audit log.
server/discover says tools.listChanged, prompts.listChanged,
resources.listChanged and resources.subscribe are true on 2026-07-28.
On 2025-11-25 and 2025-06-18, initialize still says false: those clients
fetch the list again to see changes.
Operators can change the check intervals (poll-interval, and
upstream-poll-interval for prompts and resources), the stream lifetime and
the limits (max-resource-subscriptions among them) with
dvara.mcp-gateway.subscriptions.*, or turn the feature off.
Clients on older revisions
Clients on 2025-11-25 and 2025-06-18 are served as before. They start with
initialize, and the Gateway agrees 2025-11-25 or 2025-06-18. If such a
client sends the Mcp-Method, Mcp-Name, MCP-Protocol-Version or an
Mcp-Param-… header and it disagrees with the body, the request is refused with
the error code MCP defines for a header mismatch. The headers stay optional on
these revisions: a header that is not sent is not required.
The older revisions are not being switched off. If one is ever retired, it will be announced in advance, with a date.
How the Gateway finds a request's revision
The Gateway reads the revision from the request itself, in this order:
- The
MCP-Protocol-Versionheader. - If there is no header, the protocol version in the request's
_meta. - For an
initializerequest, the revision the handshake agrees. - Otherwise, the request names no revision.
What happens next:
| The request | The Gateway |
|---|---|
names 2026-07-28, 2025-11-25 or 2025-06-18 | serves it by that revision's rules |
names any other revision, in the header or in _meta | refuses it with 400 and MCP's "unsupported protocol version" error (-32022), which lists the three revisions it serves |
is an initialize that asks for a revision the Gateway does not serve | answers with 2025-11-25, as the handshake rules require, and lets the client decide |
| names no revision | serves it as an older client (see below) |
When a request names no revision
The 2025-06-18 and 2025-11-25 specifications say that a server which gets no
version header, and has no other way to tell, should assume 2025-03-26. The
Gateway keeps no session between requests, so it has no other way to tell, and
follows that rule.
In practice the rule changes one answer. If such a request calls a tool with
invalid arguments, the Gateway returns a JSON-RPC error (-32602), as
2025-03-26 and 2025-06-18 specify. A client on 2025-11-25 or 2026-07-28
gets a tool result marked as an error instead, which a model can read and
correct. To get that answer, send the MCP-Protocol-Version header, which
clients on 2025-06-18 and later must send anyway.
See which revisions your clients use
The Gateway counts every /mcp request by revision in the Prometheus metric
mcp_requests_by_protocol_version_total. Its revision label is one of:
2026-07-28,2025-11-25or2025-06-18none, for requests that named no revisionunsupported, for requests refused for their revision
The label never holds the text a client sent, so the metric has at most five series. Use it to find clients that still use an older revision before you plan an upgrade.
The audit record of each governed call also shows where its revision came from.
mcp_protocol_version_source is header, meta or none.
How long a client may reuse a list
On 2026-07-28, MCP lets a server say how long a client may treat a list as
fresh (ttlMs, in milliseconds) and who may keep a copy (cacheScope). The
Gateway decides both itself.
Who may keep a copy: only the caller. Every list the Gateway gives is built
for one workspace and one key, so cacheScope is always "private". MCP says a
private answer must not be shared between callers: a client or proxy must keep
a separate copy for each key. An upstream server that marks its own list
"public" does not change this, because your workspace sees a filtered
version of that list.
How long: 15 seconds at most, by default.
| Result | ttlMs |
|---|---|
tools/list, server/discover | 15 seconds, and never longer than the subscriptions/listen check interval. A client that keeps its list is never further behind than one that listens for changes. |
prompts/list, resources/list, resources/templates/list, resources/read | The shortest time any of your upstream servers asked for, cut to 15 seconds. 0 (fetch again) if a server gave no time, as servers on older revisions never do, or if a server could not be reached. |
A retried request that carries inputResponses or requestState | 0. MCP says these answers must not be cached. |
A long time asked for by an upstream server is cut to the limit, never passed on. A server could otherwise ask your clients to keep its list for a month.
A reused list never lets a client do more than it may. Every tool call, resource read and prompt request is checked when it is made, so a tool quarantined a moment ago is refused even if a client still lists it.
Operators set the limit with dvara.mcp-gateway.caching.max-ttl (default
15s). 0 asks every client to fetch every list again each time.
2025-11-25 and 2025-06-18 have no caching hints, and the Gateway adds none.
The Gateway's own tool catalog does not follow a server's hints. When you sync a server's tools, the Gateway reads them again, whatever the server said about caching, and keeps them until the next sync. The server whose tools are being checked does not get to choose how often they are checked. The sync's audit record shows what the server asked for, so you can see it.
Upstream MCP servers on 2026-07-28
The Gateway is also an MCP client: it calls the MCP servers you register. For a server reached over Streamable HTTP or STDIO, it first asks the server which revisions it speaks, then uses the newest one both sides support:
| The upstream server speaks | The Gateway calls it on |
|---|---|
2026-07-28 (alone or with older revisions) | 2026-07-28, with no handshake and no session |
only 2025-11-25 or 2025-06-18 | that revision, through initialize, as before |
| only revisions the Gateway does not know | nothing: the call fails with mcp_protocol_version_unsupported |
You do not configure this. The Gateway remembers each server's answer for 15 minutes, and asks again sooner when you change the server's URL or credential, or when the server refuses the revision it remembered.
STDIO servers. A server the Gateway runs as a local process is asked the
same question, as the first message the process gets. A server that answers
as a 2026-07-28 server is called on that revision, with no handshake. One
process serves every call to that server on each Gateway instance, and the
Gateway remembers the answer for as long as the process runs. A server that
answers with any other error, or does not answer within 10 seconds, is
treated as an older server: the Gateway stops that process and starts a new one
with initialize, as before. A STDIO server gets no headers and no credential
from the Gateway; it reads its own settings from its environment.
On 2026-07-28 the Gateway sends what the revision requires on every request:
the version, method and name headers, and its own name, version and
capabilities in _meta. If a tool's input schema marks a parameter with
x-mcp-header, the Gateway also sends that value as an Mcp-Param-… header,
so a load balancer in front of the server can route on it. A tool whose
x-mcp-header markings break the protocol's rules is left out of the tool
list, as the protocol requires. The Gateway builds these headers from the arguments
it sends; it never passes on your client's own Mcp-Param-… headers.
What stays the same on either revision:
- Governance. Every call passes the same policy, PII, injection, approval and audit checks before it reaches the server.
- Network protection. Calls go through the same address check and response size limits.
- Evidence. The audit record of each tool result names the revision the Gateway used with the upstream server, beside the revision your client used with the Gateway.
- Progress. When your client sends a
progressToken, the progress the server reports during the call reaches your client under your token, on either revision (see a long tool's progress).
When a 2026-07-28 server needs more input to finish a tool call, read a
resource or give a prompt, such as an answer from the user, it says so in its
result. The Gateway applies the same workspace rules on all three: it checks
each request first, passes the allowed ones to your client, and sends your
client's answers back to the server unchanged, with the server's own state.
Every retry is checked and recorded like any other call, and goes only to a
server in your workspace. A request for a model call (sampling) is never passed
to your client: it is refused, or made by the Gateway through the LLM Gateway
if the workspace chose that (sampling).
Current limits
- Upstream servers reached over the older HTTP+SSE transport are called on
2025-11-25or2025-06-18. subscriptions/listenlearns of prompt and resource changes by reading them again, about once a minute. It does not yet use an upstream server's own change notifications.- On
2025-11-25and2025-06-18, a server cannot ask your client a question in the middle of a call (elicitation). Use2026-07-28for servers that ask for input. - Tasks (the
io.modelcontextprotocol/tasksextension) work only on2026-07-28, and only when the client, the workspace and the server all agree to them (run long tool calls as MCP Tasks). On the older revisions a long tool call runs as an ordinary call. - MCP Apps (the
io.modelcontextprotocol/uiextension) work the same way: only on2026-07-28, and only when all three agree (govern MCP Apps). Otherwise tools are listed without their page.