Skip to content

Commit 8b8412b

Browse files
JAORMXclaudeChrisJBurns
authored
Add MCPServerEntry CRD types and MCPGroup status fields (#4662)
VirtualMCPServer currently requires MCPRemoteProxy (which spawns proxy pods) to reach remote MCP servers. This forces OIDC auth on public remotes, creates dual auth boundary confusion, and wastes resources. RFC-55 introduces MCPServerEntry as a zero-infrastructure catalog entry — pure configuration telling vMCP where a remote server exists and how to authenticate. Define MCPServerEntrySpec with remoteURL, transport, groupRef, externalAuthConfigRef, headerForward, and caBundleRef fields. Define MCPServerEntryStatus with Valid/Pending/Failed phase model and GroupRefValidated, ExternalAuthConfigValidated, CABundleRefValidated conditions. Add Entries and EntryCount fields to MCPGroupStatus for tracking MCPServerEntry membership. Register MCPServerEntry in the CRD Helm wrapper feature flags map under both server and virtualMcp flags. Refs #4656 Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Chris Burns <29541485+ChrisJBurns@users.noreply.github.com>
1 parent 22a4203 commit 8b8412b

9 files changed

Lines changed: 996 additions & 0 deletions

File tree

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@ type MCPGroupStatus struct {
4343
// +optional
4444
RemoteProxyCount int32 `json:"remoteProxyCount,omitempty"`
4545

46+
// Entries lists MCPServerEntry names in this group
47+
// +listType=set
48+
// +optional
49+
Entries []string `json:"entries,omitempty"`
50+
51+
// EntryCount is the number of MCPServerEntries
52+
// +optional
53+
EntryCount int32 `json:"entryCount,omitempty"`
54+
4655
// Conditions represent observations
4756
// +listType=map
4857
// +listMapKey=type
@@ -80,6 +89,7 @@ const (
8089
//+kubebuilder:subresource:status
8190
//+kubebuilder:resource:shortName=mcpg;mcpgroup,categories=toolhive
8291
//+kubebuilder:printcolumn:name="Servers",type="integer",JSONPath=".status.serverCount"
92+
//+kubebuilder:printcolumn:name="Entries",type="integer",JSONPath=".status.entryCount"
8393
//+kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase"
8494
//+kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type=='MCPServersChecked')].status"
8595
//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package v1alpha1
5+
6+
import (
7+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8+
)
9+
10+
// MCPServerEntrySpec defines the desired state of MCPServerEntry.
11+
// MCPServerEntry is a zero-infrastructure catalog entry that declares a remote MCP
12+
// server endpoint. Unlike MCPRemoteProxy, it creates no pods, services, or deployments.
13+
type MCPServerEntrySpec struct {
14+
// RemoteURL is the URL of the remote MCP server.
15+
// HTTPS is enforced by default; set the annotation
16+
// toolhive.stacklok.dev/allow-insecure: "true" to allow HTTP for development.
17+
// +kubebuilder:validation:Required
18+
// +kubebuilder:validation:Pattern=`^https?://`
19+
RemoteURL string `json:"remoteURL"`
20+
21+
// Transport is the transport method for the remote server (sse or streamable-http).
22+
// No default is set (unlike MCPRemoteProxy) because MCPServerEntry points at external
23+
// servers the user doesn't control — requiring explicit transport avoids silent mismatches.
24+
// +kubebuilder:validation:Required
25+
// +kubebuilder:validation:Enum=sse;streamable-http
26+
Transport string `json:"transport"`
27+
28+
// GroupRef is the name of the MCPGroup this entry belongs to.
29+
// Required — every MCPServerEntry must be part of a group for vMCP discovery.
30+
// +kubebuilder:validation:Required
31+
// +kubebuilder:validation:MinLength=1
32+
GroupRef string `json:"groupRef"`
33+
34+
// ExternalAuthConfigRef references a MCPExternalAuthConfig resource for token exchange
35+
// when connecting to the remote MCP server. The referenced MCPExternalAuthConfig must
36+
// exist in the same namespace as this MCPServerEntry.
37+
// +optional
38+
ExternalAuthConfigRef *ExternalAuthConfigRef `json:"externalAuthConfigRef,omitempty"`
39+
40+
// HeaderForward configures headers to inject into requests to the remote MCP server.
41+
// Use this to add custom headers like API keys or correlation IDs.
42+
// +optional
43+
HeaderForward *HeaderForwardConfig `json:"headerForward,omitempty"`
44+
45+
// CABundleRef references a ConfigMap containing CA certificates for TLS verification
46+
// when connecting to the remote MCP server.
47+
// +optional
48+
CABundleRef *CABundleSource `json:"caBundleRef,omitempty"`
49+
}
50+
51+
// MCPServerEntryStatus defines the observed state of MCPServerEntry.
52+
type MCPServerEntryStatus struct {
53+
// ObservedGeneration reflects the generation most recently observed by the controller.
54+
// +optional
55+
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
56+
57+
// Phase indicates the current lifecycle phase of the MCPServerEntry.
58+
// +optional
59+
// +kubebuilder:default=Pending
60+
Phase MCPServerEntryPhase `json:"phase,omitempty"`
61+
62+
// Conditions represent the latest available observations of the MCPServerEntry's state.
63+
// +listType=map
64+
// +listMapKey=type
65+
// +optional
66+
Conditions []metav1.Condition `json:"conditions,omitempty"`
67+
}
68+
69+
// MCPServerEntryPhase represents the lifecycle phase of an MCPServerEntry.
70+
// +kubebuilder:validation:Enum=Valid;Pending;Failed
71+
type MCPServerEntryPhase string
72+
73+
const (
74+
// MCPServerEntryPhaseValid indicates all validations passed and the entry is usable.
75+
MCPServerEntryPhaseValid MCPServerEntryPhase = "Valid"
76+
77+
// MCPServerEntryPhasePending indicates the entry is waiting for referenced resources.
78+
MCPServerEntryPhasePending MCPServerEntryPhase = "Pending"
79+
80+
// MCPServerEntryPhaseFailed indicates a validation error occurred.
81+
MCPServerEntryPhaseFailed MCPServerEntryPhase = "Failed"
82+
)
83+
84+
// Condition types for MCPServerEntry.
85+
// Reuses shared condition type constants from mcpserver_types.go where the string
86+
// values match (GroupRefValidated, ExternalAuthConfigValidated, CABundleRefValidated).
87+
const (
88+
// ConditionTypeMCPServerEntryValid indicates overall validation status of the MCPServerEntry.
89+
// Uses the shared "Valid" condition type since this is a configuration resource, not a workload.
90+
ConditionTypeMCPServerEntryValid = ConditionTypeValid
91+
92+
// ConditionTypeMCPServerEntryGroupRefValidated indicates whether the referenced MCPGroup exists.
93+
ConditionTypeMCPServerEntryGroupRefValidated = ConditionGroupRefValidated
94+
95+
// ConditionTypeMCPServerEntryAuthConfigValidated indicates whether the referenced
96+
// MCPExternalAuthConfig exists (when configured).
97+
ConditionTypeMCPServerEntryAuthConfigValidated = ConditionTypeExternalAuthConfigValidated
98+
99+
// ConditionTypeMCPServerEntryCABundleRefValidated indicates whether the referenced
100+
// CA bundle ConfigMap exists (when configured).
101+
ConditionTypeMCPServerEntryCABundleRefValidated = ConditionCABundleRefValidated
102+
)
103+
104+
// Condition reasons for MCPServerEntry.
105+
// GroupRef reasons reuse shared constants from mcpserver_types.go.
106+
// CABundle reasons reuse shared constants from mcpserver_types.go.
107+
const (
108+
// ConditionReasonMCPServerEntryValid indicates the entry passed all validations.
109+
ConditionReasonMCPServerEntryValid = "ConfigValid"
110+
111+
// ConditionReasonMCPServerEntryInvalid indicates one or more validations failed.
112+
ConditionReasonMCPServerEntryInvalid = "ConfigInvalid"
113+
114+
// ConditionReasonMCPServerEntryGroupRefValidated reuses the shared GroupRef reason.
115+
ConditionReasonMCPServerEntryGroupRefValidated = ConditionReasonGroupRefValidated
116+
117+
// ConditionReasonMCPServerEntryGroupRefNotFound reuses the shared GroupRef reason.
118+
ConditionReasonMCPServerEntryGroupRefNotFound = ConditionReasonGroupRefNotFound
119+
120+
// ConditionReasonMCPServerEntryGroupRefNotReady reuses the shared GroupRef reason.
121+
ConditionReasonMCPServerEntryGroupRefNotReady = ConditionReasonGroupRefNotReady
122+
123+
// ConditionReasonMCPServerEntryAuthConfigValid indicates the referenced auth config exists.
124+
ConditionReasonMCPServerEntryAuthConfigValid = "AuthConfigValid"
125+
126+
// ConditionReasonMCPServerEntryAuthConfigNotFound indicates the referenced auth config was not found.
127+
ConditionReasonMCPServerEntryAuthConfigNotFound = "AuthConfigNotFound"
128+
129+
// ConditionReasonMCPServerEntryAuthConfigNotConfigured indicates no auth config ref is set.
130+
ConditionReasonMCPServerEntryAuthConfigNotConfigured = "AuthConfigNotConfigured"
131+
132+
// ConditionReasonMCPServerEntryCABundleRefValid reuses the shared CABundle reason.
133+
ConditionReasonMCPServerEntryCABundleRefValid = ConditionReasonCABundleRefValid
134+
135+
// ConditionReasonMCPServerEntryCABundleRefNotFound reuses the shared CABundle reason.
136+
ConditionReasonMCPServerEntryCABundleRefNotFound = ConditionReasonCABundleRefNotFound
137+
138+
// ConditionReasonMCPServerEntryCABundleRefNotConfigured indicates no CA bundle ref is set.
139+
ConditionReasonMCPServerEntryCABundleRefNotConfigured = "CABundleRefNotConfigured"
140+
)
141+
142+
// AllowInsecureAnnotation is the annotation key to allow HTTP URLs for development.
143+
const AllowInsecureAnnotation = "toolhive.stacklok.dev/allow-insecure"
144+
145+
//+kubebuilder:object:root=true
146+
//+kubebuilder:subresource:status
147+
//+kubebuilder:resource:shortName=mcpentry,categories=toolhive
148+
//+kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase"
149+
//+kubebuilder:printcolumn:name="Transport",type="string",JSONPath=".spec.transport"
150+
//+kubebuilder:printcolumn:name="Remote URL",type="string",JSONPath=".spec.remoteURL"
151+
//+kubebuilder:printcolumn:name="Group",type="string",JSONPath=".spec.groupRef"
152+
//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
153+
154+
// MCPServerEntry is the Schema for the mcpserverentries API.
155+
// It declares a remote MCP server endpoint for vMCP discovery and routing
156+
// without deploying any infrastructure.
157+
type MCPServerEntry struct {
158+
metav1.TypeMeta `json:",inline"` // nolint:revive
159+
metav1.ObjectMeta `json:"metadata,omitempty"`
160+
161+
Spec MCPServerEntrySpec `json:"spec,omitempty"`
162+
Status MCPServerEntryStatus `json:"status,omitempty"`
163+
}
164+
165+
//+kubebuilder:object:root=true
166+
167+
// MCPServerEntryList contains a list of MCPServerEntry.
168+
type MCPServerEntryList struct {
169+
metav1.TypeMeta `json:",inline"` // nolint:revive
170+
metav1.ListMeta `json:"metadata,omitempty"`
171+
Items []MCPServerEntry `json:"items"`
172+
}
173+
174+
func init() {
175+
SchemeBuilder.Register(&MCPServerEntry{}, &MCPServerEntryList{})
176+
}

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

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

deploy/charts/operator-crds/crd-helm-wrapper/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ var crdFeatureFlags = map[string][]string{
4646
"mcpoidcconfigs": {"server"},
4747
"mcptelemetryconfigs": {"server"},
4848
"mcpexternalauthconfigs": {"server", "virtualMcp"},
49+
"mcpserverentries": {"server", "virtualMcp"},
4950
}
5051

5152
func main() {

deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_mcpgroups.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ spec:
2323
- jsonPath: .status.serverCount
2424
name: Servers
2525
type: integer
26+
- jsonPath: .status.entryCount
27+
name: Entries
28+
type: integer
2629
- jsonPath: .status.phase
2730
name: Phase
2831
type: string
@@ -124,6 +127,16 @@ spec:
124127
x-kubernetes-list-map-keys:
125128
- type
126129
x-kubernetes-list-type: map
130+
entries:
131+
description: Entries lists MCPServerEntry names in this group
132+
items:
133+
type: string
134+
type: array
135+
x-kubernetes-list-type: set
136+
entryCount:
137+
description: EntryCount is the number of MCPServerEntries
138+
format: int32
139+
type: integer
127140
observedGeneration:
128141
description: ObservedGeneration reflects the generation most recently
129142
observed by the controller

0 commit comments

Comments
 (0)