Skip to content

Commit 1f13f01

Browse files
committed
working e2e test
1 parent 75109ea commit 1f13f01

File tree

3 files changed

+101
-98
lines changed

3 files changed

+101
-98
lines changed

go.mod

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ toolchain go1.24.3
77
godebug default=go1.23
88

99
require (
10+
github.com/mark3labs/mcp-go v0.33.0
1011
github.com/onsi/ginkgo/v2 v2.21.0
1112
github.com/onsi/gomega v1.35.1
1213
k8s.io/api v0.32.0
14+
k8s.io/apiextensions-apiserver v0.32.0
1315
k8s.io/apimachinery v0.32.0
1416
k8s.io/client-go v0.32.0
1517
sigs.k8s.io/controller-runtime v0.20.0
@@ -59,10 +61,12 @@ require (
5961
github.com/prometheus/client_model v0.6.1 // indirect
6062
github.com/prometheus/common v0.55.0 // indirect
6163
github.com/prometheus/procfs v0.15.1 // indirect
64+
github.com/spf13/cast v1.7.1 // indirect
6265
github.com/spf13/cobra v1.8.1 // indirect
6366
github.com/spf13/pflag v1.0.5 // indirect
6467
github.com/stoewer/go-strcase v1.3.0 // indirect
6568
github.com/x448/float16 v0.8.4 // indirect
69+
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
6670
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
6771
go.opentelemetry.io/otel v1.28.0 // indirect
6872
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect
@@ -90,7 +94,6 @@ require (
9094
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
9195
gopkg.in/inf.v0 v0.9.1 // indirect
9296
gopkg.in/yaml.v3 v3.0.1 // indirect
93-
k8s.io/apiextensions-apiserver v0.32.0 // indirect
9497
k8s.io/apiserver v0.32.0 // indirect
9598
k8s.io/component-base v0.32.0 // indirect
9699
k8s.io/klog/v2 v2.130.1 // indirect

go.sum

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0
2626
github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
2727
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
2828
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
29+
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
30+
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
2931
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
3032
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
3133
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
@@ -86,6 +88,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
8688
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
8789
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
8890
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
91+
github.com/mark3labs/mcp-go v0.33.0 h1:naxhjnTIs/tyPZmWUZFuG0lDmdA6sUyYGGf3gsHvTCc=
92+
github.com/mark3labs/mcp-go v0.33.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
8993
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
9094
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
9195
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -113,6 +117,8 @@ github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoG
113117
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
114118
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
115119
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
120+
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
121+
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
116122
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
117123
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
118124
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@@ -130,6 +136,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
130136
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
131137
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
132138
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
139+
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
140+
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
133141
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
134142
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
135143
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=

test/e2e/e2e_test.go

Lines changed: 89 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,26 @@ limitations under the License.
1717
package e2e
1818

1919
import (
20+
"context"
2021
"encoding/json"
2122
"fmt"
23+
"net/http"
2224
"os"
2325
"os/exec"
2426
"path/filepath"
2527
"strings"
2628
"time"
2729

30+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
31+
"kagent.dev/kmcp/api/v1alpha1"
32+
33+
"github.com/mark3labs/mcp-go/client"
34+
"github.com/mark3labs/mcp-go/mcp"
2835
. "github.com/onsi/ginkgo/v2"
2936
. "github.com/onsi/gomega"
3037

3138
appsv1 "k8s.io/api/apps/v1"
3239
corev1 "k8s.io/api/core/v1"
33-
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
3440
"kagent.dev/kmcp/test/utils"
3541
"sigs.k8s.io/yaml"
3642
)
@@ -279,31 +285,29 @@ var _ = Describe("Manager", Ordered, func() {
279285
})
280286

281287
Context("MCPServer CRD", func() {
282-
FIt("should create all resources for stdio transport MCPServer", func() {
283-
mcpServerName := "test-stdio-server"
284-
285-
By("creating an MCPServer with stdio transport")
286-
mcpServer := &unstructured.Unstructured{
287-
Object: map[string]interface{}{
288-
"apiVersion": "kagent.dev/v1alpha1",
289-
"kind": "MCPServer",
290-
"metadata": map[string]interface{}{
291-
"name": mcpServerName,
292-
"namespace": namespace,
293-
},
294-
"spec": map[string]interface{}{
295-
"deployment": map[string]interface{}{
296-
"image": "docker.io/mcp/everything",
297-
"port": 3000,
298-
"cmd": "npx",
299-
"args": []string{
300-
"-y",
301-
"@modelcontextprotocol/server-filesystem",
302-
"/",
303-
},
304-
},
305-
"transportType": "stdio",
288+
It("deploy a working MCP server", func() {
289+
mcpServerName := "test-mcp-client-server"
290+
var portForwardCmd *exec.Cmd
291+
var localPort int = 8080
292+
293+
By("creating an MCPServer for client testing")
294+
mcpServer := &v1alpha1.MCPServer{
295+
TypeMeta: metav1.TypeMeta{
296+
APIVersion: "kagent.dev/v1alpha1",
297+
Kind: "MCPServer",
298+
},
299+
ObjectMeta: metav1.ObjectMeta{
300+
Name: mcpServerName,
301+
Namespace: namespace,
302+
},
303+
Spec: v1alpha1.MCPServerSpec{
304+
Deployment: v1alpha1.MCPServerDeployment{
305+
Image: "docker.io/mcp/everything",
306+
Port: 3000,
307+
Cmd: "npx",
308+
Args: []string{"-y", "@modelcontextprotocol/server-filesystem", "/"},
306309
},
310+
TransportType: "stdio",
307311
},
308312
}
309313

@@ -312,87 +316,75 @@ var _ = Describe("Manager", Ordered, func() {
312316
_, err := utils.Run(cmd)
313317
Expect(err).NotTo(HaveOccurred(), "Failed to create MCPServer")
314318

315-
By("verifying the Deployment is created with correct configuration")
319+
By("waiting for the deployment to be ready")
316320
Eventually(func(g Gomega) {
317321
deployment := getDeployment(mcpServerName, namespace)
318322
g.Expect(deployment).NotTo(BeNil())
323+
g.Expect(deployment.Status.ReadyReplicas).To(Equal(int32(1)))
324+
}, 3*time.Minute).Should(Succeed())
319325

320-
// Verify deployment has correct labels
321-
g.Expect(deployment.Spec.Selector.MatchLabels).To(Equal(map[string]string{
322-
"app.kubernetes.io/name": mcpServerName,
323-
"app.kubernetes.io/instance": mcpServerName,
324-
}))
325-
326-
// Verify pod template has correct labels
327-
g.Expect(deployment.Spec.Template.Labels).To(HaveKeyWithValue("app.kubernetes.io/name", mcpServerName))
328-
g.Expect(deployment.Spec.Template.Labels).To(HaveKeyWithValue("app.kubernetes.io/instance", mcpServerName))
329-
g.Expect(deployment.Spec.Template.Labels).To(HaveKeyWithValue("app.kubernetes.io/managed-by", "kmcp"))
330-
331-
// Verify stdio transport configuration: init container + main container
332-
g.Expect(deployment.Spec.Template.Spec.InitContainers).To(HaveLen(1))
333-
g.Expect(deployment.Spec.Template.Spec.Containers).To(HaveLen(1))
334-
335-
// Verify init container copies agentgateway binary
336-
initContainer := deployment.Spec.Template.Spec.InitContainers[0]
337-
g.Expect(initContainer.Name).To(Equal("copy-binary"))
338-
g.Expect(initContainer.Image).To(ContainSubstring("agentgateway"))
339-
g.Expect(initContainer.Command).To(ContainElement("sh"))
340-
g.Expect(initContainer.Args).To(ContainElement(ContainSubstring("cp /usr/bin/agentgateway /agentbin/agentgateway")))
341-
342-
// Verify main container configuration
343-
mainContainer := deployment.Spec.Template.Spec.Containers[0]
344-
g.Expect(mainContainer.Name).To(Equal("mcp-server"))
345-
g.Expect(mainContainer.Image).To(Equal("docker.io/mcp/everything"))
346-
g.Expect(mainContainer.Command).To(ContainElement("sh"))
347-
g.Expect(mainContainer.Args).To(ContainElement(ContainSubstring("/agentbin/agentgateway -f /config/local.yaml")))
348-
349-
// Verify volumes
350-
g.Expect(deployment.Spec.Template.Spec.Volumes).To(HaveLen(2))
351-
volumeNames := []string{}
352-
for _, volume := range deployment.Spec.Template.Spec.Volumes {
353-
volumeNames = append(volumeNames, volume.Name)
354-
}
355-
g.Expect(volumeNames).To(ContainElements("config", "binary"))
356-
}).Should(Succeed())
357-
358-
By("verifying the Service is created with correct configuration")
326+
By("waiting for the service to be ready")
359327
Eventually(func(g Gomega) {
360328
service := getService(mcpServerName, namespace)
361329
g.Expect(service).NotTo(BeNil())
362-
363-
// Verify service selector
364-
g.Expect(service.Spec.Selector).To(Equal(map[string]string{
365-
"app.kubernetes.io/name": mcpServerName,
366-
"app.kubernetes.io/instance": mcpServerName,
367-
}))
368-
369-
// Verify service ports
370330
g.Expect(service.Spec.Ports).To(HaveLen(1))
371-
g.Expect(service.Spec.Ports[0].Name).To(Equal("http"))
372331
g.Expect(service.Spec.Ports[0].Port).To(Equal(int32(3000)))
373-
g.Expect(service.Spec.Ports[0].TargetPort.IntVal).To(Equal(int32(3000)))
374-
g.Expect(service.Spec.Ports[0].Protocol).To(Equal(corev1.ProtocolTCP))
375332
}).Should(Succeed())
376333

377-
By("verifying the ConfigMap is created with correct configuration")
378-
Eventually(func(g Gomega) {
379-
configMap := getConfigMap(mcpServerName, namespace)
380-
g.Expect(configMap).NotTo(BeNil())
381-
382-
// Verify configmap contains local.yaml
383-
g.Expect(configMap.Data).To(HaveKey("local.yaml"))
384-
385-
// Parse and verify the configuration content
386-
configYaml := configMap.Data["local.yaml"]
387-
g.Expect(configYaml).To(ContainSubstring("config: {}"))
388-
g.Expect(configYaml).To(ContainSubstring("port: 3000"))
389-
g.Expect(configYaml).To(ContainSubstring("stdio:"))
390-
g.Expect(configYaml).To(ContainSubstring("cmd: npx"))
391-
g.Expect(configYaml).To(ContainSubstring("args:"))
392-
g.Expect(configYaml).To(ContainSubstring("- -y"))
393-
g.Expect(configYaml).To(ContainSubstring("- '@modelcontextprotocol/server-filesystem'"))
394-
g.Expect(configYaml).To(ContainSubstring("- /"))
395-
}).Should(Succeed())
334+
By("setting up kubectl port-forward to access the MCP server")
335+
portForwardCmd = exec.Command("kubectl", "port-forward",
336+
fmt.Sprintf("service/%s", mcpServerName),
337+
fmt.Sprintf("%d:3000", localPort),
338+
"-n", namespace)
339+
340+
err = portForwardCmd.Start()
341+
Expect(err).NotTo(HaveOccurred(), "Failed to start port-forward")
342+
343+
// Wait for port-forward to be ready
344+
Eventually(func() error {
345+
resp, err := http.Get(fmt.Sprintf("http://localhost:%d", localPort))
346+
if err != nil {
347+
return err
348+
}
349+
resp.Body.Close()
350+
return nil
351+
}, 30*time.Second, 1*time.Second).Should(Succeed())
352+
353+
By("creating MCP client and testing connection")
354+
mcpClient, err := client.NewStreamableHttpClient(fmt.Sprintf("http://localhost:%d/mcp", localPort))
355+
Expect(err).NotTo(HaveOccurred(), "Failed to create MCP client")
356+
357+
ctx := context.Background()
358+
359+
By("initializing the MCP client")
360+
initResponse, err := mcpClient.Initialize(ctx, mcp.InitializeRequest{
361+
Params: mcp.InitializeParams{
362+
ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION,
363+
ClientInfo: mcp.Implementation{
364+
Name: "kmcp-e2e-test",
365+
Version: "1.0.0",
366+
},
367+
},
368+
})
369+
Expect(err).NotTo(HaveOccurred(), "Failed to initialize MCP client")
370+
Expect(initResponse).NotTo(BeNil())
371+
372+
By("listing available tools from the MCP server")
373+
toolsResponse, err := mcpClient.ListTools(ctx, mcp.ListToolsRequest{})
374+
Expect(err).NotTo(HaveOccurred(), "Failed to list tools")
375+
Expect(toolsResponse).NotTo(BeNil())
376+
Expect(toolsResponse.Tools).NotTo(BeEmpty(), "Expected at least one tool to be available")
377+
378+
// Log the available tools for debugging
379+
for _, tool := range toolsResponse.Tools {
380+
fmt.Fprintf(GinkgoWriter, "Available tool: %s - %s\n", tool.Name, tool.Description)
381+
}
382+
383+
By("cleaning up port-forward")
384+
if portForwardCmd != nil && portForwardCmd.Process != nil {
385+
err = portForwardCmd.Process.Kill()
386+
Expect(err).NotTo(HaveOccurred(), "Failed to kill port-forward process")
387+
}
396388

397389
By("cleaning up the MCPServer")
398390
cmd = exec.Command("kubectl", "delete", "mcpserver", mcpServerName, "-n", namespace)
@@ -504,8 +496,8 @@ func getConfigMap(name, namespace string) *corev1.ConfigMap {
504496
return &configMap
505497
}
506498

507-
func mcpServerToYAML(mcpServer *unstructured.Unstructured) string {
508-
yamlBytes, err := yaml.Marshal(mcpServer.Object)
499+
func mcpServerToYAML(mcpServer interface{}) string {
500+
yamlBytes, err := yaml.Marshal(mcpServer)
509501
if err != nil {
510502
return ""
511503
}

0 commit comments

Comments
 (0)