Minor Changes
-
#1624
6032170Thanks @SamMorrowDrums! - Add request-time OAuth scope challenges for tools, resources, resource templates,
and prompts. Each primitive'sscopeChallengecallback receives the parsed
request and verified authentication info, then either continues or returns the
exact scope set for aninsufficient_scoperesponse.requireScopesprovides a
small helper for static all-of checks.createMcpHandlerand Streamable HTTP transports return HTTP 403 with an
insufficient_scopechallenge before handler execution or SSE setup. The
preflight is active whenever a registered primitive carries ascopeChallenge
callback — there is no handler- or transport-level configuration. The
challenge'sWWW-Authenticateheader is built by the same formatter as the
bearer-auth 401/403 answers, and itsresource_metadataparameter is derived
from the verifiedAuthInfo:requireBearerAuth/verifyBearerTokennow
stamp their configuredresourceMetadataUrlonto theAuthInfothey return
(new optionalAuthInfo.resourceMetadataUrlfield), with a fallback to the
well-known location for an HTTP(S) RFC 8707resourceidentifier; the
parameter is omitted when neither is available.
Patch Changes
-
#2726
6fa4227Thanks @LuckTerence! -SdkErrorandSdkHttpErroraccept standardErrorOptionsas an optional fourth constructor argument and forward it toError, so a wrapped error is reachable through the standardError.causechain. Version-negotiation probe failures (SdkErrorCode.EraNegotiationFailed) now use it: the underlyingTypeError: fetch failedand the DNS or socket error beneath it surface viaerror.cause, so pino, Sentry, andutil.inspectrenderENOTFOUND/ECONNREFUSED/ETIMEDOUTinstead of stopping at theSdkError(#2657). The previouserror.data.causeslot is still populated for compatibility but is deprecated and slated for removal; readerror.causeinstead. -
#2654
03842cdThanks @pshah19! - Treat request id0as a real id. Two guards tested aRequestIdfor truthiness, so the legal JSON-RPC ids0and''were read as absent. Id0is not a corner case: the outbound request counter is zero-based, so it is the first id every peer assigns, which on the server→client leg is the firstsampling/createMessage,elicitation/create, orroots/lista server sends.notifications/cancelledcarrying id0was ignored, and the in-flight handler ran to completion with itsAbortSignalnever fired.- A notification sent with
relatedRequestId: 0wrongly passed the debounce gate (for methods opted intodebouncedNotificationMethods). Because the pending set is keyed by method alone, a second such notification in the same tick was silently dropped rather than sent.
Absent is now the only value that means "no id".
-
#2668
3e90449Thanks @KKonstantinov! - Stop sendingnotifications/cancelledfor theinitializehandshake. The spec is explicit that a client MUST NOT attempt to cancel itsinitializerequest, but the outbound cancel path fired for any in-flight request: aborting theAbortSignalpassed toconnect(), or letting the handshake hit its timeout, put a forbidden cancellation on the wire naming the initialize request id.The local behaviour is unchanged — the caller's promise still rejects with the same abort/timeout error, and
connect()still tears the connection down. Only the wire notification is suppressed. Every other method keeps the existing cancellation path. -
#2698
7b781edThanks @maxisbey! - Read Streamable HTTP request bodies with a size limit. Every SDK-owned body read —
WebStandardStreamableHTTPServerTransport(and the Node transport built on it),
createMcpHandler,toNodeHandler, andcreateMcpHonoApp's JSON pre-parse — now stops at
4 MiB by default (the limit the legacy SSE transport already uses; the Express adapter and stdio
bound their reads too) and answers413 Payload Too Largebefore anything is parsed.
toWebRequest(when it reads the Node stream itself) now rejects once the body exceeds the
limit with an error whosenameis'RequestBodyTooLargeError'andstatusis413, and
toNodeHandleranswers that with413; hand-wired callers oftoWebRequestshould handle the
rejection or pass a pre-parsed body, andisLegacyRequestreports such a request as non-legacy
so the modern handler answers it. JSON-RPC batch arrays are limited to 100 messages; a longer
batch is answered400/-32600and none of it is dispatched.The limit is configurable with a new
maxRequestBodySizeoption (bytes, default
DEFAULT_MAX_REQUEST_BODY_SIZE= 4 MiB, exported from@modelcontextprotocol/server) on
WebStandardStreamableHTTPServerTransportOptions,CreateMcpHandlerOptions(forwarded to its
stateless legacy leg;isLegacyRequestandlegacyStatelessFallbacktake the same option),
CreateMcpHonoAppOptions, andToNodeHandlerOptions/ToWebRequestOptions(the adapter's
bound applies before the handler's, so raise both). The bounded reader is exported as
readRequestBodyfor adapter authors. Hosts that pre-parse the body and pass it as
parsedBodyskip the SDK's read and its size limit entirely; the batch bound applies either way.createMcpHonoAppandcreateMcpExpressAppnow run their Host/Origin validation before the
JSON body parser, so a request from a disallowed Host or Origin with an invalid JSON body is
answered403rather than400, and its body is not read. -
#2590
75dc7eaThanks @davidpavlovschi! - Reject a modern (2026-07-28) POST that omits the requiredMCP-Protocol-Versionheader.createMcpHandleraccepted a request whose body carried a valid per-request_meta
envelope but whoseMCP-Protocol-Versionheader was absent: the request was classified
modern, dispatched, and answered200— tool handlers ran. Only the mismatch case
(header present, disagreeing with the body) was rejected, so of the standard headers
SEP-2243 requires on a modern POST, presence was enforced forMcp-Method(and for
Mcp-Nameon the methods that mirrorparams.name/params.uri) but not for
MCP-Protocol-Version.Such a request is now refused with
400 Bad Requestand JSON-RPC-32020
(HeaderMismatch), matching the shape the sibling missing-header cells already emit and
echoing the request id — per the Streamable HTTP spec, which requires the header on every
POST and lists a missing required standard header as aHeaderMismatchfailure. The
spec's allowance to treat a header-less request as2025-03-26is available only to a
server that also serves pre-2025-06-18 clients, and permits routing it to legacy
handling — never serving it as 2026-07-28; underlegacy: 'reject'the requirement is
unconditional.Era classification is deliberately unchanged and stays body-primary: a proxy that strips
the header still must not change the era, so such a request is still classified modern
and is refused one rung later, atstandard-header-validation— the same rung that
already answers a missingMcp-Method. Legacy-era traffic is untouched, notifications
are unaffected, body-lessGET/DELETEsession operations are method-routed before
any header validation, and stdio serving (which has no HTTP headers) is not involved.Clients built with this SDK always send the header, so no first-party client is affected;
hand-rolled clients that omitted it must add it. -
#2494
6a05402Thanks @claude! -StdioServerTransportnow closes itself and firesonclosewhen its stdin ends or closes. The stdio binding says servers "SHOULD exit promptly when their standard input is closed" — stdin EOF is the primary graceful-shutdown signal, and on some platforms (notably Windows, where no signal is delivered when the parent goes away) the only reliable one. Previously the transport listened only fordataanderror, so when an MCP client hung up its end of the pipe (window closed, session restarted, host crashed) the server never noticed:onclosenever fired, nothing tore down, and server processes accumulated as zombies until killed by hand. The transport now attachesend/closelisteners on stdin that close the transport (idempotently —onclosestill fires exactly once ifclose()is also called), soServer/McpServerandserveStdiotear down through the existingonclosechain and a well-behaved server process exits naturally. Requests still in flight when stdin ends are aborted (their handlers observesignal.aborted) and their responses are not written: EOF means the client has hung up and is no longer waiting. A client that wants answers keeps stdin open until it has read them. -
#2613
70de0c8Thanks @jwcarman! - Emit and validate theMcp-Nameheader for tasks requests per SEP-2663's Streamable HTTP binding: the client transport now mirrorsparams.taskIdintoMcp-Nameontasks/get/tasks/update/tasks/cancel(previously omitted, causing conforming servers to reject every task poll with-32020 HeaderMismatch), and the server-side standard-header validation cross-checks it via the same sharedMCP_NAME_HEADER_SOURCEtable.On the server,
createMcpHandlernow answers a modern (2026-07-28)tasks/get/tasks/update/tasks/cancelPOST that omitsMcp-Name, or whose header disagrees withparams.taskId, with400/-32020(HeaderMismatch) at thestandard-header-validationrung, the same treatmenttools/call/prompts/get/resources/readalready get. Legacy-era (2025-11-25) tasks traffic is unaffected. Clients built with this SDK release send the header; hand-rolled clients that omitted it must add it. -
Updated dependencies [
dcc0102]:- @modelcontextprotocol/core@2.1.0