Skip to content

Commit c72b52b

Browse files
authored
Merge branch 'main' into ochafik/spec-type-tests
2 parents 96a4f71 + 8714f21 commit c72b52b

16 files changed

+359
-44
lines changed

README.md

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -570,20 +570,31 @@ app.listen(3000);
570570
```
571571

572572
> [!TIP]
573-
> When using this in a remote environment, make sure to allow the header parameter `mcp-session-id` in CORS. Otherwise, it may result in a `Bad Request: No valid session ID provided` error.
574-
>
575-
> For example, in Node.js you can configure it like this:
576-
>
577-
> ```ts
578-
> app.use(
579-
> cors({
580-
> origin: ['https://your-remote-domain.com, https://your-other-remote-domain.com'],
581-
> exposedHeaders: ['mcp-session-id'],
582-
> allowedHeaders: ['Content-Type', 'mcp-session-id'],
583-
> })
584-
> );
573+
> When using this in a remote environment, make sure to allow the header parameter `mcp-session-id` in CORS. Otherwise, it may result in a `Bad Request: No valid session ID provided` error. Read the following section for examples.
585574
> ```
586575
576+
577+
#### CORS Configuration for Browser-Based Clients
578+
579+
If you'd like your server to be accessible by browser-based MCP clients, you'll need to configure CORS headers. The `Mcp-Session-Id` header must be exposed for browser clients to access it:
580+
581+
```typescript
582+
import cors from 'cors';
583+
584+
// Add CORS middleware before your MCP routes
585+
app.use(cors({
586+
origin: '*', // Configure appropriately for production, for example:
587+
// origin: ['https://your-remote-domain.com, https://your-other-remote-domain.com'],
588+
exposedHeaders: ['Mcp-Session-Id']
589+
allowedHeaders: ['Content-Type', 'mcp-session-id'],
590+
}));
591+
```
592+
593+
This configuration is necessary because:
594+
- The MCP streamable HTTP transport uses the `Mcp-Session-Id` header for session management
595+
- Browsers restrict access to response headers unless explicitly exposed via CORS
596+
- Without this configuration, browser-based clients won't be able to read the session ID from initialization responses
597+
587598
#### Without Session Management (Stateless)
588599

589600
For simpler use cases where session management isn't needed:

src/cli.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,11 @@ async function runServer(port: number | null) {
102102
await transport.handlePostMessage(req, res);
103103
});
104104

105-
app.listen(port, () => {
105+
app.listen(port, (error) => {
106+
if (error) {
107+
console.error('Failed to start server:', error);
108+
process.exit(1);
109+
}
106110
console.log(`Server running on http://localhost:${port}/sse`);
107111
});
108112
} else {

src/client/sse.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,29 @@ describe("SSEClientTransport", () => {
382382
expect(mockAuthProvider.tokens).toHaveBeenCalled();
383383
});
384384

385+
it("attaches custom header from provider on initial SSE connection", async () => {
386+
mockAuthProvider.tokens.mockResolvedValue({
387+
access_token: "test-token",
388+
token_type: "Bearer"
389+
});
390+
const customHeaders = {
391+
"X-Custom-Header": "custom-value",
392+
};
393+
394+
transport = new SSEClientTransport(resourceBaseUrl, {
395+
authProvider: mockAuthProvider,
396+
requestInit: {
397+
headers: customHeaders,
398+
},
399+
});
400+
401+
await transport.start();
402+
403+
expect(lastServerRequest.headers.authorization).toBe("Bearer test-token");
404+
expect(lastServerRequest.headers["x-custom-header"]).toBe("custom-value");
405+
expect(mockAuthProvider.tokens).toHaveBeenCalled();
406+
});
407+
385408
it("attaches auth header from provider on POST requests", async () => {
386409
mockAuthProvider.tokens.mockResolvedValue({
387410
access_token: "test-token",

src/client/sse.ts

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,8 @@ export class SSEClientTransport implements Transport {
106106
return await this._startOrAuth();
107107
}
108108

109-
private async _commonHeaders(): Promise<HeadersInit> {
110-
const headers = {
111-
...this._requestInit?.headers,
112-
} as HeadersInit & Record<string, string>;
109+
private async _commonHeaders(): Promise<Headers> {
110+
const headers: HeadersInit = {};
113111
if (this._authProvider) {
114112
const tokens = await this._authProvider.tokens();
115113
if (tokens) {
@@ -120,24 +118,24 @@ export class SSEClientTransport implements Transport {
120118
headers["mcp-protocol-version"] = this._protocolVersion;
121119
}
122120

123-
return headers;
121+
return new Headers(
122+
{ ...headers, ...this._requestInit?.headers }
123+
);
124124
}
125125

126126
private _startOrAuth(): Promise<void> {
127-
const fetchImpl = (this?._eventSourceInit?.fetch ?? this._fetch ?? fetch) as typeof fetch
127+
const fetchImpl = (this?._eventSourceInit?.fetch ?? this._fetch ?? fetch) as typeof fetch
128128
return new Promise((resolve, reject) => {
129129
this._eventSource = new EventSource(
130130
this._url.href,
131131
{
132132
...this._eventSourceInit,
133133
fetch: async (url, init) => {
134-
const headers = await this._commonHeaders()
134+
const headers = await this._commonHeaders();
135+
headers.set("Accept", "text/event-stream");
135136
const response = await fetchImpl(url, {
136137
...init,
137-
headers: new Headers({
138-
...headers,
139-
Accept: "text/event-stream"
140-
})
138+
headers,
141139
})
142140

143141
if (response.status === 401 && response.headers.has('www-authenticate')) {
@@ -238,8 +236,7 @@ const fetchImpl = (this?._eventSourceInit?.fetch ?? this._fetch ?? fetch) as typ
238236
}
239237

240238
try {
241-
const commonHeaders = await this._commonHeaders();
242-
const headers = new Headers(commonHeaders);
239+
const headers = await this._commonHeaders();
243240
headers.set("content-type", "application/json");
244241
const init = {
245242
...this._requestInit,

src/client/stdio.test.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
import { JSONRPCMessage } from "../types.js";
22
import { StdioClientTransport, StdioServerParameters } from "./stdio.js";
33

4-
const serverParameters: StdioServerParameters = {
5-
command: "/usr/bin/tee",
4+
// Configure default server parameters based on OS
5+
// Uses 'more' command for Windows and 'tee' command for Unix/Linux
6+
const getDefaultServerParameters = (): StdioServerParameters => {
7+
if (process.platform === "win32") {
8+
return { command: "more" };
9+
}
10+
return { command: "/usr/bin/tee" };
611
};
712

13+
const serverParameters = getDefaultServerParameters();
14+
815
test("should start then close cleanly", async () => {
916
const client = new StdioClientTransport(serverParameters);
1017
client.onerror = (error) => {

src/examples/server/demoInMemoryOAuthProvider.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,11 @@ export const setupAuthServer = ({authServerUrl, mcpServerUrl, strictResource}: {
200200

201201
const auth_port = authServerUrl.port;
202202
// Start the auth server
203-
authApp.listen(auth_port, () => {
203+
authApp.listen(auth_port, (error) => {
204+
if (error) {
205+
console.error('Failed to start server:', error);
206+
process.exit(1);
207+
}
204208
console.log(`OAuth Authorization Server listening on port ${auth_port}`);
205209
});
206210

src/examples/server/jsonResponseStreamableHttp.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { McpServer } from '../../server/mcp.js';
44
import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js';
55
import { z } from 'zod';
66
import { CallToolResult, isInitializeRequest } from '../../types.js';
7+
import cors from 'cors';
78

89

910
// Create an MCP server with implementation details
@@ -81,6 +82,12 @@ const getServer = () => {
8182
const app = express();
8283
app.use(express.json());
8384

85+
// Configure CORS to expose Mcp-Session-Id header for browser-based clients
86+
app.use(cors({
87+
origin: '*', // Allow all origins - adjust as needed for production
88+
exposedHeaders: ['Mcp-Session-Id']
89+
}));
90+
8491
// Map to store transports by session ID
8592
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
8693

@@ -151,7 +158,11 @@ app.get('/mcp', async (req: Request, res: Response) => {
151158

152159
// Start the server
153160
const PORT = 3000;
154-
app.listen(PORT, () => {
161+
app.listen(PORT, (error) => {
162+
if (error) {
163+
console.error('Failed to start server:', error);
164+
process.exit(1);
165+
}
155166
console.log(`MCP Streamable HTTP Server listening on port ${PORT}`);
156167
});
157168

src/examples/server/simpleSseServer.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,11 @@ app.post('/messages', async (req: Request, res: Response) => {
145145

146146
// Start the server
147147
const PORT = 3000;
148-
app.listen(PORT, () => {
148+
app.listen(PORT, (error) => {
149+
if (error) {
150+
console.error('Failed to start server:', error);
151+
process.exit(1);
152+
}
149153
console.log(`Simple SSE Server (deprecated protocol version 2024-11-05) listening on port ${PORT}`);
150154
});
151155

src/examples/server/simpleStatelessStreamableHttp.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { McpServer } from '../../server/mcp.js';
33
import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js';
44
import { z } from 'zod';
55
import { CallToolResult, GetPromptResult, ReadResourceResult } from '../../types.js';
6+
import cors from 'cors';
67

78
const getServer = () => {
89
// Create an MCP server with implementation details
@@ -96,6 +97,12 @@ const getServer = () => {
9697
const app = express();
9798
app.use(express.json());
9899

100+
// Configure CORS to expose Mcp-Session-Id header for browser-based clients
101+
app.use(cors({
102+
origin: '*', // Allow all origins - adjust as needed for production
103+
exposedHeaders: ['Mcp-Session-Id']
104+
}));
105+
99106
app.post('/mcp', async (req: Request, res: Response) => {
100107
const server = getServer();
101108
try {
@@ -151,7 +158,11 @@ app.delete('/mcp', async (req: Request, res: Response) => {
151158

152159
// Start the server
153160
const PORT = 3000;
154-
app.listen(PORT, () => {
161+
app.listen(PORT, (error) => {
162+
if (error) {
163+
console.error('Failed to start server:', error);
164+
process.exit(1);
165+
}
155166
console.log(`MCP Stateless Streamable HTTP Server listening on port ${PORT}`);
156167
});
157168

src/examples/server/simpleStreamableHttp.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { setupAuthServer } from './demoInMemoryOAuthProvider.js';
1111
import { OAuthMetadata } from 'src/shared/auth.js';
1212
import { checkResourceAllowed } from 'src/shared/auth-utils.js';
1313

14+
import cors from 'cors';
15+
1416
// Check for OAuth flag
1517
const useOAuth = process.argv.includes('--oauth');
1618
const strictOAuth = process.argv.includes('--oauth-strict');
@@ -420,12 +422,18 @@ const getServer = () => {
420422
return server;
421423
};
422424

423-
const MCP_PORT = 3000;
424-
const AUTH_PORT = 3001;
425+
const MCP_PORT = process.env.MCP_PORT ? parseInt(process.env.MCP_PORT, 10) : 3000;
426+
const AUTH_PORT = process.env.MCP_AUTH_PORT ? parseInt(process.env.MCP_AUTH_PORT, 10) : 3001;
425427

426428
const app = express();
427429
app.use(express.json());
428430

431+
// Allow CORS all domains, expose the Mcp-Session-Id header
432+
app.use(cors({
433+
origin: '*', // Allow all origins
434+
exposedHeaders: ["Mcp-Session-Id"]
435+
}));
436+
429437
// Set up OAuth if enabled
430438
let authMiddleware = null;
431439
if (useOAuth) {
@@ -640,7 +648,11 @@ if (useOAuth && authMiddleware) {
640648
app.delete('/mcp', mcpDeleteHandler);
641649
}
642650

643-
app.listen(MCP_PORT, () => {
651+
app.listen(MCP_PORT, (error) => {
652+
if (error) {
653+
console.error('Failed to start server:', error);
654+
process.exit(1);
655+
}
644656
console.log(`MCP Streamable HTTP Server listening on port ${MCP_PORT}`);
645657
});
646658

0 commit comments

Comments
 (0)