Skip to content
Open
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
12 changes: 12 additions & 0 deletions cmd/thv-operator/api/v1beta1/mcpserver_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,18 @@ type MCPServerSpec struct {
// +optional
TrustProxyHeaders bool `json:"trustProxyHeaders,omitempty"`

// RedactToolResultSecrets enables best-effort credential-shape scanning
// on tools/call responses (AWS/GitHub/Slack/Google/Stripe keys, JWTs, PEM
// private keys): matches are redacted before the response reaches the
// client. Off by default; enable when the backend MCP server is not
// fully trusted. This setting is ONLY applicable when Transport is

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This says the field is ONLY applicable when Transport is stdio and has no effect for streamable-http or sse, but the PR wires both of those through the transparent proxy, and factory.go sets httpTransport.redactToolResultSecrets for TransportTypeSSE and TransportTypeStreamableHTTP. Reads like text from an earlier iteration that didn't get updated when paths 2 and 3 landed.

Worth fixing because it's baked into both generated CRD files, both chart templates (twice each, two versions), and docs/operator/crd-api.md. So it's a customer-facing doc telling operators the field is inert exactly where it now works. The swagger description got it right, so it's just this comment plus regen.

// "stdio" (the streamable-HTTP proxy path) -- it has no effect when
// Transport is "streamable-http" or "sse" (those reverse-proxy to an
// already-HTTP backend without inspecting message content).
// +kubebuilder:default=false
// +optional
RedactToolResultSecrets bool `json:"redactToolResultSecrets,omitempty"`

// EndpointPrefix is the path prefix to prepend to SSE endpoint URLs.
// This is used to handle path-based ingress routing scenarios where the ingress
// strips a path prefix before forwarding to the backend.
Expand Down
1 change: 1 addition & 0 deletions cmd/thv-operator/controllers/mcpserver_runconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ func (r *MCPServerReconciler) createRunConfigFromMCPServer(m *mcpv1beta1.MCPServ
runner.WithProxyMode(transporttypes.ProxyMode(effectiveProxyMode)),
runner.WithHost(proxyHost),
runner.WithTrustProxyHeaders(m.Spec.TrustProxyHeaders),
runner.WithRedactToolResultSecrets(m.Spec.RedactToolResultSecrets),
runner.WithEndpointPrefix(m.Spec.EndpointPrefix),
runner.WithToolsFilter(toolsFilter),
runner.WithEnvVars(envVars),
Expand Down
39 changes: 39 additions & 0 deletions cmd/thv-operator/controllers/mcpserver_runconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1681,3 +1681,42 @@ func TestCreateRunConfigFromMCPServer_SetsMCPServerGeneration(t *testing.T) {
assert.Equal(t, int64(7), rc.MCPServerGeneration,
"MCPServerGeneration should match MCPServer .metadata.generation")
}

// TestCreateRunConfigFromMCPServer_RedactToolResultSecrets verifies
// MCPServerSpec.RedactToolResultSecrets flows into RunConfig, mirroring
// TrustProxyHeaders's plumbing.
func TestCreateRunConfigFromMCPServer_RedactToolResultSecrets(t *testing.T) {
t.Parallel()

tests := []struct {
name string
enabled bool
}{
{name: "enabled", enabled: true},
{name: "disabled (default)", enabled: false},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

m := v1beta1test.NewMCPServer("redact-secrets-server", "default",
v1beta1test.WithImage("ghcr.io/example/mcp:v1"),
v1beta1test.Mutate(func(m *mcpv1beta1.MCPServer) {
m.Spec.RedactToolResultSecrets = tt.enabled
}))

r := newTestMCPServerReconciler(
fake.NewClientBuilder().WithScheme(testutil.NewScheme(t)).WithObjects(m).Build(),
testutil.NewScheme(t),
kubernetes.PlatformKubernetes,
)

rc, err := r.createRunConfigFromMCPServer(m)

require.NoError(t, err)
require.NotNil(t, rc)
assert.Equal(t, tt.enabled, rc.RedactToolResultSecrets)
})
}
}
9 changes: 9 additions & 0 deletions cmd/thv/app/run_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ type RunFlags struct {
// on the streamable HTTP proxy.
StrictProtocolValidation bool

// RedactToolResultSecrets enables best-effort credential-shape scanning
// on tools/call responses relayed by the streamable HTTP proxy.
RedactToolResultSecrets bool

// Endpoint prefix for SSE endpoint URLs
EndpointPrefix string

Expand Down Expand Up @@ -290,6 +294,10 @@ func AddRunFlags(cmd *cobra.Command, config *RunFlags) {
cmd.Flags().BoolVar(&config.StrictProtocolValidation, "strict-protocol-validation", false,
"Reject client requests whose MCP-Protocol-Version header is an unknown/unsupported MCP revision with HTTP 400 "+
"(streamable-HTTP proxy only; an absent header is accepted). Off by default: any version is accepted.")
cmd.Flags().BoolVar(&config.RedactToolResultSecrets, "redact-tool-result-secrets", false,
"Scan tools/call responses for credential-shaped content (AWS/GitHub/Slack/Google/Stripe keys, JWTs, PEM "+
"private keys, generic Bearer tokens) and redact matches before relaying them to the client. "+
"Off by default; enable when the backend MCP server is not fully trusted.")
cmd.Flags().BoolVar(&config.Stateless, "stateless", false,
"Declare the server as stateless (POST-only, no SSE). "+
"Use for MCP servers implementing streamable-HTTP stateless mode.")
Expand Down Expand Up @@ -705,6 +713,7 @@ func buildRunnerConfig(
runner.WithAllowDockerGateway(runFlags.AllowDockerGateway),
runner.WithTrustProxyHeaders(runFlags.TrustProxyHeaders),
runner.WithStrictProtocolValidation(runFlags.StrictProtocolValidation),
runner.WithRedactToolResultSecrets(runFlags.RedactToolResultSecrets),
runner.WithStateless(runFlags.Stateless),
runner.WithSessionTTL(runFlags.SessionTTL),
runner.WithEndpointPrefix(runFlags.EndpointPrefix),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,18 @@ spec:
- message: at least one of shared, perUser, or tools must be configured
rule: has(self.shared) || has(self.perUser) || (has(self.tools)
&& size(self.tools) > 0)
redactToolResultSecrets:
default: false
description: |-
RedactToolResultSecrets enables best-effort credential-shape scanning
on tools/call responses (AWS/GitHub/Slack/Google/Stripe keys, JWTs, PEM
private keys): matches are redacted before the response reaches the
client. Off by default; enable when the backend MCP server is not
fully trusted. This setting is ONLY applicable when Transport is
"stdio" (the streamable-HTTP proxy path) -- it has no effect when
Transport is "streamable-http" or "sse" (those reverse-proxy to an
already-HTTP backend without inspecting message content).
type: boolean
replicas:
description: |-
Replicas is the desired number of proxy runner (thv run) pod replicas.
Expand Down Expand Up @@ -1459,6 +1471,18 @@ spec:
- message: at least one of shared, perUser, or tools must be configured
rule: has(self.shared) || has(self.perUser) || (has(self.tools)
&& size(self.tools) > 0)
redactToolResultSecrets:
default: false
description: |-
RedactToolResultSecrets enables best-effort credential-shape scanning
on tools/call responses (AWS/GitHub/Slack/Google/Stripe keys, JWTs, PEM
private keys): matches are redacted before the response reaches the
client. Off by default; enable when the backend MCP server is not
fully trusted. This setting is ONLY applicable when Transport is
"stdio" (the streamable-HTTP proxy path) -- it has no effect when
Transport is "streamable-http" or "sse" (those reverse-proxy to an
already-HTTP backend without inspecting message content).
type: boolean
replicas:
description: |-
Replicas is the desired number of proxy runner (thv run) pod replicas.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3195,6 +3195,14 @@ spec:
- message: at least one of shared, perUser, or tools must be configured
rule: has(self.shared) || has(self.perUser) || (has(self.tools)
&& size(self.tools) > 0)
redactToolResultSecrets:
default: false
description: |-
RedactToolResultSecrets enables best-effort credential-shape scanning
(see pkg/mcp/secretscan) on tools/call results before they are relayed
to the client: matches are redacted in place. Opt-in (default false);
enable when a backend this vMCP aggregates is not fully trusted.
type: boolean
sessionStorage:
description: |-
SessionStorage configures session storage for stateful horizontal scaling.
Expand Down Expand Up @@ -7186,6 +7194,14 @@ spec:
- message: at least one of shared, perUser, or tools must be configured
rule: has(self.shared) || has(self.perUser) || (has(self.tools)
&& size(self.tools) > 0)
redactToolResultSecrets:
default: false
description: |-
RedactToolResultSecrets enables best-effort credential-shape scanning
(see pkg/mcp/secretscan) on tools/call results before they are relayed
to the client: matches are redacted in place. Opt-in (default false);
enable when a backend this vMCP aggregates is not fully trusted.
type: boolean
sessionStorage:
description: |-
SessionStorage configures session storage for stateful horizontal scaling.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,18 @@ spec:
- message: at least one of shared, perUser, or tools must be configured
rule: has(self.shared) || has(self.perUser) || (has(self.tools)
&& size(self.tools) > 0)
redactToolResultSecrets:
default: false
description: |-
RedactToolResultSecrets enables best-effort credential-shape scanning
on tools/call responses (AWS/GitHub/Slack/Google/Stripe keys, JWTs, PEM
private keys): matches are redacted before the response reaches the
client. Off by default; enable when the backend MCP server is not
fully trusted. This setting is ONLY applicable when Transport is
"stdio" (the streamable-HTTP proxy path) -- it has no effect when
Transport is "streamable-http" or "sse" (those reverse-proxy to an
already-HTTP backend without inspecting message content).
type: boolean
replicas:
description: |-
Replicas is the desired number of proxy runner (thv run) pod replicas.
Expand Down Expand Up @@ -1462,6 +1474,18 @@ spec:
- message: at least one of shared, perUser, or tools must be configured
rule: has(self.shared) || has(self.perUser) || (has(self.tools)
&& size(self.tools) > 0)
redactToolResultSecrets:
default: false
description: |-
RedactToolResultSecrets enables best-effort credential-shape scanning
on tools/call responses (AWS/GitHub/Slack/Google/Stripe keys, JWTs, PEM
private keys): matches are redacted before the response reaches the
client. Off by default; enable when the backend MCP server is not
fully trusted. This setting is ONLY applicable when Transport is
"stdio" (the streamable-HTTP proxy path) -- it has no effect when
Transport is "streamable-http" or "sse" (those reverse-proxy to an
already-HTTP backend without inspecting message content).
type: boolean
replicas:
description: |-
Replicas is the desired number of proxy runner (thv run) pod replicas.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3198,6 +3198,14 @@ spec:
- message: at least one of shared, perUser, or tools must be configured
rule: has(self.shared) || has(self.perUser) || (has(self.tools)
&& size(self.tools) > 0)
redactToolResultSecrets:
default: false
description: |-
RedactToolResultSecrets enables best-effort credential-shape scanning
(see pkg/mcp/secretscan) on tools/call results before they are relayed
to the client: matches are redacted in place. Opt-in (default false);
enable when a backend this vMCP aggregates is not fully trusted.
type: boolean
sessionStorage:
description: |-
SessionStorage configures session storage for stateful horizontal scaling.
Expand Down Expand Up @@ -7189,6 +7197,14 @@ spec:
- message: at least one of shared, perUser, or tools must be configured
rule: has(self.shared) || has(self.perUser) || (has(self.tools)
&& size(self.tools) > 0)
redactToolResultSecrets:
default: false
description: |-
RedactToolResultSecrets enables best-effort credential-shape scanning
(see pkg/mcp/secretscan) on tools/call results before they are relayed
to the client: matches are redacted in place. Opt-in (default false);
enable when a backend this vMCP aggregates is not fully trusted.
type: boolean
sessionStorage:
description: |-
SessionStorage configures session storage for stateful horizontal scaling.
Expand Down
1 change: 1 addition & 0 deletions docs/cli/thv_run.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions docs/operator/crd-api.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions docs/server/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading