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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy