NitroStack Logo
/guides
/streamable http

Streamable HTTP and Legacy SSE (/mcp and /sse)

When you run your NitroStack MCP server over HTTP (for example with npm run start after a build, or npm run dev in development), the same HTTP port exposes two complementary ways for clients to speak MCP over the network:

  1. Streamable HTTP: the current default path, aligned with the MCP streamable HTTP transport (JSON-RPC over HTTP with optional SSE on the same logical endpoint).
  2. Legacy SDK SSE: compatibility routes for clients built against the older SSEServerTransport pattern from @modelcontextprotocol/sdk.

The CLI startup summary reflects both:

  • Streamable HTTP: http://localhost:<PORT>/mcp (or your configured endpoint)
  • Legacy SDK SSE: http://localhost:<PORT>/sse

The exact host, port, and /mcp path come from your server.start({ transport, host, port, endpoint, enableCors }) options (default endpoint is /mcp).


Streamable HTTP (/mcp)

NitroStack uses StreamableHttpTransport, which mounts MCP on a single configurable path (default /mcp).

Typical usage:

  • POST /mcp: send JSON-RPC requests to the server (tools, resources, prompts, etc.).
  • GET /mcp: open an SSE stream for server-initiated events (Streamable HTTP clients with an existing session), or start a legacy SDK SSE session when no mcp-session-id is present (Cursor and similar clients).

Use /mcp for new Streamable HTTP integrations. Older /mcp/sse and /mcp/message aliases on this transport were removed when NitroStack delegated protocol handling to the official SDK transport; use root /sse for legacy SSE clients instead (see below).

Communication model:

  1. Client sends JSON-RPC 2.0 messages (e.g. initialize, tools/list, tools/call) via POST to the MCP endpoint.
  2. Client may hold a GET connection to the same endpoint (or the transport's SSE channel) to receive streamed events and follow the MCP streamable HTTP session rules.

This is the path used by modern MCP inspectors, AI gateways, and HTTP clients that target streamable HTTP.


HTTP Accept Header Handling & Content Negotiation

The HTTP 406 Not Acceptable Challenge

Both the 2025-era and 2026-era official MCP SDK transports strictly enforce that incoming POST requests include an Accept header containing both MIME types:

Http
Accept: application/json, text/event-stream

If a client sends a standard web request with Accept: application/json, a wildcard */*, or omits the header altogether, the upstream SDK transport rejects the request immediately with an HTTP 406 Not Acceptable status error. This frequently causes connection failures when testing servers with standard tools such as curl, Postman, custom HTTP client libraries, or API reverse proxies.

Transparent Header Rewriting

NitroStack solves this incompatibility at the transport layer. When an incoming POST arrives without text/event-stream in its Accept header:

  1. NitroStack transparently rewrites the request header internally to application/json, text/event-stream to satisfy SDK transport validation.
  2. The server processes the request and responds in standard JSON response mode, returning clean JSON bodies without forcing SSE envelopes.

Diagnostic Logging (NITRO_DEBUG_ACCEPT)

To troubleshoot clients with unusual header combinations or debug unexpected 406 errors, NitroStack provides an environment variable flag: NITRO_DEBUG_ACCEPT.

When set to 1 or true, NitroStack logs every request that omits event-stream headers or receives a 406 status directly to stderr:

Bash
export NITRO_DEBUG_ACCEPT=1
npm run dev

Sample diagnostic entry written to stderr:

JSON
[NITRO_DEBUG_ACCEPT] {"engine":"modern","httpMethod":"POST","accept":"application/json","contentType":"application/json","protocolVersion":"2026-07-28","rpcMethod":"tools/call","status":200,"servedAsJson":true}

Diagnostic fields:

  • engine: Active protocol engine (modern for 2026-07-28 or legacy for 2025-era sessionful).
  • httpMethod: Incoming HTTP method (POST or GET).
  • accept: Raw client-supplied Accept header.
  • contentType: Request Content-Type header.
  • protocolVersion: Negotiated protocol era.
  • rpcMethod: The JSON-RPC method being invoked (e.g. tools/list, tools/call).
  • status: Resulting HTTP status code.
  • servedAsJson: Boolean indicating whether the response was returned as clean JSON.

Legacy SDK SSE (/sse + POST /mcp/messages)

For clients that still use the classic HTTP and SSE split from the original TypeScript SDK:

  • GET /sse: opens a Server-Sent Events connection. The server creates a per-session MCP server instance wired to SSEServerTransport.
  • POST /mcp/messages?sessionId=<id>: sends JSON-RPC messages for that session. The sessionId is issued by the transport when the SSE connection is established.

Communication model:

  1. Client connects to GET /sse and reads the event stream.
  2. Client obtains a sessionId from the SSE transport.
  3. Client posts JSON-RPC envelopes to /mcp/messages with sessionId as a query parameter.

Each active legacy session runs through the same tool, resource, and prompt surface as the main server; NitroStack registers handlers on the session McpServer identically to the primary instance.


STDIO in Dual Mode

In dual mode, STDIO remains available on the same process: one client can attach via stdin/stdout while other clients connect over HTTP. Legacy SSE and streamable HTTP share the same Express app and port as the streamable transport.


Which URL Should I Use?

Client / ScenarioSuggested EntryDetails
Cursor IDEhttp://<host>:<port>/mcp or http://<host>:<port>/sseBoth use SSE under the hood.
MCP Inspector & Streamable HTTPhttp://<host>:<port>/mcpStandard Streamable HTTP endpoint.
Legacy Examples (SSEServerTransport)http://<host>:<port>/sseConnect to /sse and POST to /mcp/messages.
Local NitroStudio / CLI STDIOSTDIODirect process piping (no HTTP URL required).

Run nitrostack cursor to generate .cursor/mcp.json. Legacy SSE (/sse) is the default; /mcp also works for Cursor because the server falls back to legacy SSE on GET /mcp when no session ID is present.


Configuration Reminders

  • Port: Usually configured via PORT environment variable or server.start({ port }); avoid port collisions with NitroStudio (often 3000) and widget dev servers (often 3001).
  • Endpoint: Defaults to /mcp; if modified, streamable routes adjust accordingly (e.g., /api/mcp).
  • Production: Terminate TLS at your reverse proxy and forward to the Node port; external clients should connect via https://.