MCP 2025-11-25 Conformance (2026-07-21)
2.0.0 — MCP 2025-11-25 Conformance (2026-07-21)
Full compliance pass against the MCP 2025-11-25 specification: every transport,
utility and auth flow was audited against the spec and brought into conformance
(PRs #158–#185). The wire behavior and several error semantics changed as a result,
so this is a major release. See Migration notes below.
Breaking Changes
- Elicitation error semantics and wire format (#158, #159). Elicitation replies
are now proper JSON-RPC responses (the previous Streamable HTTP implementation
invented anelicitation/responserequest that no spec defines). Hosts get spec
error codes instead of fabricated user answers: no handler configured →-32601
(was an automatic'decline'); handler raised →-32603(was'decline');
undeclared mode →-32602(mode is checked before the handler); non-object or
scalarcontent→-32603instead of being transmitted.contentis omitted for
decline/canceland for out-of-band (url) accepts, per theElicitResultschema. - Sampling error semantics (#177). No handler →
-32601, handler exception →
-32603(both were the user-rejection code-1); tool-enabled sampling requests
(tools/toolChoice, SEP-1577) are rejected with-32602unless the host opts in
viasampling_supports_tools: true. - Declared client capabilities are derived from registered handlers (#160).
stdio and SSE no longer unconditionally declaresampling/elicitation; every
transport declares exactly what the host wired up beforeconnect(elicitation
modesform+url,roots.listChanged,sampling). Compliant servers will stop
sending requests your host never handled — previously they were answered with
fabricated declines. - Protocol version negotiation is enforced (#161). If the server's
initialize
result carries an unsupported or missingprotocolVersion(supported:2025-11-25,
2025-06-18,2025-03-26,2024-11-05), the client disconnects and raises
MCPClient::Errors::ConnectionError. Non-object initialize results also fail the
connection (#161, #172). - Server capability gating (#173).
subscribe_resource,unsubscribe_resource,
complete,list_tasksandcancel_taskraise the new
MCPClient::Errors::CapabilityErrorwhen the server did not negotiate the
corresponding capability (the lifecycle forbids using un-negotiated capabilities).
Client#log_level=now skips servers without theloggingcapability instead of
raising on the first one — its return value only covers logging-capable servers. - Timeouts no longer re-send (#178). A request that exceeds its timeout raises the
newMCPClient::Errors::RequestTimeoutError(aTransportErrorsubclass — existing
rescues keep working) and is excluded from automatic retries, because the server may
still be executing it; a best-effortnotifications/cancelledis sent instead
(never forinitialize; task-augmented calls usetasks/cancel). Previously
timed-out requests were retried up toretriestimes, risking double execution. - Roots are validated (#169).
MCPClient::Root(andClient.new(roots:)) raises
ArgumentErrorfor non-file://URIs,..traversal segments (checked after
percent-decoding), and non-Hash_meta. - PKCE is mandatory (#165). The OAuth flow refuses to proceed (raises
ConnectionError) when the authorization server does not advertise
code_challenge_methods_supportedincludingS256, instead of silently continuing
without PKCE. - OAuth challenge parsing is Bearer-scoped (#163).
WWW-Authenticateparameters
are read from the Bearer challenge's own segment only (quoted-string aware), a 403
witherror="insufficient_scope"raises the new
MCPClient::Errors::InsufficientScopeError(aConnectionErrorsubclass exposing
#scope/#error_description), challenge-advertised scopes take priority for the
next authorization, and a challenge-advertisedresource_metadataURL is
authoritative (no silent fallback to well-known paths). _metamoved out of tool arguments (#179). Request-level_metasupplied in
call_tool/get_promptarguments (as:_metaor'_meta') is hoisted to the
JSON-RPCparamslevel on the wire on every transport, instead of being serialized
as a tool argument (where it could fail the tool's input schema).- Streamable HTTP resumability follows SEP-1699 (#168, #181).
Last-Event-IDis
no longer sent on POST requests; interrupted response streams are resumed via GET
with the per-stream cursor, honoring the server'sretry:directive. - Legacy SSE fixes change failure modes (#172). Server JSON-RPC error responses
now surface immediately asMCPClient::Errors::ServerError(previously the request
hung until the read timeout); an endpoint URL that cannot be resolved fails
connectwithConnectionError; the negotiatedMCP-Protocol-Versionheader is
sent on subsequent HTTP requests. - Session handling (#162). Session IDs are validated against the spec charset
(any visible ASCII, 1–4096 chars — JWTs and base64 IDs now accepted; the previous
[A-Za-z0-9_-]{8,128}rule rejected them), and an HTTP 404 on a session-bearing
request transparently re-initializes and re-sends once (Streamable HTTP session
expiry recovery). taskSupport: "required"without a task-capable server is a plain call (#174).
Per tasks tool-level negotiation, when the server lackstasks.requests.tools.call
the tool'sexecution.taskSupportis disregarded entirely — previously the client
raisedToolCallError.- stdio shutdown and encoding (#171).
cleanupcloses stdin and gives the server
a grace period to exit before SIGTERM/SIGKILL (previously immediate); pipes are
pinned to UTF-8 so multibyte content cannot corrupt framing on non-UTF-8 locales.
New Features
- Streamable HTTP POST SSE streams (#158): server requests and notifications
interleaved on a POST response stream are dispatched (elicitation/sampling/roots/
ping work mid-call), instead of the first event being taken as "the response". - Resumability (#168, #181): GET-based resumption with
Last-Event-ID, per-stream
cursors, and support for the SSEretry:directive (includingretry: 0). - Progress tracking (#179):
client.call_tool(name, args, progress: ->(progress, total, message) { ... })
auto-generates aprogressToken, routes matchingnotifications/progressto the
callback while the request is active, and drops stale tokens afterwards. - Per-request timeouts (#178):
Client#send_rpc(..., timeout:)and
ServerBase#rpc_request(..., timeout:)override the per-serverread_timeout. - Client identity & server instructions (#180):
Client.new(client_info: {...})
sends a host-providedImplementationasclientInfo;server.instructions
exposes the server'sinitializeinstructions hint. - Sampling tool calling, SEP-1577 (#177):
sampling_supports_tools: truedeclares
sampling.toolsand forwardstools/toolChoiceto the handler (optional fifth
handler argument receives the full request params). - Structured content validation (#176): tool results with
structuredContentare
validated against the tool'soutputSchema—validate_structured_content: :warn
(default) logs mismatches,:strictraisesValidationError; unsupported schema
keywords are surfaced transparently. - OAuth: Client ID Metadata Documents, SEP-991 (#175) via
client_id_metadata_url:(skips dynamic registration when the AS supports CIMD);
scope step-up viaInsufficientScopeError(#163); authorization applied to every
HTTP request including SSE GETs and pong/response POSTs (#167). - Model metadata (#170):
icons,titleand_metaparsed and exposed on
Tool,Prompt,ResourceandResourceTemplate. - Capability introspection (#173):
ServerBase#capability?('tasks', 'list')and
require_capability!are public API. - Tasks related-task metadata (#174):
io.modelcontextprotocol/related-task
_metais echoed on responses to server requests issued within a task context. - New error classes:
CapabilityError,RequestTimeoutError,
InsufficientScopeError.
Bug Fixes
- Streamable HTTP: server requests arriving on a POST SSE stream are answered instead
of being mistaken for the call's response; lone responses with mismatched IDs are
tracked per stream (#158). - Elicitation over Streamable HTTP uses real JSON-RPC responses, so compliant servers
(e.g. FastMCP) receive answers they understand (#159). - Legacy SSE: JSON-RPC error responses are delivered to waiters; connection failures
during endpoint resolution surface throughwait_for_connection(#172). - OAuth: Bearer tokens embedded in quoted parameter values no longer confuse
challenge parsing (quoted-string masking) (#163). - stdio: tolerates non-object JSON lines on stdout without killing the reader (#171).
- Cancellation is suppressed for requests that must not be cancelled (
initialize)
and for task-augmented calls (#178).
Examples & Tooling
- The Streamable HTTP echo server implements the full tasks feature (capability,
background_worktool,tasks/get|result|list|cancel, status notifications), and
tasks_example.rbruns the complete lifecycle against it locally (#184, #185). - The elicitation demo server accepts standard JSON-RPC
ElicitResultresponses
(previously it only understood the pre-2.0 invented method) (#182). - The two OpenAI examples pin their intended gem (
openaivsruby-openaiboth
providelib/openai.rb) onto the load path explicitly (#182). - The Anthropic example surfaces API error bodies (e.g. billing errors) and rejects
an emptyANTHROPIC_API_KEY(#183). - README documents all new public APIs (#182) and the supported protocol revisions.
Migration notes
Upgrading a host application:
- If you rescued elicitation/sampling failures by inspecting fabricated
'decline'
results or the-1error code, switch to the JSON-RPC codes above. - Wrap
subscribe_resource/complete/list_tasks/cancel_taskcalls in
rescue MCPClient::Errors::CapabilityError(or checkserver.capability?first)
if you talk to servers that do not negotiate those capabilities. - If you relied on timed-out requests being retried, retry explicitly — and treat
RequestTimeoutErroras "outcome unknown", not "not executed". - Audit
roots:values: onlyfile://URIs without traversal segments are accepted. - If a server you depend on omits
protocolVersionor answers with an unknown
revision, it will no longer connect — fix the server or pin an older gem. - Exact-class checks (
instance_of?) onTransportError/ConnectionErrorwill not
match the new subclasses;rescuehierarchies are unaffected. Client.newgained keyword arguments only (sampling_supports_tools:,
client_info:,validate_structured_content:); existing positional usage is
unchanged.
Upgrading a server implementation tested against this client:
- Expect elicitation answers as JSON-RPC responses (id echoing your request), not
elicitation/responserequests. - Expect request-level
_metainparams._meta, not insideparams.arguments. - Expect
Last-Event-IDon GET resumption requests only,MCP-Protocol-Versionon
legacy SSE POSTs, andnotifications/cancelledafter client-side timeouts. - Declared client capabilities now reflect what the host registered — do not send
elicitation/sampling requests unless the capability was declared.
1.1.0 (2026-07-04)
Breaking Changes
- Tasks API rewritten to conform to MCP 2025-11-25. The previous implementation
targeted atasks/createmethod that does not exist in the specification.- Removed
Client#create_task. To create a task, augment atools/callvia the new
Client#call_tool_as_task(name, arguments, ttl:), which returns aMCPClient::Task. MCPClient::Taskfields renamed and reduced to the spec set:id→task_id,
state→status; addedstatus_message,created_at,last_updated_at,ttl,
poll_interval; removedprogress,total,message,result,progress_token,
andprogress_percentage. Statuses are nowworking,input_required,
completed,failed,cancelled(waspending/running/…).Client#get_taskandClient#cancel_tasknow send thetaskIdparameter (wasid)
and return aTaskwith the new field set.
- Removed
New Features
Client#call_tool_as_task— create a task by augmentingtools/call(gated on the
server'stasks.requests.tools.callcapability and the tool'sexecution.taskSupport).Client#get_task_result(tasks/result) — retrieve the underlying task result.Client#list_tasks(tasks/list, paginated).- Tool-level task negotiation:
Tool#task_support,#supports_task?,#task_required?,
#task_optional?,#task_forbidden?(parsed fromexecution.taskSupport). - The client handles
notifications/tasks/statusserver notifications. (It does not
declare a clienttaskscapability: that marks a task receiver for
sampling/elicitation, which is not implemented — the client is a task requestor for
tools/callonly.) - Automatic pagination:
list_toolsandlist_promptsnow follow the server's
nextCursorand return the complete set across all pages, with a per-call safety
bound and an identical-cursor loop guard. No manual cursor handling is required (#148).
Bug Fixes
- Tool annotations: corrected
readOnlyHint/destructiveHintdefaults to match the
MCP 2025-11-25ToolAnnotationsschema — an un-annotated tool is treated as writable,
potentially destructive, and open-world (#140). - Ping utility: the stdio and SSE transports now respond to a server-initiated
ping
with an empty result (#141). - stdio deadlock: drain the subprocess's stderr so a server that writes heavily to
stderr can no longer block the pipe (#142). - MCP lifecycle: HTTP and Streamable HTTP transports now send
notifications/initializedafterinitialize, as the specification requires (#143). - stdio memory leak: bound the pending-response map so responses to abandoned
requests can no longer accumulate (#144). - Logger: stop overwriting a caller-supplied logger's formatter (#145).
- Ruby 3.4+: declare
base64as a runtime dependency to avoid aLoadError(#146). - Retry safety: application-level
ServerErrors are no longer retried; only transport
errors and transient HTTP 5xx responses (TransientServerError) are retried, so
non-idempotent requests are not executed twice (#149). - SSE reconnection: repaired the auto-reconnect path that could never fire because the
monitor thread killed itself (#150). - OAuth discovery: authorization-server and protected-resource metadata discovery now
follows RFC 8414 and RFC 9728 (#151).
Examples & Tooling
- Added
examples/run_all_examples.sh, a pre-release harness that boots each example's
server, runs every example, and reports PASS/FAIL/SKIP, plus anexamples/README.md
index. Fixed stale examples (removed the retired Playwrightbrowser_installcall,
robust first-tool selection instreamable_http_example.rb, updated the Gemini model)
and wired the Zapier/OAuth examples through a gitignoredexamples/secrets.env(#153).
Documentation
- Fixed the README OAuth snippet
requireand corrected method names in the changelog (#147).
Dependencies
- Bumped
faradayto 2.14.3, plus routine development-dependency updates.