Skip to content

Commit fc1aedf

Browse files
authored
Add ToolConfig CRD for tool filtering and renaming (#1814)
Allows MCPServers to reference reusable tool configurations for filtering (allow-list) and renaming tools with custom descriptions. Uses hash-based change detection to trigger reconciliation. --------- Signed-off-by: Juan Antonio Osorio <[email protected]>
1 parent 285528d commit fc1aedf

21 files changed

+2028
-9
lines changed

cmd/thv-operator/api/v1alpha1/mcpserver_types.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,17 @@ type MCPServerSpec struct {
8888
Audit *AuditConfig `json:"audit,omitempty"`
8989

9090
// ToolsFilter is the filter on tools applied to the MCP server
91+
// Deprecated: Use ToolConfigRef instead
9192
// +optional
9293
ToolsFilter []string `json:"tools,omitempty"`
9394

95+
// ToolConfigRef references a MCPToolConfig resource for tool filtering and renaming.
96+
// The referenced MCPToolConfig must exist in the same namespace as this MCPServer.
97+
// Cross-namespace references are not supported for security and isolation reasons.
98+
// If specified, this takes precedence over the inline ToolsFilter field.
99+
// +optional
100+
ToolConfigRef *ToolConfigRef `json:"toolConfigRef,omitempty"`
101+
94102
// Telemetry defines observability configuration for the MCP server
95103
// +optional
96104
Telemetry *TelemetryConfig `json:"telemetry,omitempty"`
@@ -440,6 +448,14 @@ type ConfigMapAuthzRef struct {
440448
Key string `json:"key,omitempty"`
441449
}
442450

451+
// ToolConfigRef defines a reference to a MCPToolConfig resource.
452+
// The referenced MCPToolConfig must be in the same namespace as the MCPServer.
453+
type ToolConfigRef struct {
454+
// Name is the name of the MCPToolConfig resource in the same namespace
455+
// +kubebuilder:validation:Required
456+
Name string `json:"name"`
457+
}
458+
443459
// InlineAuthzConfig contains direct authorization configuration
444460
type InlineAuthzConfig struct {
445461
// Policies is a list of Cedar policy strings
@@ -543,6 +559,10 @@ type MCPServerStatus struct {
543559
// +optional
544560
Conditions []metav1.Condition `json:"conditions,omitempty"`
545561

562+
// ToolConfigHash stores the hash of the referenced ToolConfig for change detection
563+
// +optional
564+
ToolConfigHash string `json:"toolConfigHash,omitempty"`
565+
546566
// URL is the URL where the MCP server can be accessed
547567
// +optional
548568
URL string `json:"url,omitempty"`
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package v1alpha1
2+
3+
import (
4+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
5+
)
6+
7+
// MCPToolConfigSpec defines the desired state of MCPToolConfig.
8+
// MCPToolConfig resources are namespace-scoped and can only be referenced by
9+
// MCPServer resources in the same namespace.
10+
type MCPToolConfigSpec struct {
11+
// ToolsFilter is a list of tool names to filter (allow list).
12+
// Only tools in this list will be exposed by the MCP server.
13+
// If empty, all tools are exposed.
14+
// +optional
15+
ToolsFilter []string `json:"toolsFilter,omitempty"`
16+
17+
// ToolsOverride is a map from actual tool names to their overridden configuration.
18+
// This allows renaming tools and/or changing their descriptions.
19+
// +optional
20+
ToolsOverride map[string]ToolOverride `json:"toolsOverride,omitempty"`
21+
}
22+
23+
// ToolOverride represents a tool override configuration.
24+
// Both Name and Description can be overridden independently, but
25+
// they can't be both empty.
26+
type ToolOverride struct {
27+
// Name is the redefined name of the tool
28+
// +optional
29+
Name string `json:"name,omitempty"`
30+
31+
// Description is the redefined description of the tool
32+
// +optional
33+
Description string `json:"description,omitempty"`
34+
}
35+
36+
// MCPToolConfigStatus defines the observed state of MCPToolConfig
37+
type MCPToolConfigStatus struct {
38+
// ObservedGeneration is the most recent generation observed for this MCPToolConfig.
39+
// It corresponds to the MCPToolConfig's generation, which is updated on mutation by the API Server.
40+
// +optional
41+
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
42+
43+
// ConfigHash is a hash of the current configuration for change detection
44+
// +optional
45+
ConfigHash string `json:"configHash,omitempty"`
46+
47+
// ReferencingServers is a list of MCPServer resources that reference this MCPToolConfig
48+
// This helps track which servers need to be reconciled when this config changes
49+
// +optional
50+
ReferencingServers []string `json:"referencingServers,omitempty"`
51+
}
52+
53+
// +kubebuilder:object:root=true
54+
// +kubebuilder:subresource:status
55+
// +kubebuilder:resource:shortName=tc;toolconfig
56+
// +kubebuilder:printcolumn:name="Filter Count",type=integer,JSONPath=`.spec.toolsFilter[*]`
57+
// +kubebuilder:printcolumn:name="Override Count",type=integer,JSONPath=`.spec.toolsOverride`
58+
// +kubebuilder:printcolumn:name="Referenced By",type=string,JSONPath=`.status.referencingServers`
59+
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
60+
61+
// MCPToolConfig is the Schema for the mcptoolconfigs API.
62+
// MCPToolConfig resources are namespace-scoped and can only be referenced by
63+
// MCPServer resources within the same namespace. Cross-namespace references
64+
// are not supported for security and isolation reasons.
65+
type MCPToolConfig struct {
66+
metav1.TypeMeta `json:",inline"` // nolint:revive
67+
metav1.ObjectMeta `json:"metadata,omitempty"`
68+
69+
Spec MCPToolConfigSpec `json:"spec,omitempty"`
70+
Status MCPToolConfigStatus `json:"status,omitempty"`
71+
}
72+
73+
// +kubebuilder:object:root=true
74+
75+
// MCPToolConfigList contains a list of MCPToolConfig
76+
type MCPToolConfigList struct {
77+
metav1.TypeMeta `json:",inline"` // nolint:revive
78+
metav1.ListMeta `json:"metadata,omitempty"`
79+
Items []MCPToolConfig `json:"items"`
80+
}
81+
82+
func init() {
83+
SchemeBuilder.Register(&MCPToolConfig{}, &MCPToolConfigList{})
84+
}

cmd/thv-operator/api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 141 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/thv-operator/controllers/mcpserver_controller.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ func (r *MCPServerReconciler) detectPlatform(ctx context.Context) (kubernetes.Pl
127127
// +kubebuilder:rbac:groups=toolhive.stacklok.dev,resources=mcpservers,verbs=get;list;watch;create;update;patch;delete
128128
// +kubebuilder:rbac:groups=toolhive.stacklok.dev,resources=mcpservers/status,verbs=get;update;patch
129129
// +kubebuilder:rbac:groups=toolhive.stacklok.dev,resources=mcpservers/finalizers,verbs=update
130+
// +kubebuilder:rbac:groups=toolhive.stacklok.dev,resources=mcptoolconfigs,verbs=get;list;watch
130131
// +kubebuilder:rbac:groups="",resources=configmaps,verbs=create;delete;get;list;patch;update;watch
131132
// +kubebuilder:rbac:groups="",resources=services,verbs=create;delete;get;list;patch;update;watch;apply
132133
// +kubebuilder:rbac:groups="rbac.authorization.k8s.io",resources=roles,verbs=create;delete;get;list;patch;update;watch
@@ -162,6 +163,17 @@ func (r *MCPServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
162163
return ctrl.Result{}, err
163164
}
164165

166+
// Check if MCPToolConfig is referenced and handle it
167+
if err := r.handleToolConfig(ctx, mcpServer); err != nil {
168+
ctxLogger.Error(err, "Failed to handle MCPToolConfig")
169+
// Update status to reflect the error
170+
mcpServer.Status.Phase = mcpv1alpha1.MCPServerPhaseFailed
171+
if statusErr := r.Status().Update(ctx, mcpServer); statusErr != nil {
172+
ctxLogger.Error(statusErr, "Failed to update MCPServer status after MCPToolConfig error")
173+
}
174+
return ctrl.Result{}, err
175+
}
176+
165177
// Check if the MCPServer instance is marked to be deleted
166178
if mcpServer.GetDeletionTimestamp() != nil {
167179
// The object is being deleted
@@ -393,6 +405,50 @@ func (r *MCPServerReconciler) updateRBACResourceIfNeeded(
393405
}
394406

395407
// ensureRBACResources ensures that the RBAC resources are in place for the MCP server
408+
409+
// handleToolConfig handles MCPToolConfig reference for an MCPServer
410+
func (r *MCPServerReconciler) handleToolConfig(ctx context.Context, m *mcpv1alpha1.MCPServer) error {
411+
if m.Spec.ToolConfigRef == nil {
412+
// No MCPToolConfig referenced, clear any stored hash
413+
if m.Status.ToolConfigHash != "" {
414+
m.Status.ToolConfigHash = ""
415+
if err := r.Status().Update(ctx, m); err != nil {
416+
return fmt.Errorf("failed to clear MCPToolConfig hash from status: %w", err)
417+
}
418+
}
419+
return nil
420+
}
421+
422+
// Get the referenced MCPToolConfig
423+
toolConfig, err := GetToolConfigForMCPServer(ctx, r.Client, m)
424+
if err != nil {
425+
return err
426+
}
427+
428+
if toolConfig == nil {
429+
return fmt.Errorf("MCPToolConfig %s not found", m.Spec.ToolConfigRef.Name)
430+
}
431+
432+
// Check if the MCPToolConfig hash has changed
433+
if m.Status.ToolConfigHash != toolConfig.Status.ConfigHash {
434+
ctxLogger.Info("MCPToolConfig has changed, updating MCPServer",
435+
"mcpserver", m.Name,
436+
"toolconfig", toolConfig.Name,
437+
"oldHash", m.Status.ToolConfigHash,
438+
"newHash", toolConfig.Status.ConfigHash)
439+
440+
// Update the stored hash
441+
m.Status.ToolConfigHash = toolConfig.Status.ConfigHash
442+
if err := r.Status().Update(ctx, m); err != nil {
443+
return fmt.Errorf("failed to update MCPToolConfig hash in status: %w", err)
444+
}
445+
446+
// The change in hash will trigger a reconciliation of the RunConfig
447+
// which will pick up the new tool configuration
448+
}
449+
450+
return nil
451+
}
396452
func (r *MCPServerReconciler) ensureRBACResources(ctx context.Context, mcpServer *mcpv1alpha1.MCPServer) error {
397453
proxyRunnerNameForRBAC := proxyRunnerServiceAccountName(mcpServer.Name)
398454

0 commit comments

Comments
 (0)