Skip to content

Commit b88bc23

Browse files
JAORMXclaude
andcommitted
Use standard JSON Schema format for VMCP composite tool parameters
Fixes #2650 Changes composite tool parameter definitions to use standard JSON Schema format per the MCP specification, instead of a non-standard simplified format. **Breaking Change**: Composite tool parameter format has changed from: ```yaml parameters: param1: type: "string" default: "value" ``` To standard JSON Schema: ```yaml parameters: type: object properties: param1: type: string default: "value" required: ["param1"] ``` Changes: - Update CompositeToolConfig.Parameters to map[string]any (full JSON Schema) - Remove parameter transformation logic from yaml_loader and workflow_converter - Add JSON Schema validation in yaml_loader (checks type: object) - Update Kubernetes CRDs to use runtime.RawExtension for parameters - Update webhook validation to handle runtime.RawExtension - Regenerate CRD manifests and deepcopy code - Update all tests and documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Juan Antonio Osorio <ozz@stacklok.com>
1 parent 25b23f1 commit b88bc23

19 files changed

+260
-401
lines changed

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

Lines changed: 15 additions & 3 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

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

Lines changed: 18 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -103,59 +103,36 @@ func (r *VirtualMCPCompositeToolDefinition) Validate() error {
103103

104104
// validateParameters validates the parameter schema using JSON Schema validation
105105
func (r *VirtualMCPCompositeToolDefinition) validateParameters() error {
106-
if len(r.Spec.Parameters) == 0 {
106+
if r.Spec.Parameters == nil || len(r.Spec.Parameters.Raw) == 0 {
107107
return nil // No parameters to validate
108108
}
109109

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

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

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

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

157134
// Validate using JSON Schema validator
158-
if err := validateJSONSchema(schemaJSON); err != nil {
135+
if err := validateJSONSchema(r.Spec.Parameters.Raw); err != nil {
159136
return fmt.Errorf("spec.parameters: invalid JSON Schema: %v", err)
160137
}
161138

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

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -105,17 +105,22 @@ func TestVirtualMCPCompositeToolDefinitionValidate(t *testing.T) {
105105
Spec: VirtualMCPCompositeToolDefinitionSpec{
106106
Name: "deploy_app",
107107
Description: "Deploy application with parameters",
108-
Parameters: map[string]ParameterSpec{
109-
"environment": {
110-
Type: "string",
111-
Description: "Target environment",
112-
Required: true,
113-
},
114-
"replicas": {
115-
Type: "integer",
116-
Description: "Number of replicas",
117-
Default: "3",
118-
},
108+
Parameters: &runtime.RawExtension{
109+
Raw: []byte(`{
110+
"type": "object",
111+
"properties": {
112+
"environment": {
113+
"type": "string",
114+
"description": "Target environment"
115+
},
116+
"replicas": {
117+
"type": "integer",
118+
"description": "Number of replicas",
119+
"default": 3
120+
}
121+
},
122+
"required": ["environment"]
123+
}`),
119124
},
120125
Steps: []WorkflowStep{
121126
{
@@ -137,10 +142,15 @@ func TestVirtualMCPCompositeToolDefinitionValidate(t *testing.T) {
137142
Spec: VirtualMCPCompositeToolDefinitionSpec{
138143
Name: "deploy_app",
139144
Description: "Deploy application",
140-
Parameters: map[string]ParameterSpec{
141-
"environment": {
142-
Type: "invalid_type",
143-
},
145+
Parameters: &runtime.RawExtension{
146+
Raw: []byte(`{
147+
"type": "invalid_type_not_object",
148+
"properties": {
149+
"environment": {
150+
"type": "string"
151+
}
152+
}
153+
}`),
144154
},
145155
Steps: []WorkflowStep{
146156
{
@@ -151,7 +161,7 @@ func TestVirtualMCPCompositeToolDefinitionValidate(t *testing.T) {
151161
},
152162
},
153163
wantErr: true,
154-
errMsg: "spec.parameters: invalid JSON Schema",
164+
errMsg: "spec.parameters: 'type' must be 'object'",
155165
},
156166
{
157167
name: "missing step ID",

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

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -185,9 +185,22 @@ type CompositeToolSpec struct {
185185
// +kubebuilder:validation:Required
186186
Description string `json:"description"`
187187

188-
// Parameters defines the input parameters for the composite tool
188+
// Parameters defines the input parameter schema in JSON Schema format.
189+
// Should be a JSON Schema object with "type": "object" and "properties".
190+
// Per MCP specification, this should follow standard JSON Schema for tool inputSchema.
191+
// Example:
192+
// {
193+
// "type": "object",
194+
// "properties": {
195+
// "param1": {"type": "string", "default": "value"},
196+
// "param2": {"type": "integer"}
197+
// },
198+
// "required": ["param2"]
199+
// }
189200
// +optional
190-
Parameters map[string]ParameterSpec `json:"parameters,omitempty"`
201+
// +kubebuilder:pruning:PreserveUnknownFields
202+
// +kubebuilder:validation:Type=object
203+
Parameters *runtime.RawExtension `json:"parameters,omitempty"`
191204

192205
// Steps defines the workflow steps
193206
// +kubebuilder:validation:Required
@@ -200,26 +213,6 @@ type CompositeToolSpec struct {
200213
Timeout string `json:"timeout,omitempty"`
201214
}
202215

203-
// ParameterSpec defines a parameter for a composite tool
204-
type ParameterSpec struct {
205-
// Type is the parameter type (string, integer, boolean, etc.)
206-
// +kubebuilder:validation:Required
207-
Type string `json:"type"`
208-
209-
// Description describes the parameter
210-
// +optional
211-
Description string `json:"description,omitempty"`
212-
213-
// Default is the default value for the parameter
214-
// +optional
215-
Default string `json:"default,omitempty"`
216-
217-
// Required indicates if the parameter is required
218-
// +kubebuilder:default=false
219-
// +optional
220-
Required bool `json:"required,omitempty"`
221-
}
222-
223216
// WorkflowStep defines a step in a composite tool workflow
224217
type WorkflowStep struct {
225218
// ID is the unique identifier for this step

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

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

cmd/thv-operator/pkg/vmcpconfig/converter.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package vmcpconfig
33

44
import (
55
"context"
6+
"encoding/json"
67
"time"
78

89
mcpv1alpha1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1alpha1"
@@ -242,7 +243,6 @@ func (*Converter) convertCompositeTools(
242243
tool := &vmcpconfig.CompositeToolConfig{
243244
Name: crdTool.Name,
244245
Description: crdTool.Description,
245-
Parameters: make(map[string]vmcpconfig.ParameterSchema),
246246
Steps: make([]*vmcpconfig.WorkflowStepConfig, 0, len(crdTool.Steps)),
247247
}
248248

@@ -253,12 +253,13 @@ func (*Converter) convertCompositeTools(
253253
}
254254
}
255255

256-
// Convert parameters
257-
for paramName, paramSpec := range crdTool.Parameters {
258-
tool.Parameters[paramName] = vmcpconfig.ParameterSchema{
259-
Type: paramSpec.Type,
260-
Default: paramSpec.Default,
256+
// Convert parameters from runtime.RawExtension to map[string]any
257+
if crdTool.Parameters != nil && len(crdTool.Parameters.Raw) > 0 {
258+
var params map[string]any
259+
if err := json.Unmarshal(crdTool.Parameters.Raw, &params); err == nil {
260+
tool.Parameters = params
261261
}
262+
// If unmarshal fails, leave Parameters as nil
262263
}
263264

264265
// Convert steps

deploy/charts/operator-crds/Chart.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ apiVersion: v2
22
name: toolhive-operator-crds
33
description: A Helm chart for installing the ToolHive Operator CRDs into Kubernetes.
44
type: application
5-
version: 0.0.59
5+
version: 0.0.60
66
appVersion: "0.0.1"
Lines changed: 1 addition & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1 @@
1-
2-
# ToolHive Operator CRDs Helm Chart
3-
4-
![Version: 0.0.59](https://img.shields.io/badge/Version-0.0.59-informational?style=flat-square)
5-
![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square)
6-
7-
A Helm chart for installing the ToolHive Operator CRDs into Kubernetes.
8-
9-
---
10-
11-
ToolHive Operator CRDs
12-
13-
## TL;DR
14-
15-
```console
16-
helm upgrade -i toolhive-operator-crds oci://ghcr.io/stacklok/toolhive/toolhive-operator-crds
17-
```
18-
19-
## Prerequisites
20-
21-
- Kubernetes 1.25+
22-
- Helm 3.10+ minimum, 3.14+ recommended
23-
24-
## Usage
25-
26-
### Installing from the Chart
27-
28-
Install one of the available versions:
29-
30-
```shell
31-
helm upgrade -i <release_name> oci://ghcr.io/stacklok/toolhive/toolhive-operator-crds --version=<version>
32-
```
33-
34-
> **Tip**: List all releases using `helm list`
35-
36-
### Uninstalling the Chart
37-
38-
To uninstall/delete the `toolhive-operator-crds` deployment:
39-
40-
```console
41-
helm uninstall <release_name>
42-
```
43-
1+
\n

deploy/charts/operator-crds/crds/toolhive.stacklok.dev_virtualmcpcompositetooldefinitions.yaml

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -93,30 +93,21 @@ spec:
9393
pattern: ^[a-z0-9]([a-z0-9_-]*[a-z0-9])?$
9494
type: string
9595
parameters:
96-
additionalProperties:
97-
description: ParameterSpec defines a parameter for a composite tool
98-
properties:
99-
default:
100-
description: Default is the default value for the parameter
101-
type: string
102-
description:
103-
description: Description describes the parameter
104-
type: string
105-
required:
106-
default: false
107-
description: Required indicates if the parameter is required
108-
type: boolean
109-
type:
110-
description: Type is the parameter type (string, integer, boolean,
111-
etc.)
112-
type: string
113-
required:
114-
- type
115-
type: object
11696
description: |-
117-
Parameters defines the input parameter schema for the workflow
118-
Each key is a parameter name, each value is the parameter specification
97+
Parameters defines the input parameter schema for the workflow in JSON Schema format.
98+
Should be a JSON Schema object with "type": "object" and "properties".
99+
Per MCP specification, this should follow standard JSON Schema for tool inputSchema.
100+
Example:
101+
{
102+
"type": "object",
103+
"properties": {
104+
"param1": {"type": "string", "default": "value"},
105+
"param2": {"type": "integer"}
106+
},
107+
"required": ["param2"]
108+
}
119109
type: object
110+
x-kubernetes-preserve-unknown-fields: true
120111
steps:
121112
description: |-
122113
Steps defines the workflow step definitions

0 commit comments

Comments
 (0)