The Modern Era (2026-07-28)
ModelContextProtocol.jl is a dual-era server. Alongside the classic initialize-handshake sessions (protocol versions 2024-11-05 through 2025-11-25), it serves the 2026-07-28 specification's stateless modern era: any request whose params _meta carries io.modelcontextprotocol/protocolVersion is validated and answered as a self-contained exchange — no handshake, no session state — on the same endpoint. Both eras share your tools, resources, prompts, and templates; you register components once and clients of either generation are served.
Nothing on the 2026-07-28 spec's server side is unimplemented: the sections below tour the modern surface and the handler APIs that drive it.
Stateless requests and server/discover
A modern request carries the protocol version and the client's capabilities in _meta, plus optional clientInfo and the per-request logLevel opt-in described below:
{
"jsonrpc": "2.0", "id": 1, "method": "tools/list",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}Validation is per-request: a missing required field is -32602, an unsupported version is -32022 with the supported list, and methods removed from the modern era (ping, logging/setLevel, resources/subscribe, resources/unsubscribe, tasks/result, tasks/list) are -32601. server/discover replaces the handshake for capability discovery — it returns supported versions across both eras, the modern capability set (including declared extensions), and the CacheableResult fields (ttlMs, cacheScope) the spec requires. Every modern result carries a resultType and identifies the server via io.modelcontextprotocol/serverInfo in its _meta.
Over Streamable HTTP, modern requests are validated against the SEP-2243 standard headers — MCP-Protocol-Version and Mcp-Method on every request, Mcp-Name mirroring the body on tools/call, prompts/get, resources/read, and the tasks/* methods — with -32020 (HTTP 400) on any mismatch, 404 for unknown methods, and sessions ignored (never minted).
Multi round-trip requests (MRTR)
The modern era has no server-initiated requests. A tool handler that needs client input (elicitation, sampling, roots) returns an InputRequired value; the server answers with resultType: "input_required", the requests to fulfill, and an integrity-protected requestState; the client retries tools/call with its inputResponses and the echoed state, and the handler simply runs again:
tool = MCPTool(
name = "greeter",
description = "Greets a confirmed user",
parameters = ToolParameter[],
handler = (args, ctx) -> begin
resp = get(input_responses(ctx), "who", nothing)
resp === nothing && return InputRequired(
Dict("who" => elicit_request("Who should I greet?")))
TextContent(text = "Hello!")
end)Build requests with elicit_request, sampling_request, or roots_request; read the retry's responses with input_responses and carried-over handler state with input_state. A request whose input requests need capabilities the client did not declare is rejected with -32021 naming them. The requestState token is HMAC-bound to the principal, a TTL, and a canonical digest of the original params — pass mrtr_state_key to mcp_server when running shared-key replicas.
The tasks extension (io.modelcontextprotocol/tasks)
Long-running work moves off the serial loop through the SEP-2663 tasks extension. Creation is server-directed: the client declares the extension in its per-request capabilities, and the handler decides — task_detach hands the running call off to a background task and immediately answers with a flat CreateTaskResult; the handler's eventual return value (or thrown error) becomes the task's terminal state, observable via tasks/get (results inlined; there is no tasks/result):
slow = MCPTool(
name = "slow_job",
description = "Expensive computation",
parameters = ToolParameter[],
task_support = :optional, # :required rejects non-declaring clients (-32021)
handler = (args, ctx) -> begin
task_detach(ctx) # false (and the call stays synchronous) when
# the client did not declare the extension
for step in 1:100
task_cancelled(ctx) && return TextContent(text = "stopped")
# ... work ...
end
TextContent(text = "done")
end)tasks/cancel is an idempotent empty ack (cooperative — poll task_cancelled); a tool-level isError result completes the task (failed is reserved for protocol errors); extension tasks and legacy SEP-1686 tasks share one era-tagged store, and neither era can see the other's records.
Mid-task input
After detaching, a handler can ask the client for input during execution with task_await_input: the task parks as input_required, tasks/get surfaces the outstanding requests under server-minted keys, and the client answers through tasks/update (partial fulfillment is accepted):
confirm = MCPTool(
name = "confirm_delete",
description = "Delete with confirmation",
parameters = [ToolParameter(name = "filename", description = "Target",
type = "string", required = true)],
task_support = :required, # mid-task input needs the detached task, so
# non-declaring clients are rejected up front
handler = (args, ctx) -> begin
task_detach(ctx)
resp = task_await_input(ctx, elicit_request("Really delete $(args["filename"])?"))
TextContent(text = "done")
end)Request params are frozen at registration (plain JSON data only), a task whose ttl elapses while parked is failed and its waiters unwound, and cancellation unwinds the handler with TaskCancelledException (distinguish expiry from a client cancel via task_cancelled(ctx)). Returning InputRequired before detaching runs the normal MRTR round, so a tool can gather input synchronously and then escalate — the SEP-2663 composition rule.
Status notifications
Clients subscribe to task status pushes through subscriptions/listen with a taskIds filter: every transition — parking on input, resuming, completing, failing, cancelling, expiring — delivers the task's complete state to subscribed streams, tagged with the subscription id. The acknowledgment echoes only ids the requestor could tasks/get, and deliveries re-check authorization per push.
Subscriptions
subscriptions/listen replaces the legacy GET stream and resources/subscribe: a long-lived request whose response stream carries only the notification types the client opted into (toolsListChanged, promptsListChanged, resourcesListChanged, resourceSubscriptions URIs, and taskIds). Announce changes server-side with notify_list_changed and notify_resource_updated.
Argument completion
completion/complete serves suggestions for prompt arguments and resource-template variables in both eras. Configure sources with the completions field on MCPPrompt / ResourceTemplate — a Vector{String} (served filtered by prefix) or a function receiving the partial value and the request context's resolved arguments:
prompt = MCPPrompt(
name = "city_report",
arguments = [PromptArgument(name = "city", required = true)],
messages = [PromptMessage(content = TextContent(text = "Report for {city}"))],
completions = Dict{String,Any}("city" => ["paris", "london", "tokyo"]))Header parameter mirroring (x-mcp-header)
For transport-level routing, a tool parameter may declare a header-mirroring suffix — ToolParameter(name = "route", description = "Routing key", type = "string", header = "Routing-Key"), or x-mcp-header in a raw input_schema — and modern HTTP clients mirror the argument as an Mcp-Param-<suffix> request header. The server validates every mirror against the body (strict Base64 sentinel handling, exact numeric comparison) and rejects violations with -32020/HTTP 400. Registration refuses invalid annotations: suffixes must be HTTP tokens, unique per tool, on string/integer/boolean properties reachable through plain properties chains.
Per-request logging
logging/setLevel does not exist in the modern era. A request opts into notifications/message delivery by setting io.modelcontextprotocol/logLevel in its _meta; records at or above that level are delivered on that request's own response stream only — including debug records below the operator's installed level — and requests without the opt-in receive none.