Skip to content

Commit 215e33a

Browse files
committed
add option to skip Virtual MCP CRDs and controllers installation
1 parent 778bb46 commit 215e33a

File tree

130 files changed

+9131
-1802
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

130 files changed

+9131
-1802
lines changed

.github/workflows/image-build-and-publish.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ jobs:
103103

104104
- name: Extract metadata
105105
id: meta
106-
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
106+
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
107107
with:
108108
images: ${{ env.BASE_REPO }}
109109
tags: |
@@ -182,7 +182,7 @@ jobs:
182182

183183
- name: Extract UBI metadata
184184
id: ubi-meta
185-
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
185+
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
186186
with:
187187
images: ${{ env.BASE_REPO }}
188188
tags: |
@@ -283,7 +283,7 @@ jobs:
283283

284284
- name: Extract UBI metadata
285285
id: ubi-meta
286-
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
286+
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
287287
with:
288288
images: ${{ env.BASE_REPO }}
289289
tags: |

.github/workflows/operator-ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ jobs:
9595
- name: Check for changes
9696
id: git-check
9797
run: |
98-
git diff --exit-code deploy/charts/operator-crds/crds || echo "crd-changes=true" >> $GITHUB_OUTPUT
98+
git diff --exit-code deploy/charts/operator-crds/crd-files || echo "crd-changes=true" >> $GITHUB_OUTPUT
9999
git diff --exit-code deploy/charts/operator/templates || echo "operator-changes=true" >> $GITHUB_OUTPUT
100100
101101
- name: Fail if CRDs are not up to date

.github/workflows/test-helm-charts.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,13 @@ jobs:
3434
- name: Create KIND Cluster
3535
uses: helm/kind-action@92086f6be054225fa813e0a4b13787fc9088faab # v1.13.0
3636

37+
- name: Cleanup stale cluster-scoped RBAC before CT install
38+
run: |
39+
echo "Deleting stale cluster-scoped RBAC to avoid Helm ownership conflicts..."
40+
kubectl delete clusterrole toolhive-operator-manager-role --ignore-not-found=true
41+
kubectl delete clusterrole toolhive-registry-api-role --ignore-not-found=true
42+
kubectl delete clusterrolebinding toolhive-operator-manager-rolebinding --ignore-not-found=true
43+
kubectl delete clusterrolebinding toolhive-registry-api-rolebinding --ignore-not-found=true
44+
3745
- name: Run chart-testing (install)
3846
run: ct install --config ct-install.yaml

.github/workflows/validate-proposal-naming.yml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,13 @@ jobs:
3737
3838
- name: Validate proposal naming
3939
run: |
40-
# Get the list of changed files in docs/proposals
41-
CHANGED_FILES=$(git diff --name-only --diff-filter A origin/${{ github.base_ref }}...HEAD | grep '^docs/proposals/')
40+
# Get the list of NEW files added in docs/proposals (not modifications to existing files)
41+
CHANGED_FILES=$(git diff --name-only --diff-filter=A origin/${{ github.base_ref }}...HEAD | grep '^docs/proposals/' || true)
4242
43-
# This is here to avoid errors when running this workflow manually without a PR
43+
# Skip validation if no new proposal files are being added
44+
# (modifications to existing proposals don't need naming validation)
4445
if [ -z "$CHANGED_FILES" ]; then
45-
echo "ℹ️ No proposal files changed in this PR"
46+
echo "ℹ️ No new proposal files added in this PR (only modifications to existing proposals or manual trigger)"
4647
exit 0
4748
fi
4849

README.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<p float="left">
22
<picture>
3-
<img src="docs/images/toolhive-icon-1024.png" alt="ToolHive Studio logo" height="100" align="middle" />
3+
<img src="docs/images/toolhive-icon-1024.png" alt="ToolHive logo" height="100" align="middle" />
44
</picture>
55
<picture>
66
<source media="(prefers-color-scheme: dark)" srcset="docs/images/toolhive-wordmark-white.png">
@@ -108,7 +108,7 @@ Deploy, run, and manage MCP servers locally or in a Kubernetes cluster with secu
108108

109109
Simplify MCP adoption for developers and knowledge workers across your enterprise
110110

111-
- Cross-platform [desktop app](https://github.com/stacklok/toolhive-studio) and browser-based cloud UI
111+
- Cross-platform [desktop app](https://github.com/stacklok/toolhive-studio) and browser-based [cloud UI](https://github.com/stacklok/toolhive-cloud-ui)
112112
- Make it easy for admins to curate MCP servers and tools
113113
- Automate server discovery
114114
- Install MCP servers with a single click
@@ -153,7 +153,7 @@ Teams and organizations manage MCP servers and registries centrally using famili
153153
- Enterprise-grade security and observability: OIDC/OAuth SSO, secure token exchange, audit logging, OpenTelemetry, and Prometheus metrics
154154
- Hybrid registry server: curate from upstream registries, dynamically register local MCP servers, or proxy trusted remote services
155155

156-
**Get started:** [Quickstart](https://docs.stacklok.com/toolhive/tutorials/quickstart-k8s), [How-to guides](https://docs.stacklok.com/toolhive/guides-k8s), [CRD reference](https://docs.stacklok.com/toolhive/reference/crd-spec), \[Example manifests\](./examples/operator/)
156+
**Get started:** [Quickstart](https://docs.stacklok.com/toolhive/tutorials/quickstart-k8s), [How-to guides](https://docs.stacklok.com/toolhive/guides-k8s), [CRD reference](https://docs.stacklok.com/toolhive/reference/crd-spec), [Example manifests](./examples/operator/)
157157

158158
### Hybrid
159159

@@ -163,16 +163,21 @@ End users access approved MCP servers through a secure, browser-based cloud UI.
163163

164164
Enterprise teams can also leverage ToolHive to integrate MCP servers into custom internal tools, agentic workflows, or chat-based interfaces, using the same runtime and access controls.
165165

166+
<picture>
167+
<source media="(prefers-color-scheme: dark)" srcset="docs/images/toolhive-platform-dark.svg">
168+
<img src="docs/images/toolhive-platform-light.svg" alt="ToolHive platform diagram" width="800" style="padding: 20px 0" />
169+
</picture>
170+
166171
---
167172

168173
## Contributing
169174

170-
We welcome contributions and feedback from the community\!
175+
We welcome contributions and feedback from the community!
171176

172177
- 🐛 [Report issues](https://github.com/stacklok/toolhive/issues)
173178
- 💬 [Join our Discord](https://discord.gg/stacklok)
174179

175-
If you have ideas, suggestions, or want to get involved, check out our contributing guide or open an issue. Join us in making ToolHive even better\!
180+
If you have ideas, suggestions, or want to get involved, check out our contributing guide or open an issue. Join us in making ToolHive even better!
176181

177182
Contribute to the CLI, API, and Kubernetes Operator (this repo):
178183

@@ -183,6 +188,7 @@ Contribute to the CLI, API, and Kubernetes Operator (this repo):
183188
Contribute to the UI, registry, and docs:
184189

185190
- 💻 [Desktop UI repository](https://github.com/stacklok/toolhive-studio)
191+
- ☁️ [Cloud UI repository](https://github.com/stacklok/toolhive-cloud-ui)
186192
- 📦 [ToolHive registry server repository](https://github.com/stacklok/toolhive-registry-server)
187193
- 🛠️ [ToolHive's built-in registry](https://github.com/stacklok/toolhive-registry)
188194
- 📚 [Documentation repository](https://github.com/stacklok/docs-website)

cmd/thv-operator/Taskfile.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ tasks:
184184
platforms: [windows]
185185
ignore_error: true # Windows has no mkdir -p, so just ignore error if it exists
186186
- go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.17.3
187-
- $(go env GOPATH)/bin/controller-gen crd webhook paths="./cmd/thv-operator/..." output:crd:artifacts:config=deploy/charts/operator-crds/crds
187+
- $(go env GOPATH)/bin/controller-gen crd webhook paths="./cmd/thv-operator/..." output:crd:artifacts:config=deploy/charts/operator-crds/crd-files
188188
- $(go env GOPATH)/bin/controller-gen rbac:roleName=toolhive-operator-manager-role paths="./cmd/thv-operator/..." output:rbac:artifacts:config=deploy/charts/operator/templates/clusterrole
189189

190190
operator-test:

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

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,33 @@ import (
77
// External auth configuration types
88
const (
99
// ExternalAuthTypeTokenExchange is the type for RFC-8693 token exchange
10-
ExternalAuthTypeTokenExchange = "tokenExchange"
10+
ExternalAuthTypeTokenExchange ExternalAuthType = "tokenExchange"
11+
12+
// ExternalAuthTypeHeaderInjection is the type for custom header injection
13+
ExternalAuthTypeHeaderInjection ExternalAuthType = "headerInjection"
1114
)
1215

16+
// ExternalAuthType represents the type of external authentication
17+
type ExternalAuthType string
18+
1319
// MCPExternalAuthConfigSpec defines the desired state of MCPExternalAuthConfig.
1420
// MCPExternalAuthConfig resources are namespace-scoped and can only be referenced by
1521
// MCPServer resources in the same namespace.
1622
type MCPExternalAuthConfigSpec struct {
1723
// Type is the type of external authentication to configure
18-
// +kubebuilder:validation:Enum=tokenExchange
24+
// +kubebuilder:validation:Enum=tokenExchange;headerInjection
1925
// +kubebuilder:validation:Required
20-
Type string `json:"type"`
26+
Type ExternalAuthType `json:"type"`
2127

2228
// TokenExchange configures RFC-8693 OAuth 2.0 Token Exchange
2329
// Only used when Type is "tokenExchange"
2430
// +optional
2531
TokenExchange *TokenExchangeConfig `json:"tokenExchange,omitempty"`
32+
33+
// HeaderInjection configures custom HTTP header injection
34+
// Only used when Type is "headerInjection"
35+
// +optional
36+
HeaderInjection *HeaderInjectionConfig `json:"headerInjection,omitempty"`
2637
}
2738

2839
// TokenExchangeConfig holds configuration for RFC-8693 OAuth 2.0 Token Exchange.
@@ -69,6 +80,20 @@ type TokenExchangeConfig struct {
6980
ExternalTokenHeaderName string `json:"externalTokenHeaderName,omitempty"`
7081
}
7182

83+
// HeaderInjectionConfig holds configuration for custom HTTP header injection authentication.
84+
// This allows injecting a secret-based header value into requests to backend MCP servers.
85+
// For security reasons, only secret references are supported (no plaintext values).
86+
type HeaderInjectionConfig struct {
87+
// HeaderName is the name of the HTTP header to inject
88+
// +kubebuilder:validation:Required
89+
// +kubebuilder:validation:MinLength=1
90+
HeaderName string `json:"headerName"`
91+
92+
// ValueSecretRef references a Kubernetes Secret containing the header value
93+
// +kubebuilder:validation:Required
94+
ValueSecretRef *SecretKeyRef `json:"valueSecretRef"`
95+
}
96+
7297
// SecretKeyRef is a reference to a key within a Secret
7398
type SecretKeyRef struct {
7499
// Name is the name of the secret

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

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -392,27 +392,6 @@ func (r *MCPRegistry) GetAPIResourceName() string {
392392
return fmt.Sprintf("%s-api", r.Name)
393393
}
394394

395-
// IsConfigMapRegistrySource returns true if any registry source is a configmap
396-
func (r *MCPRegistry) IsConfigMapRegistrySource() bool {
397-
for _, registry := range r.Spec.Registries {
398-
if registry.ConfigMapRef != nil {
399-
return true
400-
}
401-
}
402-
return false
403-
}
404-
405-
// GetConfigMapSourceName returns the name of the first configmap source
406-
// if present, otherwise returns an empty string
407-
func (r *MCPRegistry) GetConfigMapSourceName() string {
408-
for _, registry := range r.Spec.Registries {
409-
if registry.ConfigMapRef != nil {
410-
return registry.ConfigMapRef.Name
411-
}
412-
}
413-
return ""
414-
}
415-
416395
// DeriveOverallPhase determines the overall MCPRegistry phase based on sync and API status
417396
func (r *MCPRegistry) DeriveOverallPhase() MCPRegistryPhase {
418397
syncStatus := r.Status.SyncStatus

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

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,22 @@ type VirtualMCPCompositeToolDefinitionSpec struct {
1919
// +kubebuilder:validation:MinLength=1
2020
Description string `json:"description"`
2121

22-
// Parameters defines the input parameter schema for the workflow
23-
// Each key is a parameter name, each value is the parameter specification
22+
// Parameters defines the input parameter schema for the workflow in JSON Schema format.
23+
// Should be a JSON Schema object with "type": "object" and "properties".
24+
// Per MCP specification, this should follow standard JSON Schema for tool inputSchema.
25+
// Example:
26+
// {
27+
// "type": "object",
28+
// "properties": {
29+
// "param1": {"type": "string", "default": "value"},
30+
// "param2": {"type": "integer"}
31+
// },
32+
// "required": ["param2"]
33+
// }
2434
// +optional
25-
Parameters map[string]ParameterSpec `json:"parameters,omitempty"`
35+
// +kubebuilder:pruning:PreserveUnknownFields
36+
// +kubebuilder:validation:Type=object
37+
Parameters *runtime.RawExtension `json:"parameters,omitempty"`
2638

2739
// Steps defines the workflow step definitions
2840
// Steps are executed sequentially in Phase 1
@@ -41,8 +53,7 @@ type VirtualMCPCompositeToolDefinitionSpec struct {
4153
// FailureMode defines the failure handling strategy
4254
// - abort: Stop execution on first failure (default)
4355
// - continue: Continue executing remaining steps
44-
// - best_effort: Try all steps, report partial success
45-
// +kubebuilder:validation:Enum=abort;continue;best_effort
56+
// +kubebuilder:validation:Enum=abort;continue
4657
// +kubebuilder:default=abort
4758
// +optional
4859
FailureMode string `json:"failureMode,omitempty"`

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

Lines changed: 21 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,11 @@ func (r *VirtualMCPCompositeToolDefinition) Validate() error {
8484
// Validate failure mode
8585
if r.Spec.FailureMode != "" {
8686
validModes := map[string]bool{
87-
"abort": true,
88-
"continue": true,
89-
"best_effort": true,
87+
"abort": true,
88+
"continue": true,
9089
}
9190
if !validModes[r.Spec.FailureMode] {
92-
errors = append(errors, "spec.failureMode must be one of: abort, continue, best_effort")
91+
errors = append(errors, "spec.failureMode must be one of: abort, continue")
9392
}
9493
}
9594

@@ -102,59 +101,36 @@ func (r *VirtualMCPCompositeToolDefinition) Validate() error {
102101

103102
// validateParameters validates the parameter schema using JSON Schema validation
104103
func (r *VirtualMCPCompositeToolDefinition) validateParameters() error {
105-
if len(r.Spec.Parameters) == 0 {
104+
if r.Spec.Parameters == nil || len(r.Spec.Parameters.Raw) == 0 {
106105
return nil // No parameters to validate
107106
}
108107

109-
// Build a JSON Schema object from the parameters
110-
// Parameters map to a JSON Schema "properties" object
111-
properties := make(map[string]interface{})
112-
var required []string
113-
114-
for paramName, param := range r.Spec.Parameters {
115-
if param.Type == "" {
116-
return fmt.Errorf("spec.parameters[%s].type is required", paramName)
117-
}
118-
119-
// Build a JSON Schema property definition
120-
property := map[string]interface{}{
121-
"type": param.Type,
122-
}
123-
124-
if param.Description != "" {
125-
property["description"] = param.Description
126-
}
127-
128-
if param.Default != "" {
129-
// Parse default value based on type
130-
property["default"] = param.Default
131-
}
132-
133-
if param.Required {
134-
required = append(required, paramName)
135-
}
136-
137-
properties[paramName] = property
108+
// Parameters should be a JSON Schema object in RawExtension format
109+
// Unmarshal to validate structure
110+
var params map[string]interface{}
111+
if err := json.Unmarshal(r.Spec.Parameters.Raw, &params); err != nil {
112+
return fmt.Errorf("spec.parameters: invalid JSON: %v", err)
138113
}
139114

140-
// Construct a full JSON Schema document
141-
schemaDoc := map[string]interface{}{
142-
"type": "object",
143-
"properties": properties,
115+
// Validate that it has "type" field
116+
typeVal, hasType := params["type"]
117+
if !hasType {
118+
return fmt.Errorf("spec.parameters: must have 'type' field (should be 'object' for JSON Schema)")
144119
}
145120

146-
if len(required) > 0 {
147-
schemaDoc["required"] = required
121+
// Type must be a string
122+
typeStr, ok := typeVal.(string)
123+
if !ok {
124+
return fmt.Errorf("spec.parameters: 'type' field must be a string")
148125
}
149126

150-
// Marshal to JSON
151-
schemaJSON, err := json.Marshal(schemaDoc)
152-
if err != nil {
153-
return fmt.Errorf("spec.parameters: failed to marshal schema: %v", err)
127+
// Type should be "object" for parameter schemas
128+
if typeStr != "object" {
129+
return fmt.Errorf("spec.parameters: 'type' must be 'object' (got '%s')", typeStr)
154130
}
155131

156132
// Validate using JSON Schema validator
157-
if err := validateJSONSchema(schemaJSON); err != nil {
133+
if err := validateJSONSchema(r.Spec.Parameters.Raw); err != nil {
158134
return fmt.Errorf("spec.parameters: invalid JSON Schema: %v", err)
159135
}
160136

0 commit comments

Comments
 (0)