Bug description
On operator-managed VirtualMCPServer (observed on v0.42.1), setting:
spec:
config:
telemetry:
enablePrometheusMetricsPath: true
is accepted by the API and stored on the CR, but the operator does not
render it into the vmcp ConfigMap. The running process never registers a
Prometheus handler on /metrics.
GET /metrics with Accept: text/plain (or Prometheus default) then falls
through to the MCP streamable-HTTP handler and returns:
HTTP/1.1 406 Not Acceptable
Content-Type: application/json
{"jsonrpc":"2.0","id":"server-error","error":{"code":-32600,"message":"Not Acceptable: Client must accept text/event-stream"}}
That makes Prometheus ServiceMonitors look broken even though the CR “enabled”
metrics. This blocked collecting Go runtime memory series requested on #5860.
Root cause (code)
cmd/thv-operator/pkg/vmcpconfig/converter.go normalizeTelemetry only reads
spec.telemetryConfigRef → MCPTelemetryConfig. Inline spec.config.telemetry
returns nil after #4819 (“Remove deprecated inline telemetry field from CRDs”),
while:
- the CRD still exposes
spec.config.telemetry (embedded vmcp config type)
- docs still said the inline field “still works” for VirtualMCPServer
- there is no status condition or validation error when inline is set without a ref
When config.Telemetry is nil at serve time, pkg/vmcp/server/server.go skips
mux.Handle("/metrics", …) and the MCP catch-all owns the path.
Steps to reproduce
-
Install operator + CRDs v0.42.1 (or current main with the ignore behavior).
-
Create a VirtualMCPServer with only inline:
apiVersion: toolhive.stacklok.dev/v1beta1
kind: VirtualMCPServer
metadata:
name: demo
spec:
groupRef:
name: <existing-group>
incomingAuth:
type: anonymous
config:
telemetry:
enablePrometheusMetricsPath: true
-
Inspect the operator-managed ConfigMap (*-vmcp-config): no telemetry: key.
-
curl -H 'Accept: text/plain' http://<vmcp-svc>:4483/metrics → 406 JSON-RPC as above.
-
curl http://<vmcp-svc>:4483/health → 200 {"status":"ok"} (health is registered).
Expected behavior
Either:
- Documented preferred path works and is discoverable: requiring
spec.telemetryConfigRef + MCPTelemetryConfig is fine, but inline must
not be a silent no-op (status Warning condition and/or validation), and docs
must not claim inline still works for the operator; or
- Deprecated inline continues to apply until removed from the CRD, with a
deprecation log / condition pointing at telemetryConfigRef.
When Prometheus path is actually enabled, GET /metrics should return
Prometheus text (HTTP 200), not MCP 406.
Actual behavior
Silent ignore of inline telemetry → no /metrics route → MCP 406 on scrape.
No condition on the VirtualMCPServer warns that telemetry was dropped.
Environment
- ToolHive operator + vmcp image: v0.42.1
- Single-node Kubernetes
- Four VirtualMCPServers (backend counts ~12–17); no private hostnames in this report
Workaround
Use the shared config resource:
apiVersion: toolhive.stacklok.dev/v1beta1
kind: MCPTelemetryConfig
metadata:
name: vmcp-prometheus
spec:
prometheus:
enabled: true
---
apiVersion: toolhive.stacklok.dev/v1beta1
kind: VirtualMCPServer
metadata:
name: demo
spec:
telemetryConfigRef:
name: vmcp-prometheus
serviceName: demo
# ...groupRef, incomingAuth, etc.
Related
Proposed fix
Restore deprecated inline fallback when TelemetryConfigRef is unset (ref
still wins), log a migration warning, fix docs, and cover with unit tests.
PR forthcoming.
Bug description
On operator-managed VirtualMCPServer (observed on v0.42.1), setting:
is accepted by the API and stored on the CR, but the operator does not
render it into the vmcp ConfigMap. The running process never registers a
Prometheus handler on
/metrics.GET /metricswithAccept: text/plain(or Prometheus default) then fallsthrough to the MCP streamable-HTTP handler and returns:
That makes Prometheus ServiceMonitors look broken even though the CR “enabled”
metrics. This blocked collecting Go runtime memory series requested on #5860.
Root cause (code)
cmd/thv-operator/pkg/vmcpconfig/converter.gonormalizeTelemetryonly readsspec.telemetryConfigRef→MCPTelemetryConfig. Inlinespec.config.telemetryreturns
nilafter #4819 (“Remove deprecated inline telemetry field from CRDs”),while:
spec.config.telemetry(embedded vmcp config type)When
config.Telemetryis nil at serve time,pkg/vmcp/server/server.goskipsmux.Handle("/metrics", …)and the MCP catch-all owns the path.Steps to reproduce
Install operator + CRDs v0.42.1 (or current main with the ignore behavior).
Create a VirtualMCPServer with only inline:
Inspect the operator-managed ConfigMap (
*-vmcp-config): notelemetry:key.curl -H 'Accept: text/plain' http://<vmcp-svc>:4483/metrics→ 406 JSON-RPC as above.curl http://<vmcp-svc>:4483/health→ 200{"status":"ok"}(health is registered).Expected behavior
Either:
spec.telemetryConfigRef+MCPTelemetryConfigis fine, but inline mustnot be a silent no-op (status Warning condition and/or validation), and docs
must not claim inline still works for the operator; or
deprecation log / condition pointing at
telemetryConfigRef.When Prometheus path is actually enabled,
GET /metricsshould returnPrometheus text (HTTP 200), not MCP 406.
Actual behavior
Silent ignore of inline telemetry → no
/metricsroute → MCP 406 on scrape.No condition on the VirtualMCPServer warns that telemetry was dropped.
Environment
Workaround
Use the shared config resource:
Related
metrics actually scrape
claiming “still works”, producing this footgun
Proposed fix
Restore deprecated inline fallback when
TelemetryConfigRefis unset (refstill wins), log a migration warning, fix docs, and cover with unit tests.
PR forthcoming.