Skip to content

StreamableHTTPServerTransport rejects JSON-only Accept with 406 even when enableJsonResponse: true #1944

@bokelley

Description

@bokelley

Summary

StreamableHTTPServerTransport returns 406 Not Acceptable when a client sends Accept: application/json without also including text/event-stream, even when the transport is constructed with enableJsonResponse: true. In that mode the transport is operating in pure request/response — there is no SSE stream to negotiate — so requiring text/event-stream in Accept is incorrect.

This blocks buyer agents and validators that probe MCP servers with a plain JSON Accept (a reasonable default for JSON-RPC over HTTP). Every MCP seller implementation in the AdCP ecosystem hits this; we currently ship a tiny Express middleware that rewrites the Accept header before the transport sees it, which is an escape hatch we'd rather not need.

Reproduction

import express from 'express';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';

const app = express();
app.use(express.json());

app.post('/mcp', async (req, res) => {
  const server = new McpServer({ name: 'repro', version: '0.0.0' });
  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: undefined,
    enableJsonResponse: true, // pure request/response, no SSE
  });
  await server.connect(transport);
  await transport.handleRequest(req, res, req.body);
});

app.listen(3000);
# Fails with 406 Not Acceptable
curl -sS -X POST http://localhost:3000/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"c","version":"0"}}}'

# Succeeds when both are advertised
curl -sS -X POST http://localhost:3000/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"c","version":"0"}}}'

Expected behavior

When enableJsonResponse: true and the response will be a plain JSON body (no SSE), the Accept check should accept application/json alone. text/event-stream should only be required when the transport is about to open a stream.

Proposed fix

In the POST handler's Accept validation:

  • If enableJsonResponse is true and the request is not one the transport would stream for, require only application/json (or a wildcard that matches it) in Accept.
  • Keep the existing stricter check for the streaming path: when the response will be SSE, continue to require text/event-stream.
  • GET (SSE subscription) is unaffected — that path genuinely needs text/event-stream.

Happy to send a PR if the approach looks right.

Context

Filed from the AdCP client SDK (@adcp/client), which ships a temporary middleware that normalizes Accept before the transport sees it. We'd like to drop that once this is fixed upstream.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions