Skip to content

Commit 85d4f3e

Browse files
authored
Merge pull request #4 from akshay5995/expose-headers-cors
feat: add expose_headers configuration to CORS settings
2 parents 315ef09 + f310b1d commit 85d4f3e

File tree

6 files changed

+92
-0
lines changed

6 files changed

+92
-0
lines changed

CLAUDE.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,48 @@ cors:
550550
- **Development** (`debug=true`): Permissive origin validation for easy testing
551551
- **Production** (`debug=false`): Strict origin validation with localhost fallback for debugging
552552

553+
### Browser Client Configuration
554+
555+
For browser-based MCP clients, configure CORS to expose necessary headers:
556+
557+
**Basic Browser Support:**
558+
```yaml
559+
cors:
560+
allow_origins: ["https://your-web-app.com"]
561+
allow_credentials: true
562+
expose_headers: [] # Empty by default
563+
```
564+
565+
**Advanced Browser Configuration:**
566+
```yaml
567+
cors:
568+
allow_origins: ["https://your-web-app.com"]
569+
allow_credentials: true
570+
allow_methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
571+
allow_headers: ["Authorization", "Content-Type", "MCP-Protocol-Version"]
572+
expose_headers: # Headers browser clients can access
573+
- "MCP-Session-ID" # For session persistence
574+
- "X-Request-ID" # For request tracking
575+
- "X-Rate-Limit-Remaining" # For usage display
576+
- "X-Custom-Header" # Your custom headers
577+
```
578+
579+
**What `expose_headers` Does:**
580+
- By default, browsers can only access basic headers (Content-Type, etc.)
581+
- `expose_headers` allows JavaScript to read custom response headers
582+
- Essential for MCP clients that need session IDs or request tracking
583+
584+
**Browser Client Example:**
585+
```javascript
586+
// Without expose_headers, these return null:
587+
const sessionId = response.headers.get('MCP-Session-ID'); // null
588+
const requestId = response.headers.get('X-Request-ID'); // null
589+
590+
// With expose_headers configured, these work:
591+
const sessionId = response.headers.get('MCP-Session-ID'); // "session-123"
592+
const requestId = response.headers.get('X-Request-ID'); // "req-456"
593+
```
594+
553595
### Adding New MCP Services
554596

555597
1. **Configure the service** in `config.yaml`:

config.example.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ cors:
1818
- "DELETE"
1919
- "OPTIONS"
2020
allow_headers: ["*"] # Allowed headers (use specific headers in production)
21+
expose_headers: [] # Headers exposed to browser clients (e.g., ["MCP-Session-ID", "X-Request-ID"])
2122

2223
# Storage backend configuration
2324
storage:

docs/architecture.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,48 @@ The gateway uses a component-based architecture. Key components:
234234
- **PKCE**: Required for all authorization flows
235235
- **Audience Binding**: Service-specific token scoping
236236

237+
### 7. Browser Client Support
238+
239+
**Decision**: CORS configuration with exposed headers for browser-based MCP clients.
240+
241+
**Why Browser Support Matters**:
242+
- Web-based MCP clients need access to custom headers
243+
- Session management requires header visibility
244+
- OAuth flows work naturally in browsers
245+
246+
**Configuration for Browser Clients**:
247+
```yaml
248+
cors:
249+
allow_origins: ["https://your-web-app.com"] # Your web app's domain
250+
allow_credentials: true # Required for cookies/auth
251+
expose_headers: # Headers browser can access
252+
- "MCP-Session-ID" # Session tracking
253+
- "X-Request-ID" # Request correlation
254+
- "X-Rate-Limit-Remaining" # Usage tracking
255+
```
256+
257+
**What This Enables**:
258+
- **Session Persistence**: Browser can read and store `MCP-Session-ID`
259+
- **Request Tracking**: Correlate requests with `X-Request-ID`
260+
- **Rate Limit Awareness**: Display usage with rate limit headers
261+
262+
**Example Browser Client**:
263+
```javascript
264+
// Browser can now access exposed headers
265+
const response = await fetch('https://gateway.example.com/service/mcp', {
266+
headers: {
267+
'Authorization': 'Bearer ' + accessToken,
268+
'MCP-Protocol-Version': '2025-06-18'
269+
}
270+
});
271+
272+
// Access exposed headers
273+
const sessionId = response.headers.get('MCP-Session-ID');
274+
const requestId = response.headers.get('X-Request-ID');
275+
```
276+
277+
**Security Note**: Only expose headers that browser clients need. Avoid exposing sensitive internal headers.
278+
237279
## Why These Decisions?
238280

239281
**Focus**: Do one thing well - transparent OAuth for MCP HTTP services

src/config/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class CorsConfig:
4444
default_factory=lambda: ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
4545
)
4646
allow_headers: List[str] = field(default_factory=lambda: ["*"])
47+
expose_headers: List[str] = field(default_factory=list)
4748

4849

4950
@dataclass
@@ -286,6 +287,7 @@ def load_config(self) -> GatewayConfig:
286287
"allow_methods", ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
287288
),
288289
allow_headers=cors_data.get("allow_headers", ["*"]),
290+
expose_headers=cors_data.get("expose_headers", []),
289291
)
290292

291293
# Parse Storage configuration
@@ -381,6 +383,7 @@ def save_config(self) -> None:
381383
"allow_credentials": self.config.cors.allow_credentials,
382384
"allow_methods": self.config.cors.allow_methods,
383385
"allow_headers": self.config.cors.allow_headers,
386+
"expose_headers": self.config.cors.expose_headers,
384387
},
385388
"oauth_providers": {},
386389
"mcp_services": {},

src/gateway.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ def _setup_middleware(self):
263263
allow_credentials=self.config.cors.allow_credentials,
264264
allow_methods=self.config.cors.allow_methods,
265265
allow_headers=self.config.cors.allow_headers,
266+
expose_headers=self.config.cors.expose_headers,
266267
)
267268

268269
def _setup_routes(self):

tests/config/test_config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ def test_load_config_from_yaml(self):
9595
allow_credentials: false
9696
allow_methods: ["GET", "POST"]
9797
allow_headers: ["Authorization", "Content-Type"]
98+
expose_headers: ["MCP-Session-ID", "X-Request-ID"]
9899
99100
oauth_providers:
100101
github:
@@ -134,6 +135,7 @@ def test_load_config_from_yaml(self):
134135
assert config.cors.allow_credentials is False
135136
assert config.cors.allow_methods == ["GET", "POST"]
136137
assert config.cors.allow_headers == ["Authorization", "Content-Type"]
138+
assert config.cors.expose_headers == ["MCP-Session-ID", "X-Request-ID"]
137139

138140
# Check OAuth provider
139141
assert "github" in config.oauth_providers
@@ -597,6 +599,7 @@ def test_cors_config_defaults(self):
597599
assert config.allow_credentials is True
598600
assert config.allow_methods == ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
599601
assert config.allow_headers == ["*"]
602+
assert config.expose_headers == []
600603

601604
def test_gateway_config_defaults(self):
602605
"""Test gateway config with defaults."""

0 commit comments

Comments
 (0)