Skip to content

Commit f6d1708

Browse files
committed
fix: resolve linting issues in kagent integration
- Define constants for repeated string literals (goconst) - Fix detectKagentAPIVersion function signature to return single value - Update function calls to match new signature - Add nolint comments for test parallelization with env vars This resolves all golangci-lint issues and ensures CI passes.
1 parent 7f7b295 commit f6d1708

File tree

3 files changed

+85
-43
lines changed

3 files changed

+85
-43
lines changed

cmd/thv-operator/README.md

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,13 @@ kubectl describe mcpserver <name>
177177

178178
### Kagent Integration
179179

180-
The ToolHive operator supports optional integration with [kagent](https://kagent.dev), allowing kagent agents to discover and use MCP servers managed by ToolHive. When enabled, the operator automatically creates kagent ToolServer resources that reference the ToolHive-managed MCP servers.
180+
The ToolHive operator supports optional integration with [kagent](https://kagent.dev), allowing kagent agents to discover and use MCP servers managed by ToolHive. When enabled, the operator automatically creates kagent resources that reference the ToolHive-managed MCP servers.
181+
182+
The integration supports both:
183+
- **kagent v1alpha1**: Creates `ToolServer` resources
184+
- **kagent v1alpha2**: Creates `RemoteMCPServer` resources (when available)
185+
186+
The operator automatically detects which kagent API version is available in your cluster and creates the appropriate resources.
181187

182188
#### Enabling Kagent Integration
183189

@@ -196,25 +202,43 @@ kagentIntegration:
196202
enabled: true
197203
```
198204
205+
#### Configuration Options
206+
207+
You can control the kagent API version preference via environment variable:
208+
209+
```yaml
210+
# In your values file
211+
kagentIntegration:
212+
enabled: true
213+
apiVersion: v1alpha2 # Optional: prefer v1alpha2 when available (defaults to v1alpha1)
214+
```
215+
216+
This sets the `KAGENT_API_VERSION` environment variable in the operator deployment.
217+
199218
#### How It Works
200219

201220
When kagent integration is enabled:
202221

203-
1. For each ToolHive MCPServer resource created, the operator automatically creates a corresponding kagent ToolServer resource
204-
2. The ToolServer resource references the ToolHive-managed MCP server service URL
205-
3. The ToolServer is owned by the MCPServer, ensuring it's deleted when the MCPServer is removed
206-
4. Kagent agents can then discover and use these ToolServers to access the MCP servers
222+
1. The operator detects which kagent API version is available in your cluster
223+
2. For each ToolHive MCPServer resource created, the operator automatically creates:
224+
- A kagent `ToolServer` resource (v1alpha1), OR
225+
- A kagent `RemoteMCPServer` resource (v1alpha2)
226+
3. The kagent resource references the ToolHive-managed MCP server service URL
227+
4. The resource is owned by the MCPServer, ensuring it's deleted when the MCPServer is removed
228+
5. Kagent agents can then discover and use these resources to access the MCP servers
207229

208-
The kagent ToolServer resources are created with:
230+
The kagent resources are created with:
209231
- Name: `toolhive-<mcpserver-name>`
210232
- Namespace: Same as the MCPServer
211-
- Transport configuration: Mapped from ToolHive transport types (sse → sse, streamable-http → streamableHttp, stdio → sse)
233+
- Transport configuration:
234+
- v1alpha1: Mapped to config types (sse → sse, streamable-http → streamableHttp, stdio → sse)
235+
- v1alpha2: Mapped to protocols (sse → SSE, streamable-http → STREAMABLE_HTTP, stdio → SSE)
212236
- Service URL: Points to the ToolHive proxy service
213237

214238
#### Requirements
215239

216-
- Kagent must be installed in your cluster
217-
- The operator needs permissions to manage kagent ToolServer resources (automatically configured when integration is enabled)
240+
- Kagent must be installed in your cluster (either v1alpha1 or v1alpha2)
241+
- The operator needs permissions to manage kagent resources (automatically configured when integration is enabled)
218242

219243
#### Example
220244

@@ -232,8 +256,9 @@ spec:
232256
port: 8080
233257
```
234258

235-
With kagent integration enabled, the operator automatically creates:
259+
With kagent integration enabled, the operator automatically creates one of the following:
236260

261+
**For kagent v1alpha1:**
237262
```yaml
238263
apiVersion: kagent.dev/v1alpha1
239264
kind: ToolServer
@@ -251,7 +276,23 @@ spec:
251276
url: http://mcp-github-proxy.toolhive-system.svc.cluster.local:8080
252277
```
253278

254-
Kagent agents can then reference this ToolServer to use the GitHub MCP server in their workflows.
279+
**For kagent v1alpha2:**
280+
```yaml
281+
apiVersion: kagent.dev/v1alpha2
282+
kind: RemoteMCPServer
283+
metadata:
284+
name: toolhive-github
285+
namespace: toolhive-system
286+
labels:
287+
toolhive.stacklok.dev/managed-by: toolhive-operator
288+
toolhive.stacklok.dev/mcpserver: github
289+
spec:
290+
description: "ToolHive MCP Server: github"
291+
url: http://mcp-github-proxy.toolhive-system.svc.cluster.local:8080
292+
protocol: SSE
293+
```
294+
295+
Kagent agents can then reference these resources to use the GitHub MCP server in their workflows.
255296

256297
### Permission Profiles
257298

cmd/thv-operator/controllers/mcpserver_kagent.go

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,22 @@ import (
1717
mcpv1alpha1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1alpha1"
1818
)
1919

20+
const (
21+
kagentAPIVersionV1Alpha1 = "v1alpha1"
22+
kagentAPIVersionV1Alpha2 = "v1alpha2"
23+
)
24+
2025
// kagentToolServerGVK defines the GroupVersionKind for kagent v1alpha1 ToolServer
2126
var kagentToolServerGVK = schema.GroupVersionKind{
2227
Group: "kagent.dev",
23-
Version: "v1alpha1",
28+
Version: kagentAPIVersionV1Alpha1,
2429
Kind: "ToolServer",
2530
}
2631

2732
// kagentRemoteMCPServerGVK defines the GroupVersionKind for kagent v1alpha2 RemoteMCPServer
2833
var kagentRemoteMCPServerGVK = schema.GroupVersionKind{
2934
Group: "kagent.dev",
30-
Version: "v1alpha2",
35+
Version: kagentAPIVersionV1Alpha2,
3136
Kind: "RemoteMCPServer",
3237
}
3338

@@ -36,11 +41,11 @@ const (
3641
// v1alpha1 config types
3742
kagentConfigTypeSSE = "sse"
3843
kagentConfigTypeStreamableHTTP = "streamableHttp"
39-
44+
4045
// v1alpha2 protocol types
4146
kagentProtocolSSE = "SSE"
4247
kagentProtocolStreamableHTTP = "STREAMABLE_HTTP"
43-
48+
4449
// Environment variable for kagent API version preference
4550
kagentAPIVersionEnv = "KAGENT_API_VERSION"
4651
)
@@ -71,41 +76,41 @@ func getPreferredKagentAPIVersion() string {
7176
}
7277

7378
// detectKagentAPIVersion detects which kagent API version is available in the cluster
74-
func (r *MCPServerReconciler) detectKagentAPIVersion(ctx context.Context) (string, error) {
79+
func (r *MCPServerReconciler) detectKagentAPIVersion(ctx context.Context) string {
7580
// First check if user has a preference
7681
preferred := getPreferredKagentAPIVersion()
77-
82+
7883
// Try to list resources of the preferred version to see if it's available
79-
if preferred == "v1alpha2" {
84+
if preferred == kagentAPIVersionV1Alpha2 {
8085
// Try v1alpha2 RemoteMCPServer
8186
list := &unstructured.UnstructuredList{}
8287
list.SetGroupVersionKind(schema.GroupVersionKind{
8388
Group: "kagent.dev",
84-
Version: "v1alpha2",
89+
Version: kagentAPIVersionV1Alpha2,
8590
Kind: "RemoteMCPServerList",
8691
})
87-
92+
8893
// We just want to check if the API exists, limit to 1 item
8994
if err := r.List(ctx, list, &client.ListOptions{Limit: 1}); err == nil {
90-
return "v1alpha2", nil
95+
return kagentAPIVersionV1Alpha2
9196
}
9297
}
93-
98+
9499
// Try v1alpha1 ToolServer
95100
list := &unstructured.UnstructuredList{}
96101
list.SetGroupVersionKind(schema.GroupVersionKind{
97102
Group: "kagent.dev",
98-
Version: "v1alpha1",
103+
Version: kagentAPIVersionV1Alpha1,
99104
Kind: "ToolServerList",
100105
})
101-
106+
102107
if err := r.List(ctx, list, &client.ListOptions{Limit: 1}); err == nil {
103-
return "v1alpha1", nil
108+
return kagentAPIVersionV1Alpha1
104109
}
105-
110+
106111
// If neither works, return the preferred version anyway
107112
// The actual resource creation will fail with a clear error
108-
return preferred, nil
113+
return preferred
109114
}
110115

111116
// ensureKagentToolServer ensures a kagent resource exists for the ToolHive MCPServer
@@ -120,18 +125,14 @@ func (r *MCPServerReconciler) ensureKagentToolServer(ctx context.Context, mcpSer
120125
}
121126

122127
// Detect which kagent API version to use
123-
apiVersion, err := r.detectKagentAPIVersion(ctx)
124-
if err != nil {
125-
logger.Error(err, "Failed to detect kagent API version, using default", "default", apiVersion)
126-
}
127-
128+
apiVersion := r.detectKagentAPIVersion(ctx)
128129
logger.V(1).Info("Using kagent API version", "version", apiVersion)
129130

130131
// Create the appropriate kagent resource based on API version
131132
var kagentResource *unstructured.Unstructured
132133
var gvk schema.GroupVersionKind
133-
134-
if apiVersion == "v1alpha2" {
134+
135+
if apiVersion == kagentAPIVersionV1Alpha2 {
135136
kagentResource = r.createKagentRemoteMCPServerObject(mcpServer)
136137
gvk = kagentRemoteMCPServerGVK
137138
} else {
@@ -142,7 +143,7 @@ func (r *MCPServerReconciler) ensureKagentToolServer(ctx context.Context, mcpSer
142143
// Check if the kagent resource already exists
143144
existing := &unstructured.Unstructured{}
144145
existing.SetGroupVersionKind(gvk)
145-
err = r.Get(ctx, types.NamespacedName{
146+
err := r.Get(ctx, types.NamespacedName{
146147
Name: kagentResource.GetName(),
147148
Namespace: kagentResource.GetNamespace(),
148149
}, existing)

cmd/thv-operator/controllers/mcpserver_kagent_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -199,12 +199,12 @@ func TestCreateKagentToolServerObjectStreamableHTTP(t *testing.T) {
199199
assert.Equal(t, expectedURL, url)
200200
}
201201

202-
func TestEnsureKagentToolServer(t *testing.T) {
202+
func TestEnsureKagentToolServer(t *testing.T) { //nolint:tparallel // Can't parallelize due to environment variable usage
203203
t.Parallel()
204204
// Create scheme with support for unstructured resources
205205
scheme := runtime.NewScheme()
206206
_ = mcpv1alpha1.AddToScheme(scheme)
207-
207+
208208
// Register kagent GVKs as unstructured types
209209
// This allows the fake client to handle them even without the actual CRDs
210210
// We need to add these to the scheme so the fake client knows about them
@@ -218,7 +218,7 @@ func TestEnsureKagentToolServer(t *testing.T) {
218218
Version: "v1alpha2",
219219
Kind: "RemoteMCPServer",
220220
}
221-
221+
222222
// Add the unstructured types to the scheme
223223
scheme.AddKnownTypeWithName(kagentToolServerGVK, &unstructured.Unstructured{})
224224
scheme.AddKnownTypeWithName(schema.GroupVersionKind{
@@ -247,7 +247,7 @@ func TestEnsureKagentToolServer(t *testing.T) {
247247
},
248248
}
249249

250-
t.Run("creates kagent ToolServer when enabled", func(t *testing.T) {
250+
t.Run("creates kagent ToolServer when enabled", func(t *testing.T) { //nolint:paralleltest // Can't parallelize due to environment variable usage
251251
// Don't run in parallel as environment variables are shared
252252
// Enable kagent integration
253253
os.Setenv("KAGENT_INTEGRATION_ENABLED", "true")
@@ -270,22 +270,22 @@ func TestEnsureKagentToolServer(t *testing.T) {
270270
// In a real cluster with kagent CRDs installed, this would create the resource
271271
// With the fake client, it will try to create but may not be able to verify
272272
require.NoError(t, err)
273-
273+
274274
// Try to get the created resource using the client
275275
kagentToolServer := &unstructured.Unstructured{}
276276
kagentToolServer.SetGroupVersionKind(schema.GroupVersionKind{
277277
Group: "kagent.dev",
278278
Version: "v1alpha1",
279279
Kind: "ToolServer",
280280
})
281-
281+
282282
// Try to get the resource - this may fail with fake client
283283
// but the important part is that ensureKagentToolServer didn't error
284284
err = client.Get(context.Background(), types.NamespacedName{
285285
Name: "toolhive-test-mcp",
286286
Namespace: "test-namespace",
287287
}, kagentToolServer)
288-
288+
289289
// With fake client and unregistered CRDs, we expect this to fail
290290
// but that's OK - the main test is that ensureKagentToolServer works
291291
if err == nil {
@@ -294,7 +294,7 @@ func TestEnsureKagentToolServer(t *testing.T) {
294294
}
295295
})
296296

297-
t.Run("deletes kagent ToolServer when disabled", func(t *testing.T) {
297+
t.Run("deletes kagent ToolServer when disabled", func(t *testing.T) { //nolint:paralleltest // Can't parallelize due to environment variable usage
298298
// Don't run in parallel as environment variables are shared
299299
// Disable kagent integration
300300
os.Setenv("KAGENT_INTEGRATION_ENABLED", "false")

0 commit comments

Comments
 (0)