feat: add envFrom secret injection for stdio MCP servers#114
feat: add envFrom secret injection for stdio MCP servers#114alexeykazakov wants to merge 1 commit intocodeready-toolchain:masterfrom
Conversation
- Add McpEnvFromSecret type and EnvFrom field to McpServerSpec - Validate referenced Secrets exist with specified keys before reconciling - Inject secretKeyRef env vars onto gateway container for envFrom entries - Stamp MCP secret ResourceVersions as annotations for rollout on change - Set McpServersConfigured=False on validation failure with recovery path - Include envFrom names as placeholder values in operator.json env map Signed-off-by: Alexey Kazakov <alkazako@redhat.com> Co-authored-by: Cursor <cursoragent@cursor.com>
WalkthroughThis PR adds support for MCP (Model Context Protocol) server secret-backed environment variables. It introduces new API types ( ChangesMCP Secret Environment Variable Support
Sequence DiagramsequenceDiagram
participant Controller as Controller<br/>(Reconcile)
participant API as Kubernetes API<br/>(Secrets)
participant Deployment as Gateway<br/>Deployment
participant ConfigMap as ConfigMap<br/>(MCP Config)
Controller->>Controller: Receive reconciliation request
Controller->>Controller: Validate MCP envFrom<br/>secret references exist
alt Validation Fails
Controller->>Controller: Set McpServersConfigured=False<br/>Update status
Controller-->>Deployment: (no changes)
else Validation Succeeds
Controller->>API: Fetch referenced secrets<br/>for version info
API-->>Controller: Return secrets + ResourceVersions
Controller->>Deployment: Configure gateway container<br/>with valueFrom.secretKeyRef<br/>env vars
Controller->>Deployment: Stamp pod annotations<br/>with secret versions<br/>(triggers rollout)
Controller->>ConfigMap: Inject envFrom placeholders<br/>into MCP config
Controller->>Controller: Set McpServersConfigured=True
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested labels
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@api/v1alpha1/claw_types.go`:
- Around line 247-251: Add API-level validation to reject EnvFrom when the
server is not a stdio MCP: implement/extend the CRD admission validation
(ValidateCreate and ValidateUpdate on the MCP type) to check the EnvFrom
[]McpEnvFromSecret field and return an error if it is non-empty while the server
is configured with a URL (i.e., Url is set) or otherwise not a stdio server; use
the EnvFrom and Url field names in the validation logic and include a clear
error message instructing users that envFrom is only allowed for stdio MCP
servers.
In `@internal/controller/claw_deployment.go`:
- Around line 417-467: The env injection is non-deterministic and can create
duplicate/overriding entries; to fix, build a deterministic, de-duplicated list
from instance.Spec.McpServers -> envFromEntries by sorting entries (e.g., by
Name then SecretRef.Name then SecretRef.Key) and collapsing duplicates by env
name, then examine the existing container envs (use unstructured.NestedSlice on
the container found for ClawGatewayContainerName) and for each desired env: if
an env with the same name exists and its secretKeyRef matches, skip (no-op); if
it exists but differs, replace it deterministically; otherwise append; finally
write the merged env slice back into the container so updates are stable (use
the same unstructured helpers you already use such as
unstructured.NestedSlice/NestedString and update the obj for the gateway
returned by getClawDeploymentName and filtered by DeploymentKind).
- Around line 500-521: The current annotation key built as key := serverName +
"-" + ef.Name can produce invalid Kubernetes annotation keys for unconstrained
McpServers keys or McpEnvFromSecret.Name; replace this concatenation with a
validated, deterministic identifier (e.g., compute a short hash or sanitized
slug of serverName and ef.Name) and use that identifier when populating versions
and when writing annotations (the code that sets
annotations[clawv1alpha1.AnnotationPrefixMcpSecretVersion+key+clawv1alpha1.AnnotationSuffixMcpSecretVersion]
= rv). Ensure the hashing/sanitization always produces only allowed characters,
keeps the length below 63 characters, and remains stable so the same inputs map
to the same annotation key.
In `@internal/controller/claw_resource_controller.go`:
- Around line 461-464: The reconcile path won't be triggered for MCP-only
envFrom secret changes because findClawsReferencingSecret only inspects
credential secret refs; update the secret-to-Claw mapping so MCP envFrom secrets
are included: extend findClawsReferencingSecret (or the controller's Secret
watch mapper) to also search MCP-related EnvFromSource entries (and any MCP
field that references a Secret) for matches and return the associated Claw
objects so that stampMcpSecretVersionAnnotation is invoked on envFrom secret
updates.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Enterprise
Run ID: 95b24f93-bdf3-4464-8e4f-e7ad9b9e8b12
📒 Files selected for processing (9)
api/v1alpha1/claw_types.goapi/v1alpha1/zz_generated.deepcopy.goconfig/crd/bases/claw.sandbox.redhat.com_claws.yamldocs/proposals/mcp-support-design.mdinternal/controller/claw_deployment.gointernal/controller/claw_deployment_test.gointernal/controller/claw_mcp.gointernal/controller/claw_mcp_test.gointernal/controller/claw_resource_controller.go
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Unit Tests
- GitHub Check: E2E Tests
🧰 Additional context used
📓 Path-based instructions (1)
**
⚙️ CodeRabbit configuration file
-Focus on major issues impacting performance, readability, maintainability and security. Avoid nitpicks and avoid verbosity.
Files:
api/v1alpha1/zz_generated.deepcopy.goconfig/crd/bases/claw.sandbox.redhat.com_claws.yamlinternal/controller/claw_deployment.goapi/v1alpha1/claw_types.gointernal/controller/claw_resource_controller.gointernal/controller/claw_mcp_test.gointernal/controller/claw_deployment_test.godocs/proposals/mcp-support-design.mdinternal/controller/claw_mcp.go
🪛 LanguageTool
docs/proposals/mcp-support-design.md
[grammar] ~254-~254: Ensure spelling is correct
Context: ... and claw_proxy_test.go ### Phase 2: Stdio MCP secret injection (tier 3 — escape h...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)
🔇 Additional comments (6)
internal/controller/claw_mcp_test.go (6)
26-31: LGTM!The new imports are appropriately added to support the integration tests for deployment verification and reconcile request construction.
244-330: LGTM!Well-structured unit tests for
buildMcpServerConfigcovering the key scenarios: envFrom merging with plain env, envFrom-only, neither env nor envFrom, HTTP server exclusion, and multiple envFrom entries.
332-359: LGTM!Good integration test verifying that
injectMcpServersIntoConfigMapcorrectly propagates both plain env values and envFrom placeholder values into the MCP server configuration.
551-635: LGTM!Comprehensive integration test that validates the full reconciliation flow: ConfigMap placeholder injection, gateway deployment secretKeyRef env var mounting, and condition status verification.
637-733: LGTM!Well-structured failure path tests that verify both the reconcile error and condition state when envFrom secrets or keys are missing. The message assertions ensure user-facing error details are informative.
735-897: LGTM!Excellent test coverage including:
- Secret version annotation stamping for rollout triggers
- Backward compatibility for MCP servers without envFrom
- Recovery path from validation failure after creating missing secret
- CEL validation acceptance of envFrom configuration
| // EnvFrom are secret-backed environment variables mounted on the gateway | ||
| // container and inherited by the stdio server subprocess (tier 3). | ||
| // Use only when the proxy-placeholder pattern (tier 2) is not viable. | ||
| // +optional | ||
| EnvFrom []McpEnvFromSecret `json:"envFrom,omitempty"` |
There was a problem hiding this comment.
Restrict envFrom to stdio MCP servers at the API level.
envFrom is documented as stdio-only, but the schema currently allows it when url is set. That permits unnecessary secret exposure on gateway for HTTP servers.
🔧 Suggested validation
// +kubebuilder:validation:XValidation:rule="has(self.command) || has(self.url)",message="either command (stdio) or url (HTTP) must be set"
// +kubebuilder:validation:XValidation:rule="!has(self.command) || !has(self.url)",message="command and url are mutually exclusive"
+// +kubebuilder:validation:XValidation:rule="!has(self.url) || !has(self.envFrom)",message="envFrom is only valid for stdio MCP servers"
type McpServerSpec struct {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@api/v1alpha1/claw_types.go` around lines 247 - 251, Add API-level validation
to reject EnvFrom when the server is not a stdio MCP: implement/extend the CRD
admission validation (ValidateCreate and ValidateUpdate on the MCP type) to
check the EnvFrom []McpEnvFromSecret field and return an error if it is
non-empty while the server is configured with a URL (i.e., Url is set) or
otherwise not a stdio server; use the EnvFrom and Url field names in the
validation logic and include a clear error message instructing users that
envFrom is only allowed for stdio MCP servers.
| var envFromEntries []clawv1alpha1.McpEnvFromSecret | ||
| for _, spec := range instance.Spec.McpServers { | ||
| envFromEntries = append(envFromEntries, spec.EnvFrom...) | ||
| } | ||
| if len(envFromEntries) == 0 { | ||
| return nil | ||
| } | ||
|
|
||
| gatewayName := getClawDeploymentName(instance.Name) | ||
| for _, obj := range objects { | ||
| if obj.GetKind() != DeploymentKind || obj.GetName() != gatewayName { | ||
| continue | ||
| } | ||
|
|
||
| containers, found, err := unstructured.NestedSlice(obj.Object, "spec", "template", "spec", "containers") | ||
| if err != nil { | ||
| return fmt.Errorf("failed to get containers from claw deployment: %w", err) | ||
| } | ||
| if !found { | ||
| return fmt.Errorf("containers field not found in claw deployment") | ||
| } | ||
|
|
||
| containerIdx := -1 | ||
| var container map[string]any | ||
| for i, c := range containers { | ||
| cm, ok := c.(map[string]any) | ||
| if !ok { | ||
| continue | ||
| } | ||
| if name, _, _ := unstructured.NestedString(cm, "name"); name == ClawGatewayContainerName { | ||
| containerIdx = i | ||
| container = cm | ||
| break | ||
| } | ||
| } | ||
| if containerIdx < 0 { | ||
| return fmt.Errorf("container %q not found in claw deployment", ClawGatewayContainerName) | ||
| } | ||
|
|
||
| envVars, _, _ := unstructured.NestedSlice(container, "env") | ||
| for _, ef := range envFromEntries { | ||
| envVars = append(envVars, map[string]any{ | ||
| "name": ef.Name, | ||
| "valueFrom": map[string]any{ | ||
| "secretKeyRef": map[string]any{ | ||
| "name": ef.SecretRef.Name, | ||
| "key": ef.SecretRef.Key, | ||
| }, | ||
| }, | ||
| }) | ||
| } |
There was a problem hiding this comment.
Stabilize and validate gateway env injection to prevent collisions and churn.
Line 418 iterates over a map, so injected env var order is non-deterministic. Also, env names are appended without duplicate checks, which can override existing container envs or conflict across MCP servers.
🔧 Suggested hardening
func configureGatewayForMcpServers(objects []*unstructured.Unstructured, instance *clawv1alpha1.Claw) error {
- var envFromEntries []clawv1alpha1.McpEnvFromSecret
- for _, spec := range instance.Spec.McpServers {
- envFromEntries = append(envFromEntries, spec.EnvFrom...)
- }
+ type envBinding struct {
+ name string
+ secret string
+ secretKey string
+ }
+ var envFromEntries []envBinding
+ serverNames := make([]string, 0, len(instance.Spec.McpServers))
+ for name := range instance.Spec.McpServers {
+ serverNames = append(serverNames, name)
+ }
+ sort.Strings(serverNames)
+ for _, serverName := range serverNames {
+ spec := instance.Spec.McpServers[serverName]
+ for _, ef := range spec.EnvFrom {
+ envFromEntries = append(envFromEntries, envBinding{
+ name: ef.Name, secret: ef.SecretRef.Name, secretKey: ef.SecretRef.Key,
+ })
+ }
+ }
if len(envFromEntries) == 0 {
return nil
}
@@
- envVars, _, _ := unstructured.NestedSlice(container, "env")
- for _, ef := range envFromEntries {
+ envVars, _, _ := unstructured.NestedSlice(container, "env")
+ seen := map[string]struct{}{}
+ for _, e := range envVars {
+ if em, ok := e.(map[string]any); ok {
+ if n, ok := em["name"].(string); ok && n != "" {
+ seen[n] = struct{}{}
+ }
+ }
+ }
+ for _, ef := range envFromEntries {
+ if _, exists := seen[ef.name]; exists {
+ return fmt.Errorf("duplicate gateway env var %q from MCP envFrom", ef.name)
+ }
+ seen[ef.name] = struct{}{}
envVars = append(envVars, map[string]any{
- "name": ef.Name,
+ "name": ef.name,
"valueFrom": map[string]any{
"secretKeyRef": map[string]any{
- "name": ef.SecretRef.Name,
- "key": ef.SecretRef.Key,
+ "name": ef.secret,
+ "key": ef.secretKey,
},
},
})
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/controller/claw_deployment.go` around lines 417 - 467, The env
injection is non-deterministic and can create duplicate/overriding entries; to
fix, build a deterministic, de-duplicated list from instance.Spec.McpServers ->
envFromEntries by sorting entries (e.g., by Name then SecretRef.Name then
SecretRef.Key) and collapsing duplicates by env name, then examine the existing
container envs (use unstructured.NestedSlice on the container found for
ClawGatewayContainerName) and for each desired env: if an env with the same name
exists and its secretKeyRef matches, skip (no-op); if it exists but differs,
replace it deterministically; otherwise append; finally write the merged env
slice back into the container so updates are stable (use the same unstructured
helpers you already use such as unstructured.NestedSlice/NestedString and update
the obj for the gateway returned by getClawDeploymentName and filtered by
DeploymentKind).
| key := serverName + "-" + ef.Name | ||
| versions[key] = secret.ResourceVersion | ||
| } | ||
| } | ||
|
|
||
| if len(versions) == 0 { | ||
| return nil | ||
| } | ||
|
|
||
| gatewayName := getClawDeploymentName(instance.Name) | ||
| for _, obj := range objects { | ||
| if obj.GetKind() != DeploymentKind || obj.GetName() != gatewayName { | ||
| continue | ||
| } | ||
|
|
||
| annotations, _, _ := unstructured.NestedStringMap(obj.Object, "spec", "template", "metadata", "annotations") | ||
| if annotations == nil { | ||
| annotations = make(map[string]string) | ||
| } | ||
| for key, rv := range versions { | ||
| annotations[clawv1alpha1.AnnotationPrefixMcpSecretVersion+key+clawv1alpha1.AnnotationSuffixMcpSecretVersion] = rv | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "AnnotationPrefixMcpSecretVersion|AnnotationSuffixMcpSecretVersion|serverName \\+ \"-\" \\+ ef.Name" internal/controller/claw_deployment.go api/v1alpha1/claw_types.go -C2
rg -n "type McpServerSpec|EnvFrom \\[]McpEnvFromSecret|type McpEnvFromSecret|Name string `json:\"name\"`" api/v1alpha1/claw_types.go -C2Repository: codeready-toolchain/claw-operator
Length of output: 2711
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find where serverName comes from in the function context
rg -B 30 "key := serverName" internal/controller/claw_deployment.go | head -50
# Check if serverName has any validation constraints
rg -n "serverName" internal/controller/claw_deployment.go | head -20Repository: codeready-toolchain/claw-operator
Length of output: 1480
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check the CRD definition for MCP servers - look for serverName/name field constraints
rg -A 10 "type McpServer |name.*string|Name.*string" api/v1alpha1/claw_types.go | grep -A 10 "McpServer"Repository: codeready-toolchain/claw-operator
Length of output: 59
🏁 Script executed (no clone):
# Verify Kubernetes annotation key constraints
curl -s "https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/" 2>/dev/null | grep -i "annotation.*key\|valid.*key\|key.*rule" | head -5 || echo "Will verify from k8s docs knowledge"
Length of output: 1966
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find McpServers field definition in the CRD
rg -n "McpServers" api/v1alpha1/claw_types.go -B 3 -A 3
# Also search for field definitions to understand constraints
rg -n "type Claw struct" api/v1alpha1/claw_types.go -A 30Repository: codeready-toolchain/claw-operator
Length of output: 1373
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Look for validation rules on name-related fields
rg -n "kubebuilder:validation" api/v1alpha1/claw_types.go | grep -i "name\|max\|pattern\|min" | head -20Repository: codeready-toolchain/claw-operator
Length of output: 558
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Get the full McpEnvFromSecret definition with all validation rules
rg -n "type McpEnvFromSecret" api/v1alpha1/claw_types.go -A 20
# Also check if there's pattern validation on any name fields
rg -n "Pattern" api/v1alpha1/claw_types.goRepository: codeready-toolchain/claw-operator
Length of output: 900
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check if there are validation rules elsewhere on serverName (CRD patterns, webhook validation, etc)
rg -n "McpServers|serverName" api/v1alpha1/claw_types.go | head -10
# Check if there's any validation/transformation of serverName in the controller
rg -n "serverName" internal/controller/ -B 3 -A 3Repository: codeready-toolchain/claw-operator
Length of output: 3291
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check the actual line 282 and surrounding context for any kubebuilder validation on McpServers
sed -n '278,290p' api/v1alpha1/claw_types.goRepository: codeready-toolchain/claw-operator
Length of output: 559
MCP secret-version annotation keys can become invalid for legal CR values.
The annotation key is constructed from serverName + "-" + ef.Name without validation. Since McpServers map keys and McpEnvFromSecret.Name field have no constraints preventing uppercase letters, underscores, special characters, or excessive length, the resulting annotation key can violate Kubernetes naming rules (must start/end with [a-z0-9A-Z], allow only [-_.] between, max 63 characters) and cause deployment failures.
Consider hashing the inputs to ensure keys remain valid regardless of user-supplied values:
🔧 Suggested approach
+func mcpSecretAnnotationToken(serverName, envName string) string {
+ raw := serverName + "-" + envName
+ sum := sha256.Sum256([]byte(raw))
+ return fmt.Sprintf("%x", sum[:8])
+}
@@
- key := serverName + "-" + ef.Name
+ key := mcpSecretAnnotationToken(serverName, ef.Name)
versions[key] = secret.ResourceVersion📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| key := serverName + "-" + ef.Name | |
| versions[key] = secret.ResourceVersion | |
| } | |
| } | |
| if len(versions) == 0 { | |
| return nil | |
| } | |
| gatewayName := getClawDeploymentName(instance.Name) | |
| for _, obj := range objects { | |
| if obj.GetKind() != DeploymentKind || obj.GetName() != gatewayName { | |
| continue | |
| } | |
| annotations, _, _ := unstructured.NestedStringMap(obj.Object, "spec", "template", "metadata", "annotations") | |
| if annotations == nil { | |
| annotations = make(map[string]string) | |
| } | |
| for key, rv := range versions { | |
| annotations[clawv1alpha1.AnnotationPrefixMcpSecretVersion+key+clawv1alpha1.AnnotationSuffixMcpSecretVersion] = rv | |
| } | |
| key := mcpSecretAnnotationToken(serverName, ef.Name) | |
| versions[key] = secret.ResourceVersion | |
| } | |
| } | |
| if len(versions) == 0 { | |
| return nil | |
| } | |
| gatewayName := getClawDeploymentName(instance.Name) | |
| for _, obj := range objects { | |
| if obj.GetKind() != DeploymentKind || obj.GetName() != gatewayName { | |
| continue | |
| } | |
| annotations, _, _ := unstructured.NestedStringMap(obj.Object, "spec", "template", "metadata", "annotations") | |
| if annotations == nil { | |
| annotations = make(map[string]string) | |
| } | |
| for key, rv := range versions { | |
| annotations[clawv1alpha1.AnnotationPrefixMcpSecretVersion+key+clawv1alpha1.AnnotationSuffixMcpSecretVersion] = rv | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/controller/claw_deployment.go` around lines 500 - 521, The current
annotation key built as key := serverName + "-" + ef.Name can produce invalid
Kubernetes annotation keys for unconstrained McpServers keys or
McpEnvFromSecret.Name; replace this concatenation with a validated,
deterministic identifier (e.g., compute a short hash or sanitized slug of
serverName and ef.Name) and use that identifier when populating versions and
when writing annotations (the code that sets
annotations[clawv1alpha1.AnnotationPrefixMcpSecretVersion+key+clawv1alpha1.AnnotationSuffixMcpSecretVersion]
= rv). Ensure the hashing/sanitization always produces only allowed characters,
keeps the length below 63 characters, and remains stable so the same inputs map
to the same annotation key.
| // Stamp MCP envFrom Secret versions on gateway deployment for rollout | ||
| if err := r.stampMcpSecretVersionAnnotation(ctx, objects, instance); err != nil { | ||
| return ctrl.Result{}, fmt.Errorf("failed to stamp MCP secret version annotations: %w", err) | ||
| } |
There was a problem hiding this comment.
MCP secret updates won’t reliably trigger this new rollout path.
This stamping logic is correct, but reconcile won’t run for many MCP secret changes because findClawsReferencingSecret currently maps only credential secret references. MCP-only envFrom secrets can change without enqueueing the Claw.
🔧 Suggested mapper update
func (r *ClawResourceReconciler) findClawsReferencingSecret(ctx context.Context, obj client.Object) []reconcile.Request {
@@
for _, instance := range openClawList.Items {
+ matches := false
for _, cred := range instance.Spec.Credentials {
if referencesSecret(cred, secret.Name) {
- requests = append(requests, reconcile.Request{
- NamespacedName: types.NamespacedName{
- Name: instance.Name,
- Namespace: instance.Namespace,
- },
- })
- break
+ matches = true
+ break
+ }
+ }
+ if !matches {
+ for _, mcp := range instance.Spec.McpServers {
+ for _, ef := range mcp.EnvFrom {
+ if ef.SecretRef.Name == secret.Name {
+ matches = true
+ break
+ }
+ }
+ if matches {
+ break
+ }
}
}
+ if matches {
+ requests = append(requests, reconcile.Request{
+ NamespacedName: types.NamespacedName{
+ Name: instance.Name,
+ Namespace: instance.Namespace,
+ },
+ })
+ }
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/controller/claw_resource_controller.go` around lines 461 - 464, The
reconcile path won't be triggered for MCP-only envFrom secret changes because
findClawsReferencingSecret only inspects credential secret refs; update the
secret-to-Claw mapping so MCP envFrom secrets are included: extend
findClawsReferencingSecret (or the controller's Secret watch mapper) to also
search MCP-related EnvFromSource entries (and any MCP field that references a
Secret) for matches and return the associated Claw objects so that
stampMcpSecretVersionAnnotation is invoked on envFrom secret updates.
Summary by CodeRabbit