Skip to content
Merged
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
42 changes: 42 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,48 @@ cors:
- **Development** (`debug=true`): Permissive origin validation for easy testing
- **Production** (`debug=false`): Strict origin validation with localhost fallback for debugging

### Browser Client Configuration

For browser-based MCP clients, configure CORS to expose necessary headers:

**Basic Browser Support:**
```yaml
cors:
allow_origins: ["https://your-web-app.com"]
allow_credentials: true
expose_headers: [] # Empty by default
```

**Advanced Browser Configuration:**
```yaml
cors:
allow_origins: ["https://your-web-app.com"]
allow_credentials: true
allow_methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
allow_headers: ["Authorization", "Content-Type", "MCP-Protocol-Version"]
expose_headers: # Headers browser clients can access
- "MCP-Session-ID" # For session persistence
- "X-Request-ID" # For request tracking
- "X-Rate-Limit-Remaining" # For usage display
- "X-Custom-Header" # Your custom headers
```

**What `expose_headers` Does:**
- By default, browsers can only access basic headers (Content-Type, etc.)
- `expose_headers` allows JavaScript to read custom response headers
- Essential for MCP clients that need session IDs or request tracking

**Browser Client Example:**
```javascript
// Without expose_headers, these return null:
const sessionId = response.headers.get('MCP-Session-ID'); // null
const requestId = response.headers.get('X-Request-ID'); // null

// With expose_headers configured, these work:
const sessionId = response.headers.get('MCP-Session-ID'); // "session-123"
const requestId = response.headers.get('X-Request-ID'); // "req-456"
```

### Adding New MCP Services

1. **Configure the service** in `config.yaml`:
Expand Down
1 change: 1 addition & 0 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ cors:
- "DELETE"
- "OPTIONS"
allow_headers: ["*"] # Allowed headers (use specific headers in production)
expose_headers: [] # Headers exposed to browser clients (e.g., ["MCP-Session-ID", "X-Request-ID"])

# Storage backend configuration
storage:
Expand Down
42 changes: 42 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,48 @@ The gateway uses a component-based architecture. Key components:
- **PKCE**: Required for all authorization flows
- **Audience Binding**: Service-specific token scoping

### 7. Browser Client Support

**Decision**: CORS configuration with exposed headers for browser-based MCP clients.

**Why Browser Support Matters**:
- Web-based MCP clients need access to custom headers
- Session management requires header visibility
- OAuth flows work naturally in browsers

**Configuration for Browser Clients**:
```yaml
cors:
allow_origins: ["https://your-web-app.com"] # Your web app's domain
allow_credentials: true # Required for cookies/auth
expose_headers: # Headers browser can access
- "MCP-Session-ID" # Session tracking
- "X-Request-ID" # Request correlation
- "X-Rate-Limit-Remaining" # Usage tracking
```

**What This Enables**:
- **Session Persistence**: Browser can read and store `MCP-Session-ID`
- **Request Tracking**: Correlate requests with `X-Request-ID`
- **Rate Limit Awareness**: Display usage with rate limit headers

**Example Browser Client**:
```javascript
// Browser can now access exposed headers
const response = await fetch('https://gateway.example.com/service/mcp', {
headers: {
'Authorization': 'Bearer ' + accessToken,
'MCP-Protocol-Version': '2025-06-18'
}
});

// Access exposed headers
const sessionId = response.headers.get('MCP-Session-ID');
const requestId = response.headers.get('X-Request-ID');
```

**Security Note**: Only expose headers that browser clients need. Avoid exposing sensitive internal headers.

## Why These Decisions?

**Focus**: Do one thing well - transparent OAuth for MCP HTTP services
Expand Down
3 changes: 3 additions & 0 deletions src/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class CorsConfig:
default_factory=lambda: ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
)
allow_headers: List[str] = field(default_factory=lambda: ["*"])
expose_headers: List[str] = field(default_factory=list)


@dataclass
Expand Down Expand Up @@ -286,6 +287,7 @@ def load_config(self) -> GatewayConfig:
"allow_methods", ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
),
allow_headers=cors_data.get("allow_headers", ["*"]),
expose_headers=cors_data.get("expose_headers", []),
)

# Parse Storage configuration
Expand Down Expand Up @@ -381,6 +383,7 @@ def save_config(self) -> None:
"allow_credentials": self.config.cors.allow_credentials,
"allow_methods": self.config.cors.allow_methods,
"allow_headers": self.config.cors.allow_headers,
"expose_headers": self.config.cors.expose_headers,
},
"oauth_providers": {},
"mcp_services": {},
Expand Down
1 change: 1 addition & 0 deletions src/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ def _setup_middleware(self):
allow_credentials=self.config.cors.allow_credentials,
allow_methods=self.config.cors.allow_methods,
allow_headers=self.config.cors.allow_headers,
expose_headers=self.config.cors.expose_headers,
)

def _setup_routes(self):
Expand Down
3 changes: 3 additions & 0 deletions tests/config/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ def test_load_config_from_yaml(self):
allow_credentials: false
allow_methods: ["GET", "POST"]
allow_headers: ["Authorization", "Content-Type"]
expose_headers: ["MCP-Session-ID", "X-Request-ID"]

oauth_providers:
github:
Expand Down Expand Up @@ -134,6 +135,7 @@ def test_load_config_from_yaml(self):
assert config.cors.allow_credentials is False
assert config.cors.allow_methods == ["GET", "POST"]
assert config.cors.allow_headers == ["Authorization", "Content-Type"]
assert config.cors.expose_headers == ["MCP-Session-ID", "X-Request-ID"]

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

def test_gateway_config_defaults(self):
"""Test gateway config with defaults."""
Expand Down