Skip to content

Commit 5d3f27f

Browse files
committed
feat: add cors configuration
1 parent 6338d14 commit 5d3f27f

File tree

3 files changed

+128
-23
lines changed

3 files changed

+128
-23
lines changed

README.md

+46-1
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,52 @@ const server = new MCPServer({
9595
options: {
9696
port: 8080, // Optional (default: 8080)
9797
endpoint: "/sse", // Optional (default: "/sse")
98-
messageEndpoint: "/messages" // Optional (default: "/messages")
98+
messageEndpoint: "/messages", // Optional (default: "/messages")
99+
cors: {
100+
allowOrigin: "*", // Optional (default: "*")
101+
allowMethods: "GET, POST, OPTIONS", // Optional (default: "GET, POST, OPTIONS")
102+
allowHeaders: "Content-Type, Authorization, x-api-key", // Optional (default: "Content-Type, Authorization, x-api-key")
103+
exposeHeaders: "Content-Type, Authorization, x-api-key", // Optional (default: "Content-Type, Authorization, x-api-key")
104+
maxAge: "86400" // Optional (default: "86400")
105+
}
106+
}
107+
}
108+
});
109+
```
110+
111+
#### CORS Configuration
112+
113+
The SSE transport supports flexible CORS configuration. By default, it uses permissive settings suitable for development. For production, you should configure CORS according to your security requirements:
114+
115+
```typescript
116+
const server = new MCPServer({
117+
transport: {
118+
type: "sse",
119+
options: {
120+
// Restrict to specific origin
121+
cors: {
122+
allowOrigin: "https://myapp.com",
123+
allowMethods: "GET, POST",
124+
allowHeaders: "Content-Type, Authorization",
125+
exposeHeaders: "Content-Type, Authorization",
126+
maxAge: "3600"
127+
}
128+
}
129+
}
130+
});
131+
132+
// Or with multiple allowed origins
133+
const server = new MCPServer({
134+
transport: {
135+
type: "sse",
136+
options: {
137+
cors: {
138+
allowOrigin: "https://app1.com, https://app2.com",
139+
allowMethods: "GET, POST, OPTIONS",
140+
allowHeaders: "Content-Type, Authorization, Custom-Header",
141+
exposeHeaders: "Content-Type, Authorization",
142+
maxAge: "86400"
143+
}
99144
}
100145
}
101146
});

src/transports/sse/server.ts

+29-21
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,14 @@ import getRawBody from "raw-body"
66
import { APIKeyAuthProvider } from "../../auth/providers/apikey.js"
77
import { DEFAULT_AUTH_ERROR } from "../../auth/types.js"
88
import { AbstractTransport } from "../base.js"
9-
import { DEFAULT_SSE_CONFIG, SSETransportConfig, SSETransportConfigInternal } from "./types.js"
9+
import { DEFAULT_SSE_CONFIG, SSETransportConfig, SSETransportConfigInternal, DEFAULT_CORS_CONFIG, CORSConfig } from "./types.js"
1010
import { logger } from "../../core/Logger.js"
1111
import { getRequestHeader, setResponseHeaders } from "../../utils/headers.js"
1212

1313
interface ExtendedIncomingMessage extends IncomingMessage {
1414
body?: ClientRequest
1515
}
1616

