SEP-2532: Resource Streaming for Binary Content Delivery#2532
Open
patrick-rodgers wants to merge 5 commits intomodelcontextprotocol:mainfrom
Open
SEP-2532: Resource Streaming for Binary Content Delivery#2532patrick-rodgers wants to merge 5 commits intomodelcontextprotocol:mainfrom
patrick-rodgers wants to merge 5 commits intomodelcontextprotocol:mainfrom
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
SEP-2532: Resource Streaming for Binary Content Delivery
Abstract
This SEP proposes adding a new
resources/streammethod to the Model Context Protocol, enabling servers to deliver resource content as raw binary HTTP streams rather than base64-encoded JSON-RPC payloads. The client sends a standard JSON-RPC request; the server responds with stream metadata including adownloadUrl. The client then performs a standard HTTP GET against that URL — using the MCP session's authorization credentials — and receives raw binary bytes with no JSON-RPC envelope, no base64 encoding, and no intermediary framing.This mirrors established patterns in cloud APIs (e.g., Microsoft Graph's
/contentendpoint returning a redirect to a binary download stream) and leverages HTTP's native capabilities for chunked transfer, range requests, and content negotiation.Both servers and clients opt into streaming support through capability negotiation, ensuring full backward compatibility. Existing
resources/readbehavior remains unchanged for context-window-oriented content delivery. This SEP is scoped to HTTP-based remote transports; stdio transport is explicitly out of scope.Motivation
The Base64 Scaling Problem
The current
resources/readmethod returns content as eithertext(UTF-8 string) orblob(base64-encoded binary) within a JSON-RPC response envelope. This design works well for small resources destined for a language model's context window, but creates significant problems at scale:Memory pressure: Base64 encoding inflates content by ~33%. The server must hold the entire encoded payload in memory to construct a valid JSON-RPC response. For a remote server hosting thousands of concurrent users reading multi-megabyte files (e.g., OneDrive documents, GitHub repository archives, SharePoint assets), this means gigabytes of unnecessary memory overhead.
No streaming semantics: JSON-RPC responses must be complete before delivery. A 50MB PDF must be fully base64-encoded, wrapped in JSON, and buffered before a single byte reaches the client. This creates latency spikes and timeout risks.
Context window waste: When a client needs a file's bytes (to save to disk, upload elsewhere, or pass to a tool), routing those bytes through base64 → JSON → parse → decode is pure overhead. The content was never intended for the LLM.
Transport mismatch: Modern transports (Streamable HTTP, WebSocket) natively support binary streaming, but MCP forces all content through a text-based JSON-RPC envelope regardless of transport capabilities.
Real-World Use Cases Blocked Today
The following scenarios are impractical or broken with
resources/readalone:Cloud file storage (OneDrive, SharePoint, Google Drive, S3): Files range from kilobytes to gigabytes. Encoding a 100MB PowerPoint as base64 in a JSON-RPC response is not viable for remote servers at scale. (See #527 — "Base64 in JSON RPC will not scale for file content")
Repository content (GitHub, GitLab): Repository archives, large binaries, and media assets need to be delivered to clients for local caching or forwarding. GitHub's MCP server team has identified this as a scaling concern for remote servers at high concurrency. (#527 comments)
Document processing pipelines: A client may read a PDF resource from one MCP server and upload it to another (OCR service, document converter). The bytes never need LLM context — they're piped between servers. Today, the bytes are needlessly base64-encoded, JSON-wrapped, parsed, and decoded in the middle.
Local file synchronization: An IDE client may want to download resources to the local filesystem for editing, diffing, or version control. Streaming to disk is natural; buffering the entire file as a JSON string is not.
Why Not Out-of-Band Downloads?
Previous proposals (PR #607
uriScope/supportsDirectRead, download URLs) solve this by exiting the protocol — the server provides a URL and the client fetches directly via HTTP. These approaches have merit but also significant limitations:Servers must become HTTP file servers: Not every MCP server can host download endpoints. Servers behind NAT or in containers may not be directly reachable for arbitrary HTTP requests.
Auth fragmentation: Direct download URLs require separate auth handling (presigned URLs, token forwarding, OAuth token passing). MCP already has an auth model; ad-hoc exit from the protocol means reimplementing it.
Network topology assumptions: The server doesn't know the client's network capabilities. A
supportsDirectRead: trueresource with an internal URL may be unreachable from the client's network. (connor4312 on PR #607)Loss of protocol guarantees: Progress reporting, cancellation, error handling, and subscription semantics all exist within MCP. Fully out-of-band downloads lose these.
resources/streamaddresses these concerns by keeping the negotiation within the protocol (capabilities, auth, error handling) while delegating byte delivery to HTTP — the transport that already does this well. The server provides adownloadUrlscoped to the MCP session; the client fetches it with the session's credentials. Auth stays unified, the server controls the URL lifecycle, and protocol-level error handling covers the negotiation phase.Scope
This SEP is scoped to HTTP-based remote transports (Streamable HTTP, and future transports such as WebSocket with HTTP sidecar). The stdio transport is explicitly out of scope — it is designed for local server communication where large binary file transfer is typically handled through filesystem access. A future SEP may address stdio binary framing if demand warrants it.
Community Signal
This problem has been raised repeatedly across the MCP specification repository:
connor4312 (VS Code MCP) noted on PR #607:
This SEP is the formal proposal for that streamed read mechanism.
Specification
1. Capability Declaration
Server Capability
Servers that support resource streaming MUST declare the
streamsub-capability withinresources:{ "capabilities": { "resources": { "stream": true, "subscribe": true, "listChanged": true } } }The
streamcapability is independent of and additive to existing resource capabilities. A server MAY support any combination:{ "capabilities": { "resources": { "stream": true } } }{ "capabilities": { "resources": { "subscribe": true } } }{ "capabilities": { "resources": { "stream": true, "subscribe": true, "listChanged": true } } }Client Capability
Clients that can consume resource streams MUST declare the
resourceStreamingcapability:{ "capabilities": { "resourceStreaming": { "maxStreamSize": 1073741824 } } }maxStreamSize(optional): Maximum byte size the client is willing to accept in a single stream. Servers SHOULD respect this limit and return an error if the resource exceeds it.Servers MUST NOT send
resources/streamresponses to clients that did not declareresourceStreamingsupport.2. Resource Metadata Extension
Resources that support streaming MAY indicate this via the existing resource object returned by
resources/list:{ "uri": "onedrive:///documents/quarterly-report.pptx", "name": "quarterly-report.pptx", "title": "Q1 2026 Quarterly Report", "mimeType": "application/vnd.openxmlformats-officedocument.presentationml.presentation", "size": 15728640, "annotations": { "audience": ["user"], "priority": 0.6 }, "streamable": true }The
streamablefield (boolean, optional, defaultfalse) indicates that the server supportsresources/streamfor this specific resource. This allows servers to selectively offer streaming for resources where it is appropriate (large binaries) while continuing to serve small text resources exclusively viaresources/read.If a server declares the
streamcapability, it SHOULD setstreamable: trueon resources where streaming is the preferred delivery mechanism.3. The
resources/streamMethodRequest
{ "jsonrpc": "2.0", "id": 1, "method": "resources/stream", "params": { "uri": "onedrive:///documents/quarterly-report.pptx" } }The request is a standard JSON-RPC call, identical in shape to
resources/read. The method name itself is the signal that the client expects raw binary delivery rather than a base64-encoded JSON-RPC payload.Response: Direct Binary Stream (Primary Mode)
This SEP extends the Streamable HTTP transport to allow a third response Content-Type for
resources/streamrequests specifically. Today, the transport spec requires servers to return eitherContent-Type: application/jsonorContent-Type: text/event-stream. Forresources/stream, the server MAY also respond with the resource's actual media type and immediately begin streaming raw bytes:That's it. No JSON-RPC response envelope. No base64 encoding. No SSE framing. The server validates the request, resolves the resource, and starts pumping bytes — exactly like Microsoft Graph's
/contentendpoint or any standard HTTP file download.The client knows it called
resources/stream, so it inspects the responseContent-Typeto determine what it received:application/json→ A JSON-RPC error response (resource not found, stream not supported, etc.). Parse as JSON-RPC and handle the error.This is the same content negotiation pattern used broadly in HTTP APIs — the caller knows to expect either an error payload or the content itself.
Required response headers for binary streams:
Content-TypeContent-LengthmaxStreamSizeenforcement before streaming begins.Content-Dispositionattachment; filename="...".MCP-Resource-UriClient behavior for direct binary streams:
Content-Typebefore processing the body.Content-Typeisapplication/json, the client MUST parse the body as a JSON-RPC error response.Content-Type, the client MUST consume the body as raw binary.maxStreamSizelimit. IfContent-Lengthexceeds it, or if bytes received exceed it, abort the download.resources/stream(rather thanresources/read) signals that this content is intended for file-level operations, not token consumption.Response: Redirect Mode (Alternative)
Servers MAY respond with an HTTP redirect instead of streaming bytes directly. This is useful when the content is hosted in external storage (e.g., CDN, S3, Azure Blob) and the server wants to avoid proxying the bytes:
The client follows the redirect transparently and receives the raw binary stream from the storage endpoint.
This enables a pattern identical to Microsoft Graph:
GET /me/drive/items/{id}/content→302→ presigned blob URL → raw bytesRequirements for redirect mode:
MCP-Resource-Uriheader on the redirect response.Response: Download URL Mode (Alternative)
For servers that cannot respond with binary directly on the MCP endpoint (e.g., architectural constraints, load balancer limitations), the server MAY respond with a JSON-RPC result containing a
downloadUrl:{ "jsonrpc": "2.0", "id": 1, "result": { "uri": "onedrive:///documents/quarterly-report.pptx", "mimeType": "application/vnd.openxmlformats-officedocument.presentationml.presentation", "size": 15728640, "downloadUrl": "https://mcp-server.example.com/streams/a1b2c3d4-e5f6-7890-abcd-ef1234567890" } }The client detects this mode because
Content-Type: application/jsonwith aresult(not anerror) indicates a metadata response. The client then performs a separate HTTP GET to thedownloadUrlwith session credentials.Requirements for
downloadUrlmode:downloadUrlMUST use HTTPS.downloadUrl, unless it is self-authenticating.downloadUrlendpoint returns raw binary with the same headers as the direct binary stream mode.This mode adds a round trip but may be necessary for servers that share a single HTTP endpoint for all MCP traffic and cannot return non-JSON responses on it.
Response Mode Summary
The client handles all three modes through a simple Content-Type check on the response to its
resources/streamPOST:application/pdf)application/jsonwitherrorapplication/jsonwithresult.downloadUrldownloadUrl, stream the resultServers SHOULD prefer direct binary streaming or redirect mode for simplicity and efficiency. The
downloadUrlmode exists as a fallback for constrained server architectures.Example Flows
Flow 1: Direct binary stream (preferred — like Graph
/contentStream)sequenceDiagram participant Client as MCP Client participant Server as MCP Server participant Storage as Cloud Storage Client->>Server: POST /mcp (resources/stream { uri: "onedrive:///docs/report.pptx" }) Server->>Storage: Fetch content stream Storage-->>Server: Binary stream Server-->>Client: 200 OK, Content-Type: application/vnd...pptx, raw bytes Client->>Client: Write to diskFlow 2: Redirect to storage (like Graph
/content→ 302)sequenceDiagram participant Client as MCP Client participant Server as MCP Server participant Storage as Cloud Storage (presigned URL) Client->>Server: POST /mcp (resources/stream { uri: "onedrive:///docs/report.pptx" }) Server->>Server: Generate presigned URL for resource Server-->>Client: 302 Found, Location: https://storage.blob.../report.pptx?sig=... Client->>Storage: GET (follows redirect) Storage-->>Client: 200 OK, raw binary stream Client->>Client: Write to diskFlow 3: Download URL fallback
sequenceDiagram participant Client as MCP Client participant Server as MCP Server Client->>Server: POST /mcp (resources/stream { uri: "repo:///archive.tar.gz" }) Server-->>Client: 200 OK, Content-Type: application/json<br>{ result: { downloadUrl, mimeType, size } } Client->>Server: GET /streams/x9y8z7... (Authorization: Bearer <token>) Server-->>Client: 200 OK, Content-Type: application/gzip, raw bytes Client->>Client: Write to diskFlow 4: Error
sequenceDiagram participant Client as MCP Client participant Server as MCP Server Client->>Server: POST /mcp (resources/stream { uri: "onedrive:///missing.doc" }) Server-->>Client: 200 OK, Content-Type: application/json<br>{ error: { code: -32002, message: "Resource not found" } } Client->>Client: Handle errorError Handling
Errors during the
resources/streamrequest use standard JSON-RPC errors returned asContent-Type: application/json:-32002-32003-32004maxStreamSize-32603Example:
{ "jsonrpc": "2.0", "id": 1, "error": { "code": -32003, "message": "Stream not supported", "data": { "uri": "file:///small-config.json", "suggestion": "Use resources/read for this resource" } } }Errors during the binary stream itself (after bytes have started flowing) manifest as HTTP-level failures:
Content-Lengthpromised (client detects and retries or discards)For download URL mode, errors at the download endpoint use standard HTTP status codes:
200206401404410500Clients SHOULD treat download failures as retriable. If the URL returns
410 Gone, the client MAY re-issue theresources/streamJSON-RPC request to obtain a fresh URL.4. Transport Considerations
Streamable HTTP Transport Extension
This SEP proposes a targeted extension to the Streamable HTTP transport specification. Currently, the spec states:
For
resources/streamrequests only, this SEP adds a third permitted response type:This extension is narrowly scoped — it applies only to
resources/stream, not to any other JSON-RPC method. All other MCP methods continue to useapplication/jsonortext/event-streamexclusively.Why this is safe:
resources/stream, so it already expects non-JSON responses.Content-Typebefore parsing the body —application/jsonmeans error/metadata, anything else means binary.resources/stream(i.e., didn't declareresourceStreaming) will never call it, so they never encounter a binary response.Streamable HTTP (Primary Target)
The Streamable HTTP transport is the natural fit for
resources/stream. The interaction cleanly maps to HTTP semantics:Content-Type: application/jsoncontaining a JSON-RPC errorContent-Type: application/jsoncontaining a JSON-RPC result withdownloadUrl(fallback mode)This leverages HTTP's native capabilities — chunked transfer encoding,
Content-Length,Content-Disposition, range requests, redirects, CDN caching — without reinventing any of them inside JSON-RPC.The server's MCP endpoint and any redirect targets MAY be on different origins. When different origins are used, the server is responsible for ensuring the target is accessible to the client and properly authenticated (e.g., via presigned URLs).
WebSocket Transport
For WebSocket-based MCP connections, the JSON-RPC request and response flow over the WebSocket. Since WebSocket frames cannot carry HTTP-style headers,
resources/streamover WebSocket MUST use the download URL mode: the server returns a JSON-RPC result with adownloadUrl, and the client performs a separate HTTP GET.This is deliberate — using a separate HTTP request:
stdio Transport
The stdio transport is out of scope for this SEP.
resources/streamis designed for remote server scenarios where HTTP is available for binary content delivery. Local stdio-based servers typically have direct filesystem access, making binary streaming over the protocol unnecessary.A future SEP may propose binary framing for stdio if demand warrants it. Servers using stdio transport SHOULD NOT declare the
streamcapability.5. Interaction with Existing Features
Relationship to
resources/readresources/streamdoes NOT replaceresources/read. The two methods serve different purposes:resources/readresources/streamtext/blobServers that support both MUST continue responding to
resources/readfor all resources. A client MAY use either method for anystreamableresource — for example, a client mightresources/reada PDF to extract text for the LLM, orresources/streamthe same PDF to save it to disk.Subscriptions
Resource subscriptions (
resources/subscribe,notifications/resources/updated) work identically for streamable resources. When a subscribed resource changes, the client receives the update notification and may choose to re-read viaresources/reador re-stream viaresources/stream.Resource Templates
Resource templates (
resources/templates/list) MAY includestreamable: truein the template definition to indicate that instantiated resources support streaming.6. Error Handling
Error handling for
resources/streamis covered in detail in Section 3, Error Handling. In summary:Content-Type: application/jsonJSON-RPC error responses on the same POST request.Content-Length. Clients SHOULD retry.Rationale
Why a New Method Instead of Extending
resources/read?Adding streaming as a parameter to
resources/read(e.g.,"stream": true) was considered but rejected because:Semantic clarity: The method name itself communicates intent. Clients and servers can route, log, and handle the two methods differently without inspecting parameters.
Backward compatibility: Existing
resources/readhandlers don't need modification. New method = new handler.Transport optimization: Transports can specialize their handling. A Streamable HTTP transport can return a binary response for
resources/streamwhile continuing to return JSON forresources/read, without conditional logic per-request.SDK ergonomics:
client.readResource(uri)returns content for the LLM.client.streamResource(uri)returns a readable stream / file handle. Clean separation.Why Not Just Download URLs?
Previous proposals (PR #607,
supportsDirectRead) suggested adding download URLs as a property on the resource object itself, with the client fetching directly. This SEP takes a different approach: thedownloadUrlis returned as part of aresources/streamJSON-RPC response, not as a static resource property.This distinction matters because:
downloadUrlon a resource listing would require either long-lived URLs (security risk) or constant re-generation (complexity).resources/streammethod specifies exactly how those credentials flow to the download — no need for presigned URL infrastructure.resources/streamtells the server "I want binary bytes, prepare a download for me." The server can provision appropriately (e.g., request a download URL from Graph API, generate a presigned URL, set up a proxy). With static URLs, the server has to speculatively prepare download paths for all resources.Why Capability Negotiation on Both Sides?
Both server and client must opt in because:
Without both sides opting in, you'd get servers sending binary data to clients that can't handle it, or clients requesting streams from servers that can only produce JSON.
Alternatives Considered
supportsDirectRead/uriScope(PR AddResource.uriScopeto enable direct reads #607): Static download URL as a resource property. Problems: URL lifecycle management, auth fragmentation, no error handling before fetch. This SEP's dynamicdownloadUrlviaresources/streamsolves all three.Binary framing within JSON-RPC: Streaming base64-chunked data as JSON-RPC notifications. Retains encoding overhead, adds protocol complexity (stream IDs, chunk sequencing, completion signals). We considered this for stdio support but concluded it's better left to a future SEP.
SSE binary events: Delivering binary content as SSE
data:fields (base64 encoded) on the existing Streamable HTTP connection. Same encoding overhead asresources/read, blocks the SSE connection for the duration of the download, prevents concurrent messages.New transport-level primitive: A generic binary channel in the transport layer. Too broad, too disruptive, requires changes to every transport and SDK.
resources/streamis a protocol-level method that works with existing HTTP infrastructure.Multipart JSON-RPC responses: Non-standard, breaks existing JSON-RPC libraries, limited ecosystem support.
Backward Compatibility
This SEP introduces no backward incompatibilities.
resources/readremains unchanged and MUST continue to work for all resources.streamcapability is opt-in for both servers and clients.streamin their capabilities will never receiveresources/streamrequests (well-behaved clients check capabilities).resourceStreamingwill never receive stream data.streamablefield on resources defaults tofalseand is ignored by clients that don't understand it.Migration path: Implementations adopt streaming incrementally. A server can start by marking a few large-file resources as
streamable: true. Clients can add streaming support when they're ready. There is no flag day.Security Implications
New Attack Surfaces
Resource exhaustion via large downloads: A malicious server could attempt to overwhelm a client with an unbounded stream. Mitigation: Client's
maxStreamSizecapability sets an upper bound. Clients MUST enforce this limit — checkingContent-Lengthbefore streaming begins and aborting if bytes received exceed the limit.Download URL abuse: If download URLs are predictable or long-lived, an attacker could access resource content. Mitigation: Download URLs MUST be cryptographically unguessable (e.g., containing a UUID v4 or HMAC-signed token), session-scoped, and time-limited. Servers SHOULD make URLs single-use.
Open redirect via
downloadUrl: A compromised or malicious server could return adownloadUrlpointing to an attacker-controlled endpoint. Mitigation: Clients SHOULD validate that thedownloadUrlorigin matches the MCP server's origin or a set of trusted origins. Clients MUST apply the same TLS verification to the download as to the MCP connection.Incomplete download exploitation: A client might act on partially downloaded content (e.g., a truncated archive). Mitigation: Clients SHOULD verify
Content-Lengthmatches bytes received. Servers MAY include integrity metadata (e.g.,ETag, checksum headers) in the download response.Privacy Considerations
resources/readcontent — it is accessed using the same session credentials.downloadUrlexposes content via an HTTP endpoint. The URL MUST be session-scoped and time-limited to prevent unauthorized access.Transport Security
downloadUrlMUST use HTTPS/TLS.downloadUrl, unless the URL is self-authenticating (e.g., contains a cryptographic signature in its query parameters, as with presigned URLs).Cache-Control: no-store) on download responses to prevent sensitive content from being cached by intermediaries.Reference Implementation
Planned Implementation
TypeScript SDK: Fork of
@modelcontextprotocol/sdkadding:StreamableResourcetype extendingResourcewithstreamablefieldclient.streamResource(uri)method that returns{ downloadUrl, mimeType, size }and provides a helper to fetch the binary stream as aReadableStream<Uint8Array>server.setStreamHandler()for registering stream providers that return download URLsProof-of-Concept Server: MCP server for OneDrive/SharePoint file access demonstrating:
resources/streamreturning Graph API download URLs (mirroring/content→ 302 pattern)Content-Lengthand client-side trackingresources/readfor small text filesProof-of-Concept Client: CLI client demonstrating:
downloadUrlContent-LengthheadersmaxStreamSizeenforcementPerformance Implications
resources/readresources/stream(direct)resources/stream(redirect)Key wins:
Open Questions
Transport spec amendment process: This SEP proposes extending the Streamable HTTP transport to allow binary Content-Types for
resources/streamresponses. Should this be a standalone transport amendment, or is it acceptable as part of this SEP? Current thinking: include it here since the extension is narrowly scoped to a single method.Resume support: Should the spec define a mechanism for resuming interrupted streams (e.g.,
Accept-Ranges/Rangeheaders on the binary response)? Current thinking: Servers SHOULD support range requests, but this SEP does not mandate it. HTTP range requests are well-understood and servers can adopt them without protocol-level specification.Relationship to streaming tool results (Streaming tool use results #117): This SEP addresses resource streaming specifically. Should tool results have an analogous mechanism for returning binary content? Current thinking: Yes, but as a separate SEP. The patterns established here (binary HTTP response, Content-Type switching) could be reused.
Acceptheader from client: Should the client include an additional value in theAcceptheader when POSTing aresources/streamrequest (e.g.,Accept: application/json, */*) to signal willingness to receive binary? Current thinking: Yes, the client SHOULD include*/*or the expected MIME type inAccept, in addition toapplication/json. This gives standard HTTP intermediaries correct caching/routing signals.Concurrent stream limits: Should there be a capability for clients to declare how many concurrent streams they support? Current thinking: Out of scope. Clients can manage their own concurrency. Servers can rate-limit download endpoints independently.
Acknowledgments
supportsDirectRead/uriScopeproposal in PR #607, which shaped the out-of-band download approach incorporated here