Skip to content

Commit e66720b

Browse files
ChrisJBurnsclaude
andcommitted
Normalize proxyMode to always reflect effective HTTP protocol
proxyMode was only meaningful for stdio transports but could be set on any transport, returning empty or misleading values to clients. This caused confusion and required every client to implement conditional logic via GetEffectiveProxyMode() to determine the actual protocol. Add EffectiveProxyMode() to pkg/transport/types as the canonical typed implementation, and NormalizeProxyMode() on RunConfig to apply it at creation and load time. This ensures proxyMode is always the effective value in persisted configs and API responses. Also fixes the conflicting default in StdioTransport (SSE vs streamable-http everywhere else). Fixes #3296 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 28370f6 commit e66720b

8 files changed

Lines changed: 192 additions & 16 deletions

File tree

pkg/export/k8s.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ func runConfigToMCPServer(config *runner.RunConfig) (*v1alpha1.MCPServer, error)
8888
}
8989

9090
// Set proxy mode if transport is stdio
91-
if config.Transport == types.TransportTypeStdio && config.ProxyMode != "" {
91+
if config.Transport == types.TransportTypeStdio {
9292
mcpServer.Spec.ProxyMode = string(config.ProxyMode)
9393
}
9494

pkg/runner/config.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,9 @@ type RunConfig struct {
171171
// TrustProxyHeaders indicates whether to trust X-Forwarded-* headers from reverse proxies
172172
TrustProxyHeaders bool `json:"trust_proxy_headers,omitempty" yaml:"trust_proxy_headers,omitempty"`
173173

174-
// ProxyMode is the proxy mode for stdio transport ("sse" or "streamable-http")
174+
// ProxyMode is the effective HTTP protocol the proxy uses.
175+
// For stdio transports, this is the configured mode (sse or streamable-http).
176+
// For direct transports (sse/streamable-http), this matches the transport type.
175177
// Note: "sse" is deprecated; use "streamable-http" instead.
176178
ProxyMode types.ProxyMode `json:"proxy_mode,omitempty" yaml:"proxy_mode,omitempty" enums:"sse,streamable-http"`
177179

@@ -261,6 +263,12 @@ type SessionRedisConfig struct {
261263
KeyPrefix string `json:"key_prefix,omitempty" yaml:"key_prefix,omitempty"`
262264
}
263265

266+
// NormalizeProxyMode sets ProxyMode to the effective value based on the
267+
// transport type, so downstream readers always see the actual HTTP protocol.
268+
func (c *RunConfig) NormalizeProxyMode() {
269+
c.ProxyMode = types.EffectiveProxyMode(c.Transport, c.ProxyMode)
270+
}
271+
264272
// WriteJSON serializes the RunConfig to JSON and writes it to the provided writer
265273
func (c *RunConfig) WriteJSON(w io.Writer) error {
266274
// Ensure the schema version is set
@@ -313,6 +321,9 @@ func ReadJSON(r io.Reader) (*RunConfig, error) {
313321
return nil, fmt.Errorf("failed to migrate bearer token: %w", err)
314322
}
315323

324+
// Normalize proxyMode so pre-existing configs always reflect the effective protocol
325+
config.NormalizeProxyMode()
326+
316327
return &config, nil
317328
}
318329

pkg/runner/config_builder.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -837,6 +837,9 @@ func internalRunConfigBuilder(
837837
// Set schema version.
838838
b.config.SchemaVersion = CurrentSchemaVersion
839839

840+
// Normalize proxyMode to the effective value before returning.
841+
b.config.NormalizeProxyMode()
842+
840843
return b.config, nil
841844
}
842845

pkg/runner/config_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,81 @@ func TestRunConfig_WithTransport(t *testing.T) {
9595
}
9696
}
9797

98+
func TestRunConfig_NormalizeProxyMode(t *testing.T) {
99+
t.Parallel()
100+
101+
testCases := []struct {
102+
name string
103+
config *RunConfig
104+
expected types.ProxyMode
105+
}{
106+
{
107+
name: "stdio with empty proxy mode defaults to streamable-http",
108+
config: &RunConfig{
109+
Transport: types.TransportTypeStdio,
110+
ProxyMode: "",
111+
},
112+
expected: types.ProxyModeStreamableHTTP,
113+
},
114+
{
115+
name: "stdio with sse proxy mode stays sse",
116+
config: &RunConfig{
117+
Transport: types.TransportTypeStdio,
118+
ProxyMode: types.ProxyModeSSE,
119+
},
120+
expected: types.ProxyModeSSE,
121+
},
122+
{
123+
name: "stdio with streamable-http proxy mode stays streamable-http",
124+
config: &RunConfig{
125+
Transport: types.TransportTypeStdio,
126+
ProxyMode: types.ProxyModeStreamableHTTP,
127+
},
128+
expected: types.ProxyModeStreamableHTTP,
129+
},
130+
{
131+
name: "sse transport with empty proxy mode becomes sse",
132+
config: &RunConfig{
133+
Transport: types.TransportTypeSSE,
134+
ProxyMode: "",
135+
},
136+
expected: types.ProxyMode("sse"),
137+
},
138+
{
139+
name: "sse transport with streamable-http proxy mode becomes sse",
140+
config: &RunConfig{
141+
Transport: types.TransportTypeSSE,
142+
ProxyMode: types.ProxyModeStreamableHTTP,
143+
},
144+
expected: types.ProxyMode("sse"),
145+
},
146+
{
147+
name: "streamable-http transport with empty proxy mode becomes streamable-http",
148+
config: &RunConfig{
149+
Transport: types.TransportTypeStreamableHTTP,
150+
ProxyMode: "",
151+
},
152+
expected: types.ProxyMode("streamable-http"),
153+
},
154+
{
155+
name: "streamable-http transport with sse proxy mode becomes streamable-http",
156+
config: &RunConfig{
157+
Transport: types.TransportTypeStreamableHTTP,
158+
ProxyMode: types.ProxyModeSSE,
159+
},
160+
expected: types.ProxyMode("streamable-http"),
161+
},
162+
}
163+
164+
for _, tc := range testCases {
165+
t.Run(tc.name, func(t *testing.T) {
166+
t.Parallel()
167+
tc.config.NormalizeProxyMode()
168+
assert.Equal(t, tc.expected, tc.config.ProxyMode)
169+
})
170+
}
171+
}
172+
98173
// TestRunConfig_WithPorts tests the WithPorts method
99174
// Note: This test uses actual port finding logic, so it may fail if ports are in use
100175
func TestRunConfig_WithPorts(t *testing.T) {

pkg/transport/stdio.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ func NewStdioTransport(
125125
middlewares: middlewares,
126126
prometheusHandler: prometheusHandler,
127127
shutdownCh: make(chan struct{}),
128-
proxyMode: types.ProxyModeSSE, // default to SSE for backward compatibility
128+
proxyMode: types.ProxyModeStreamableHTTP, // default to streamable-http
129129
retryConfig: defaultRetryConfig(),
130130
}
131131
}

pkg/transport/types/transport.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,3 +286,17 @@ func IsValidProxyMode(mode string) bool {
286286
func (p ProxyMode) String() string {
287287
return string(p)
288288
}
289+
290+
// EffectiveProxyMode determines the actual HTTP protocol the proxy is using.
291+
// For stdio transports, this returns the proxy mode (sse or streamable-http).
292+
// For direct transports (sse/streamable-http), this returns the transport type
293+
// since the transport itself is the protocol.
294+
func EffectiveProxyMode(transportType TransportType, proxyMode ProxyMode) ProxyMode {
295+
if transportType == TransportTypeStdio {
296+
if proxyMode == "" {
297+
return ProxyModeStreamableHTTP
298+
}
299+
return proxyMode
300+
}
301+
return ProxyMode(transportType.String())
302+
}

pkg/transport/types/transport_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,86 @@ func TestTransportTypeConstants(t *testing.T) {
159159
assert.Equal(t, "inspector", string(TransportTypeInspector))
160160
}
161161

162+
func TestEffectiveProxyMode(t *testing.T) {
163+
t.Parallel()
164+
165+
tests := []struct {
166+
name string
167+
transport TransportType
168+
proxyMode ProxyMode
169+
expected ProxyMode
170+
}{
171+
{
172+
name: "stdio with sse proxy mode returns sse",
173+
transport: TransportTypeStdio,
174+
proxyMode: ProxyModeSSE,
175+
expected: ProxyModeSSE,
176+
},
177+
{
178+
name: "stdio with streamable-http proxy mode returns streamable-http",
179+
transport: TransportTypeStdio,
180+
proxyMode: ProxyModeStreamableHTTP,
181+
expected: ProxyModeStreamableHTTP,
182+
},
183+
{
184+
name: "stdio with empty proxy mode defaults to streamable-http",
185+
transport: TransportTypeStdio,
186+
proxyMode: "",
187+
expected: ProxyModeStreamableHTTP,
188+
},
189+
{
190+
name: "sse transport with empty proxy mode returns sse",
191+
transport: TransportTypeSSE,
192+
proxyMode: "",
193+
expected: ProxyMode("sse"),
194+
},
195+
{
196+
name: "sse transport with sse proxy mode returns sse",
197+
transport: TransportTypeSSE,
198+
proxyMode: ProxyModeSSE,
199+
expected: ProxyMode("sse"),
200+
},
201+
{
202+
name: "sse transport with streamable-http proxy mode returns sse",
203+
transport: TransportTypeSSE,
204+
proxyMode: ProxyModeStreamableHTTP,
205+
expected: ProxyMode("sse"),
206+
},
207+
{
208+
name: "streamable-http transport with empty proxy mode returns streamable-http",
209+
transport: TransportTypeStreamableHTTP,
210+
proxyMode: "",
211+
expected: ProxyMode("streamable-http"),
212+
},
213+
{
214+
name: "streamable-http transport with sse proxy mode returns streamable-http",
215+
transport: TransportTypeStreamableHTTP,
216+
proxyMode: ProxyModeSSE,
217+
expected: ProxyMode("streamable-http"),
218+
},
219+
{
220+
name: "streamable-http transport with streamable-http proxy mode returns streamable-http",
221+
transport: TransportTypeStreamableHTTP,
222+
proxyMode: ProxyModeStreamableHTTP,
223+
expected: ProxyMode("streamable-http"),
224+
},
225+
{
226+
name: "inspector transport with empty proxy mode returns inspector",
227+
transport: TransportTypeInspector,
228+
proxyMode: "",
229+
expected: ProxyMode("inspector"),
230+
},
231+
}
232+
233+
for _, tt := range tests {
234+
t.Run(tt.name, func(t *testing.T) {
235+
t.Parallel()
236+
result := EffectiveProxyMode(tt.transport, tt.proxyMode)
237+
assert.Equal(t, tt.expected, result)
238+
})
239+
}
240+
}
241+
162242
func TestTransportType_RoundTrip(t *testing.T) {
163243
t.Parallel()
164244

pkg/workloads/types/types.go

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -126,18 +126,11 @@ func WorkloadFromContainerInfo(container *runtime.ContainerInfo, runConfigStore
126126
}, nil
127127
}
128128

129-
// GetEffectiveProxyMode determines the effective proxy mode that clients should use
130-
// For stdio transports, this returns the proxy mode (sse or streamable-http)
131-
// For direct transports (sse/streamable-http), this returns the transport type as the proxy mode
129+
// GetEffectiveProxyMode determines the effective proxy mode that clients should use.
130+
// For stdio transports, this returns the proxy mode (sse or streamable-http).
131+
// For direct transports (sse/streamable-http), this returns the transport type as the proxy mode.
132+
//
133+
// Prefer types.EffectiveProxyMode for new code operating on typed values.
132134
func GetEffectiveProxyMode(transportType types.TransportType, proxyMode string) string {
133-
// If the underlying transport is stdio, return the proxy mode (could be empty)
134-
if transportType == types.TransportTypeStdio {
135-
if proxyMode == "" {
136-
return types.ProxyModeStreamableHTTP.String()
137-
}
138-
return proxyMode
139-
}
140-
141-
// For direct transports (sse, streamable-http), return the transport type as the proxy mode
142-
return transportType.String()
135+
return types.EffectiveProxyMode(transportType, types.ProxyMode(proxyMode)).String()
143136
}

0 commit comments

Comments
 (0)