17-
const CORS_HEADERS = {
18-
"Access-Control-Allow-Origin": "*",
19-
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
20-
"Access-Control-Allow-Headers": "Content-Type, Authorization, x-api-key",
21-
"Access-Control-Expose-Headers": "Content-Type, Authorization, x-api-key"
22-
}
23-
2417
const SSE_HEADERS = {
2518
"Content-Type": "text/event-stream",
2619
"Cache-Control": "no-cache",
@@ -52,6 +45,31 @@ export class SSEServerTransport extends AbstractTransport {
5245
})}`)
5346
}
5447

48+
private getCorsHeaders(includeMaxAge: boolean = false): Record<string, string> {
49+
// Ensure all CORS properties are present by merging with defaults
50+
const corsConfig = {
51+
allowOrigin: DEFAULT_CORS_CONFIG.allowOrigin,
52+
allowMethods: DEFAULT_CORS_CONFIG.allowMethods,
53+
allowHeaders: DEFAULT_CORS_CONFIG.allowHeaders,
54+
exposeHeaders: DEFAULT_CORS_CONFIG.exposeHeaders,
55+
maxAge: DEFAULT_CORS_CONFIG.maxAge,
56+
...this._config.cors
57+
} as Required<CORSConfig>
58+
59+
const headers: Record<string, string> = {
60+
"Access-Control-Allow-Origin": corsConfig.allowOrigin,
61+
"Access-Control-Allow-Methods": corsConfig.allowMethods,
62+
"Access-Control-Allow-Headers": corsConfig.allowHeaders,
63+
"Access-Control-Expose-Headers": corsConfig.exposeHeaders
64+
}
65+
66+
if (includeMaxAge) {
67+
headers["Access-Control-Max-Age"] = corsConfig.maxAge
68+
}
69+
70+
return headers
71+
}
72+
5573
async start(): Promise<void> {
5674
if (this._server) {
5775
throw new Error("SSE transport already started")
@@ -88,16 +106,12 @@ export class SSEServerTransport extends AbstractTransport {
88106
logger.debug(`Incoming request: ${req.method} ${req.url}`)
89107

90108
if (req.method === "OPTIONS") {
91-
const preflightHeaders = {
92-
...CORS_HEADERS,
93-
"Access-Control-Max-Age": "86400"
94-
}
95-
setResponseHeaders(res, preflightHeaders)
109+
setResponseHeaders(res, this.getCorsHeaders(true))
96110
res.writeHead(204).end()
97111
return
98112
}
99113

100-
setResponseHeaders(res, CORS_HEADERS)
114+
setResponseHeaders(res, this.getCorsHeaders())
101115

102116
const url = new URL(req.url!, `http://${req.headers.host}`)
103117
const sessionId = url.searchParams.get("sessionId")
@@ -194,7 +208,7 @@ export class SSEServerTransport extends AbstractTransport {
194208

195209
const headers = {
196210
...SSE_HEADERS,
197-
...CORS_HEADERS,
211+
...this.getCorsHeaders(),
198212
...this._config.headers
199213
}
200214
setResponseHeaders(res, headers)
@@ -301,12 +315,6 @@ export class SSEServerTransport extends AbstractTransport {
301315
params: params
302316
})}`);
303317

304-
logger.debug(`Processing RPC message: ${JSON.stringify({
305-
id: rpcMessage.id,
306-
method: rpcMessage.method,
307-
params: rpcMessage.params
308-
})}`)
309-
310318
if (!this._onmessage) {
311319
throw new Error("No message handler registered")
312320
}

src/transports/sse/types.ts

+53-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,40 @@
11
import { AuthConfig } from "../../auth/types.js";
22

3+
/**
4+
* CORS configuration options for SSE transport
5+
*/
6+
export interface CORSConfig {
7+
/**
8+
* Access-Control-Allow-Origin header
9+
* @default "*"
10+
*/
11+
allowOrigin?: string;
12+
13+
/**
14+
* Access-Control-Allow-Methods header
15+
* @default "GET, POST, OPTIONS"
16+
*/
17+
allowMethods?: string;
18+
19+
/**
20+
* Access-Control-Allow-Headers header
21+
* @default "Content-Type, Authorization, x-api-key"
22+
*/
23+
allowHeaders?: string;
24+
25+
/**
26+
* Access-Control-Expose-Headers header
27+
* @default "Content-Type, Authorization, x-api-key"
28+
*/
29+
exposeHeaders?: string;
30+
31+
/**
32+
* Access-Control-Max-Age header for preflight requests
33+
* @default "86400"
34+
*/
35+
maxAge?: string;
36+
}
37+
338
/**
439
* Configuration options for SSE transport
540
*/
@@ -32,6 +67,11 @@ export interface SSETransportConfig {
3267
*/
3368
headers?: Record<string, string>;
3469

70+
/**
71+
* CORS configuration
72+
*/
73+
cors?: CORSConfig;
74+
3575
/**
3676
* Authentication configuration
3777
*/
@@ -41,9 +81,21 @@ export interface SSETransportConfig {
4181
/**
4282
* Internal configuration type with required fields except headers
4383
*/
44-
export type SSETransportConfigInternal = Required<Omit<SSETransportConfig, 'headers' | 'auth'>> & {
84+
export type SSETransportConfigInternal = Required<Omit<SSETransportConfig, 'headers' | 'auth' | 'cors'>> & {
4585
headers?: Record<string, string>;
4686
auth?: AuthConfig;
87+
cors?: CORSConfig;
88+
};
89+
90+
/**
91+
* Default CORS configuration
92+
*/
93+
export const DEFAULT_CORS_CONFIG: CORSConfig = {
94+
allowOrigin: "*",
95+
allowMethods: "GET, POST, OPTIONS",
96+
allowHeaders: "Content-Type, Authorization, x-api-key",
97+
exposeHeaders: "Content-Type, Authorization, x-api-key",
98+
maxAge: "86400"
4799
};
48100

49101
/**

0 commit comments

Comments
 (0)