Problem
The operator/proxyrunner creates a headless service (mcp-<name>-headless) for every SSE/streamable-HTTP MCPServer deployment. This service selects the MCP backend StatefulSet pods directly via app: containerName. There are several problems with this:
1. The headless service bypasses the proxy runner
The service selector points to the raw MCP backend pods, not the proxy runner Deployment. This means anyone in the namespace can resolve pod DNS names and hit the backend directly, skipping all the proxy's auth, session management, and routing logic.
2. The StatefulSet serviceName doesn't match (on main)
On the current main branch, buildStatefulSetSpec sets spec.serviceName to the bare containerName (e.g. "myserver"), but the headless service is named mcp-myserver-headless. These don't match, so Kubernetes never creates the stable per-pod DNS records that the headless service is supposed to enable. The headless service exists but doesn't actually do what it was intended for.
3. For stdio transports, backend pods don't listen on the network
The transportTypeRequiresBackendServices guard skips headless service creation for stdio, but the StatefulSet's spec.serviceName still references a service that won't exist in that case.
4. Nobody consumes the headless service today
The proxy runner uses the ClusterIP service (mcp-<name>) as its target host, set via MCPServiceName in pkg/runtime/setup.go. The headless service is created but nothing reads from it.
Where the code lives
- Headless service creation:
pkg/container/kubernetes/client.go in createHeadlessService() / ensureBackendServices()
- StatefulSet serviceName:
pkg/container/kubernetes/client.go in buildStatefulSetSpec() (line ~450)
- Service selector:
pkg/container/kubernetes/client.go in applyService() (line ~993)
- Transport guard:
pkg/container/kubernetes/client.go in transportTypeRequiresBackendServices()
Proposal
Remove the headless service creation entirely. If per-pod DNS routing is needed in the future for session pinning, it should be designed with clear answers to:
- How does the proxy discover which pod handled a given session's initialize request (not random selection after the fact)?
- How do we avoid exposing raw backend pods to the cluster?
- What happens for stdio backends that don't listen on the network?
- How do we handle the StatefulSet
spec.serviceName immutability constraint for existing deployments?
Context
This came up during review of #4638, which builds on top of the headless service to add per-pod session routing. The foundation needs fixing before we add more plumbing on top.
Problem
The operator/proxyrunner creates a headless service (
mcp-<name>-headless) for every SSE/streamable-HTTP MCPServer deployment. This service selects the MCP backend StatefulSet pods directly viaapp: containerName. There are several problems with this:1. The headless service bypasses the proxy runner
The service selector points to the raw MCP backend pods, not the proxy runner Deployment. This means anyone in the namespace can resolve pod DNS names and hit the backend directly, skipping all the proxy's auth, session management, and routing logic.
2. The StatefulSet
serviceNamedoesn't match (on main)On the current
mainbranch,buildStatefulSetSpecsetsspec.serviceNameto the barecontainerName(e.g."myserver"), but the headless service is namedmcp-myserver-headless. These don't match, so Kubernetes never creates the stable per-pod DNS records that the headless service is supposed to enable. The headless service exists but doesn't actually do what it was intended for.3. For stdio transports, backend pods don't listen on the network
The
transportTypeRequiresBackendServicesguard skips headless service creation for stdio, but the StatefulSet'sspec.serviceNamestill references a service that won't exist in that case.4. Nobody consumes the headless service today
The proxy runner uses the ClusterIP service (
mcp-<name>) as its target host, set viaMCPServiceNameinpkg/runtime/setup.go. The headless service is created but nothing reads from it.Where the code lives
pkg/container/kubernetes/client.goincreateHeadlessService()/ensureBackendServices()pkg/container/kubernetes/client.goinbuildStatefulSetSpec()(line ~450)pkg/container/kubernetes/client.goinapplyService()(line ~993)pkg/container/kubernetes/client.gointransportTypeRequiresBackendServices()Proposal
Remove the headless service creation entirely. If per-pod DNS routing is needed in the future for session pinning, it should be designed with clear answers to:
spec.serviceNameimmutability constraint for existing deployments?Context
This came up during review of #4638, which builds on top of the headless service to add per-pod session routing. The foundation needs fixing before we add more plumbing on top.