Skip to content

Commit

Permalink
feat(kubernetes): use custom ToolResponseEncoder for common serializa…
Browse files Browse the repository at this point in the history
…tion
  • Loading branch information
manusa committed Feb 7, 2025
1 parent d51115d commit a7d7f41
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 71 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.quarkus.mcp.servers.kubernetes;

import io.fabric8.kubernetes.client.KubernetesClient;
import io.quarkiverse.mcp.server.TextContent;
import io.quarkiverse.mcp.server.ToolResponse;
import io.quarkiverse.mcp.server.ToolResponseEncoder;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;

import javax.annotation.Priority;
import java.util.List;

@Singleton
@Priority(1)
public class KubernetesResourceEncoder implements ToolResponseEncoder<Object> {

@Inject
KubernetesClient kubernetesClient;

@Override
public boolean supports(Class<?> runtimeType) {
return true;
}

@Override
public ToolResponse encode(Object value) {
return new ToolResponse(false, List.of(new TextContent(
kubernetesClient.getKubernetesSerialization().asJson(value)
)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@
//DEPS io.quarkiverse.mcp:quarkus-mcp-server-stdio:1.0.0.Beta1
//DEPS io.fabric8:openshift-model

import io.fabric8.kubernetes.api.model.GenericKubernetesResource;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.Namespace;
import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.api.model.ServiceBuilder;
import io.fabric8.kubernetes.api.model.ServiceSpecBuilder;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.dsl.NonDeletingOperation;
import io.fabric8.openshift.api.model.Route;
Expand All @@ -24,7 +28,7 @@


import java.util.ArrayList;
import java.util.List;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.TimeUnit;

Expand All @@ -47,60 +51,58 @@ void init() {
}

@Tool(description = "Get the current Kubernetes configuration")
public String configuration_get() {
public Config configuration_get() {
try {
return asJson(kubernetesClient.getConfiguration());
return kubernetesClient.getConfiguration();
} catch (Exception e) {
throw new ToolCallException("Failed to get configuration: " + e.getMessage(), e);
}
}

@Tool(description = "List Kubernetes resources in the current cluster by providing their apiVersion and kind and optionally the namespace")
public String resources_list(
public Collection<GenericKubernetesResource> resources_list(
@ToolArg(description = "apiVersion of the resources (examples of valid apiVersion are: v1, apps/v1, networking.k8s.io/v1") String apiVersion,
@ToolArg(description = "kind of the resources (examples of valid kind are: Pod, Service, Deployment, Ingress)") String kind,
@ToolArg(description = "Namespace to retrieve the namespaced resources from (ignored in case of cluster scoped resources)", required = false) String namespace
) {
try {
final var resource = kubernetesClient.genericKubernetesResources(apiVersion, kind);
if (namespace != null && !namespace.isBlank()) {
return asJson(resource.inNamespace(namespace).list().getItems());
return resource.inNamespace(namespace).list().getItems();
}
try {
return asJson(resource.inAnyNamespace().list().getItems());
return resource.inAnyNamespace().list().getItems();
} catch (Exception e) {
return asJson(resource.list().getItems());
return resource.list().getItems();
}
} catch (Exception e) {
throw new ToolCallException("Failed to get resources for " + apiVersion + " " + kind + ": " + e.getMessage(), e);
}
}

@Tool(description = "Get a Kubernetes resource in the current cluster by providing its apiVersion, kind, optionally the namespace, and its name")
public String resources_get(
public GenericKubernetesResource resources_get(
@ToolArg(description = "apiVersion of the resources (examples of valid apiVersion are: v1, apps/v1, networking.k8s.io/v1") String apiVersion,
@ToolArg(description = "kind of the resources (examples of valid kind are: Pod, Service, Deployment, Ingress)") String kind,
@ToolArg(description = "Namespace to retrieve the namespaced resource from (ignored in case of cluster scoped resources)", required = false) String namespace,
@ToolArg(description = "Name of the resource", required = false) String name
) {
try {
return asJson(
kubernetesClient.genericKubernetesResources(apiVersion, kind)
.inNamespace(namespace == null ? kubernetesClient.getNamespace() : namespace)
.withName(name)
.get()
);
return kubernetesClient.genericKubernetesResources(apiVersion, kind)
.inNamespace(namespace == null ? kubernetesClient.getNamespace() : namespace)
.withName(name)
.get();
} catch (Exception e) {
throw new ToolCallException("Failed to get the resource for " + apiVersion + " " + kind + ": " + e.getMessage(), e);
}
}

@Tool(description = "Create or update a Kubernetes resource in the current cluster by providing a YAML or JSON representation of the resource")
public String resources_create_or_update(
public HasMetadata resources_create_or_update(
@ToolArg(description = "A JSON or YAML containing a representation of the Kubernetes resource. Should include top-level fields such as apiVersion,kind,metadata, and spec") String resource
) {
try {
return asJson(kubernetesClient.resource(resource).createOr(NonDeletingOperation::update));
return kubernetesClient.resource(resource).createOr(NonDeletingOperation::update);
} catch (Exception e) {
throw new ToolCallException("Failed to create or update the resource: " + e.getMessage(), e);
}
Expand All @@ -126,48 +128,46 @@ public String resources_delete(
}

@Tool(description = "List all the Kubernetes namespaces in the current cluster")
public String namespaces_list() {
public Collection<Namespace> namespaces_list() {
try {
return asJson(kubernetesClient.namespaces().list().getItems());
return kubernetesClient.namespaces().list().getItems();
} catch (Exception e) {
throw new ToolCallException("Failed to list namespaces: " + e.getMessage(), e);
}
}

@Tool(description = "List all the Kubernetes pods in the current cluster")
public String pods_list() {
public Collection<Pod> pods_list() {
try {
return asJson(kubernetesClient.pods().inAnyNamespace().list().getItems());
return kubernetesClient.pods().inAnyNamespace().list().getItems();
} catch (Exception e) {
try {
return asJson(kubernetesClient.pods().list().getItems());
return kubernetesClient.pods().list().getItems();
} catch (Exception e2) {
throw new ToolCallException("Failed to list pods: " + e2.getMessage(), e2);
}
}
}

@Tool(description = "List all the Kubernetes pods in the specified namespace in the current cluster")
public String pods_list_in_namespace(@ToolArg(description = "Namespace to list pods from") String namespace) {
public Collection<Pod> pods_list_in_namespace(@ToolArg(description = "Namespace to list pods from") String namespace) {
try {
return asJson(kubernetesClient.pods().inNamespace(namespace).list().getItems());
return kubernetesClient.pods().inNamespace(namespace).list().getItems();
} catch (Exception e) {
throw new ToolCallException("Failed to list pods in namespace: " + e.getMessage(), e);
}
}

@Tool(description = "Get a Kubernetes Pod in the current namespace with the provided name")
public String pods_get(
public Pod pods_get(
@ToolArg(description = "Namespace to get the Pod from", required = false) String namespace,
@ToolArg(description = "Name of the Pod", required = false) String name
) {
try {
return asJson(
kubernetesClient.pods()
.inNamespace(namespace == null ? kubernetesClient.getNamespace() : namespace)
.withName(name)
.get()
);
return kubernetesClient.pods()
.inNamespace(namespace == null ? kubernetesClient.getNamespace() : namespace)
.withName(name)
.get();
} catch (Exception e) {
throw new ToolCallException("Failed to get pod: " + e.getMessage(), e);
}
Expand Down Expand Up @@ -224,14 +224,14 @@ public String pods_log(
}

@Tool(description = "Run a Kubernetes Pod in the current namespace with the provided container image and optional name")
public String pods_run(
public Collection<HasMetadata> pods_run(
@ToolArg(description = "Namespace to run the Pod in", required = false) String namespace,
@ToolArg(description = "Name of the Pod (Optional, random name if not provided)", required = false) String name,
@ToolArg(description = "Container Image to run in the Pod") String image,
@ToolArg(description = "TCP/IP port to expose from the Pod container (Optional, no port exposed if not provided)", required = false) Integer port
) {
try {
final List<HasMetadata> createdResources = new ArrayList<>();
final Collection<HasMetadata> createdResources = new ArrayList<>();
final var effectiveName = name == null ? "mcp-kubernetes-pod-" + System.currentTimeMillis() : name;
final var effectiveNamespace = namespace == null ? kubernetesClient.getNamespace() : namespace;
final var labels = Map.of(
Expand Down Expand Up @@ -270,13 +270,9 @@ public String pods_run(
createdResources.add(kubernetesClient.resource(route).unlock().createOr(NonDeletingOperation::update));
}
createdResources.add(runCommand.done());
return asJson(createdResources);
return createdResources;
} catch (Exception e) {
throw new ToolCallException("Failed to run pod: " + e.getMessage(), e);
}
}

private <T> String asJson(T object) {
return kubernetesClient.getKubernetesSerialization().asJson(object);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import dev.langchain4j.mcp.client.McpClient;
import io.fabric8.kubernetes.api.model.Namespace;
import io.fabric8.kubernetes.api.model.NamespaceBuilder;
import io.fabric8.kubernetes.api.model.Node;
import io.fabric8.kubernetes.api.model.NodeBuilder;
import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.api.model.PodBuilder;
import io.fabric8.kubernetes.client.KubernetesClient;
Expand All @@ -25,6 +27,7 @@

import static io.quarkus.mcp.servers.kubernetes.MCPTestUtils.initMcpClient;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.tuple;

public class MCPServerKubernetesIT {

Expand Down Expand Up @@ -76,19 +79,38 @@ void namespaces_list() {
.contains("a-namespace-to-list"));
}

@Nested
class GenericResourceOperations {

@Test
void resources_list_clusterScoped() {
kubernetesClient.nodes()
.resource(new NodeBuilder().withNewMetadata().withName("a-node-to-list").endMetadata().build())
.create();
assertThat(client.executeTool(ToolExecutionRequest.builder().name("resources_list")
.arguments("{\"apiVersion\":\"v1\",\"kind\":\"Node\"}").build()))
.isNotBlank()
.satisfies(pList -> assertThat(unmarshalList(pList, Node.class))
.extracting("kind", "metadata.name")
.contains(tuple("Node", "a-node-to-list")));
}
}

@Nested
class PodOperations {

@Test
void pods_list() {
kubernetesClient.pods()
.resource(new PodBuilder().withNewMetadata().withName("a-pod-to-list").endMetadata().build())
.create();
for (int it = 0; it < 3; it++) {
kubernetesClient.pods()
.resource(new PodBuilder().withNewMetadata().withName("a-pod-to-list-" + it).endMetadata().build())
.create();
}
assertThat(client.executeTool(ToolExecutionRequest.builder().name("pods_list").arguments("{}").build()))
.isNotBlank()
.satisfies(pList -> assertThat(unmarshalList(pList, Pod.class))
.extracting("metadata.name")
.contains("a-pod-to-list"));
.contains("a-pod-to-list-1", "a-pod-to-list-2"));
}

@Test
Expand Down
Loading

0 comments on commit a7d7f41

Please sign in to comment.