Skip to content

Add X-Forwarded-Prefix support for SSE transport (TransparentProxy) #3071

@jhrozek

Description

@jhrozek

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:

  1. Ingress rewrites path: /playwright/sse/sse (sent to backend)
  2. MCP server responds with SSE event:
    event: endpoint
    data: /sse?sessionId=abc123
    
  3. Client constructs POST URL as http://host/sse?sessionId=abc123
  4. This fails - the correct URL should be http://host/playwright/sse?sessionId=abc123

Current Behavior

  • transport: stdio + proxyMode: sse uses HTTPSSEProxy which supports X-Forwarded-Prefix (added in commit 08f60ec)
  • transport: sse uses 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:

  1. Parse SSE events as they pass through
  2. When seeing event: endpoint followed by data: /path..., prepend the X-Forwarded-Prefix
  3. 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.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestgoPull requests that update go codekubernetesItems related to Kubernetesproxy

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions