-
Notifications
You must be signed in to change notification settings - Fork 198
Add X-Forwarded-Prefix support for SSE transport (TransparentProxy) #3071
Description
Summary
When using transport: sse with path-based ingress routing (e.g., /playwright/sse), clients receive incorrect endpoint URLs because the TransparentProxy doesn't rewrite SSE responses to include the path prefix.
Problem Description
In Kubernetes deployments with path-based ingress routing, multiple MCP servers are exposed under different path prefixes:
/playwright/* → playwright MCP server
/calculator/* → calculator MCP server
/postgres/* → postgres MCP server
When a client connects to /playwright/sse:
- Ingress rewrites path:
/playwright/sse→/sse(sent to backend) - MCP server responds with SSE event:
event: endpoint data: /sse?sessionId=abc123 - Client constructs POST URL as
http://host/sse?sessionId=abc123 - This fails - the correct URL should be
http://host/playwright/sse?sessionId=abc123
Current Behavior
transport: stdio+proxyMode: sseuses HTTPSSEProxy which supportsX-Forwarded-Prefix(added in commit 08f60ec)transport: sseuses TransparentProxy which passes SSE responses through unchanged - no X-Forwarded-Prefix support
Expected Behavior
When trustProxyHeaders: true is set and X-Forwarded-Prefix header is present, the TransparentProxy should rewrite the endpoint event's data field to include the prefix:
event: endpoint
data: /playwright/sse?sessionId=abc123
Workaround
Currently requires nginx sub_filter at the ingress level:
nginx.ingress.kubernetes.io/configuration-snippet: |
sub_filter 'data: /sse' 'data: /playwright/sse';
sub_filter 'data: /message' 'data: /playwright/message';
sub_filter_once off;
sub_filter_types text/event-stream;This workaround has limitations:
- Requires separate Ingress resources per MCP server (annotations are per-Ingress, not per-path)
- Not all ingress controllers support
sub_filter - Adds operational complexity
Proposed Solution
Extend TransparentProxy to intercept SSE responses and rewrite endpoint URLs when trustProxyHeaders is enabled, similar to how HTTPSSEProxy handles this in buildEndpointURL().
The implementation would:
- Parse SSE events as they pass through
- When seeing
event: endpointfollowed bydata: /path..., prepend theX-Forwarded-Prefix - Pass everything else through unchanged
Why Streamable HTTP Doesn't Have This Problem
With streamable-http transport, the client POSTs to the same URL it connected to - there's no separate "endpoint" event telling the client where to send messages. The client maintains the original URL with the path prefix throughout.
Environment
- ToolHive Operator in Kubernetes
- Native SSE MCP servers (e.g., Playwright MCP)
- Path-based ingress routing (nginx, Azure Application Gateway, etc.)