Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/guides/streamable-http-and-legacy-sse.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ Run **`nitrostack cursor`** to write `.cursor/mcp.json`. **Legacy SSE (`/sse`)**

- **Port** is usually set via **`PORT`** or your `start()` options; avoid colliding with NitroStudio (often **3000**) and the widget dev server (often **3001**).
- **`endpoint`** defaults to **`/mcp`**; if you change it, streamable URLs move with it (for example `/api/mcp`). Legacy routes stay **`/sse`** and **`/mcp/messages`** unless you customize the server.
- **Logging:** NitroStack routes Streamable HTTP request failures through its configured logger. Direct `StreamableHttpTransport` instances can pass `logger`; without one, errors go to `console.error`.
- **Production:** terminate TLS at your reverse proxy and forward to the Node port; clients should use **`https://`**.

## Related documentation
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
import { jest, describe, it, expect, beforeEach, afterEach } from '@jest/globals';
import { StreamableHttpTransport } from '../transports/streamable-http.js';
import { Server as McpServer } from '@modelcontextprotocol/sdk/server/index.js';
import {
Expand Down Expand Up @@ -44,14 +44,17 @@ const MCP_ACCEPT = 'application/json, text/event-stream';

describe('StreamableHttpTransport (SDK-delegated host)', () => {
let transport: StreamableHttpTransport;
const logError = jest.fn();
const port = 3060;
const baseUrl = `http://localhost:${port}/mcp`;

beforeEach(async () => {
logError.mockClear();
transport = new StreamableHttpTransport({
port,
host: 'localhost',
enableCors: true,
logger: { error: logError },
});
transport.setMcpServerFactory(makeServerFactory());
await transport.start();
Expand Down Expand Up @@ -159,6 +162,37 @@ describe('StreamableHttpTransport (SDK-delegated host)', () => {
expect(body.error).toBeDefined();
});

it('logs POST and GET failures and returns an error response', async () => {
const postError = new Error('POST failed');
transport.setMcpServerFactory(() => { throw postError; });

const postResponse = await fetch(baseUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: MCP_ACCEPT },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize' }),
});

expect(postResponse.status).toBe(500);
expect((await postResponse.json() as any).error.code).toBe(-32603);

const getError = new Error('GET failed');
transport.setLegacySseHandler(async () => { throw getError; });
const getResponse = await fetch(baseUrl);

expect(getResponse.status).toBe(500);
expect((await getResponse.json() as any).error.code).toBe(-32603);
expect(logError).toHaveBeenNthCalledWith(1, 'MCP request error', {
error: postError.message,
method: 'POST',
path: '/mcp',
});
expect(logError).toHaveBeenNthCalledWith(2, 'MCP request error', {
error: getError.message,
method: 'GET',
path: '/mcp',
});
});

it('validates Origin when CORS is disabled', async () => {
const secureTransport = new StreamableHttpTransport({ port: 3068, enableCors: false });
secureTransport.setMcpServerFactory(makeServerFactory());
Expand Down
3 changes: 3 additions & 0 deletions typescript/packages/core/src/core/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1122,6 +1122,7 @@ export class NitroStackServer {
endpoint: '/mcp',
enableSessions: transportType === 'http', // Sessions ONLY in pure http mode
enableCors: process.env.ENABLE_CORS !== 'false',
logger: this.logger,
...getStreamableHttpEnvOptions(),
});

Expand Down Expand Up @@ -1220,6 +1221,7 @@ export class NitroStackServer {
host: transportOptions?.host || 'localhost',
endpoint: transportOptions?.endpoint || '/mcp',
enableCors: transportOptions?.enableCors !== false, // Enable CORS by default for web clients
logger: this.logger,
...getStreamableHttpEnvOptions(),
});
transport.setMcpServerFactory(() => this.createConfiguredMcpServer());
Expand Down Expand Up @@ -1250,6 +1252,7 @@ export class NitroStackServer {
host: transportOptions?.host || 'localhost',
endpoint: transportOptions?.endpoint || '/mcp',
enableCors: transportOptions?.enableCors || false,
logger: this.logger,
...getStreamableHttpEnvOptions(),
});

Expand Down

Large diffs are not rendered by default.