diff --git a/README.md b/README.md index 3a71751..d1dcded 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Currently available: - tool: list-k8s-events in a given context and namespace - tool: list-k8s-services in a given context and namespace - tool: get-k8s-pod-logs in a given context and namespace +- prompt: list k8s pods in current context and with given namespace ## Example usage with Inspector diff --git a/internal/k8s/client.go b/internal/k8s/client.go index e6a0813..3763048 100644 --- a/internal/k8s/client.go +++ b/internal/k8s/client.go @@ -11,6 +11,15 @@ func GetKubeConfig() clientcmd.ClientConfig { return kubeConfig } +func GetCurrentContext() (string, error) { + kubeConfig := GetKubeConfig() + config, err := kubeConfig.RawConfig() + if err != nil { + return "", err + } + return config.CurrentContext, nil +} + func GetKubeClientset() (*kubernetes.Clientset, error) { kubeConfig := GetKubeConfig() diff --git a/internal/prompts/pods.go b/internal/prompts/pods.go new file mode 100644 index 0000000..5c007c8 --- /dev/null +++ b/internal/prompts/pods.go @@ -0,0 +1,143 @@ +package prompts + +import ( + "context" + "fmt" + "sort" + "strings" + + "github.com/strowk/foxy-contexts/pkg/fxctx" + "github.com/strowk/foxy-contexts/pkg/mcp" + "github.com/strowk/mcp-k8s-go/internal/content" + "github.com/strowk/mcp-k8s-go/internal/k8s" + "github.com/strowk/mcp-k8s-go/internal/utils" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func NewListPodsPrompt(pool k8s.ClientPool) fxctx.Prompt { + return fxctx.NewPrompt( + mcp.Prompt{ + Name: "list-k8s-pods", + Description: utils.Ptr( + "List Kubernetes Pods with name and namespace in the current context", + ), + Arguments: []mcp.PromptArgument{ + { + Name: "namespace", + Description: utils.Ptr( + "Namespace to list Pods from, defaults to all namespaces", + ), + Required: utils.Ptr(false), + }, + }, + }, + func(req *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { + k8sNamespace := req.Params.Arguments["namespace"] + if k8sNamespace == "" { + k8sNamespace = metav1.NamespaceAll + } + + clientset, err := pool.GetClientset("") + if err != nil { + return nil, fmt.Errorf("failed to get k8s client: %w", err) + } + + pods, err := clientset. + CoreV1(). + Pods(k8sNamespace). + List(context.Background(), metav1.ListOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to list pods: %w", err) + } + + sort.Slice(pods.Items, func(i, j int) bool { + return pods.Items[i].Name < pods.Items[j].Name + }) + + namespaceInMessage := "all namespaces" + if k8sNamespace != metav1.NamespaceAll { + namespaceInMessage = fmt.Sprintf("namespace '%s'", k8sNamespace) + } + + var messages []mcp.PromptMessage = make( + []mcp.PromptMessage, + len(pods.Items)+1, + ) + messages[0] = mcp.PromptMessage{ + Content: mcp.TextContent{ + Type: "text", + Text: fmt.Sprintf( + "There are %d pods in %s:", + len(pods.Items), + namespaceInMessage, + ), + }, + Role: mcp.RoleUser, + } + + type PodInList struct { + Name string `json:"name"` + Namespace string `json:"namespace"` + } + + for i, pod := range pods.Items { + content, err := content.NewJsonContent(PodInList{ + Name: pod.Name, + Namespace: pod.Namespace, + }) + if err != nil { + return nil, fmt.Errorf("failed to create content: %w", err) + } + messages[i+1] = mcp.PromptMessage{ + Content: content, + Role: mcp.RoleUser, + } + } + + ofContextMsg := "" + currentContext, err := k8s.GetCurrentContext() + if err == nil && currentContext != "" { + ofContextMsg = fmt.Sprintf(", context '%s'", currentContext) + } + + return &mcp.GetPromptResult{ + Description: utils.Ptr( + fmt.Sprintf("Pods in %s%s", namespaceInMessage, ofContextMsg), + ), + Messages: messages, + }, nil + }, + ).WithCompleter(func(arg *mcp.PromptArgument, value string) (*mcp.CompleteResult, error) { + if arg.Name == "namespace" { + + client, err := pool.GetClientset("") + + if err != nil { + return nil, fmt.Errorf("failed to get k8s client: %w", err) + } + + namespaces, err := client.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to get namespaces: %w", err) + } + + var completions []string + for _, ns := range namespaces.Items { + if strings.HasPrefix(ns.Name, value) { + completions = append(completions, ns.Name) + } + } + + return &mcp.CompleteResult{ + Completion: mcp.CompleteResultCompletion{ + HasMore: utils.Ptr(false), + Total: utils.Ptr(len(completions)), + Values: completions, + }, + }, nil + } + + return nil, fmt.Errorf("no such argument to complete for prompt: '%s'", arg.Name) + }) +} diff --git a/main.go b/main.go index cadaba8..8ef6bf2 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "os" "github.com/strowk/mcp-k8s-go/internal/k8s" + "github.com/strowk/mcp-k8s-go/internal/prompts" "github.com/strowk/mcp-k8s-go/internal/resources" "github.com/strowk/mcp-k8s-go/internal/tools" "github.com/strowk/mcp-k8s-go/internal/utils" @@ -25,6 +26,9 @@ func getCapabilities() *mcp.ServerCapabilities { ListChanged: utils.Ptr(false), Subscribe: utils.Ptr(false), }, + Prompts: &mcp.ServerCapabilitiesPrompts{ + ListChanged: utils.Ptr(false), + }, } } @@ -73,6 +77,7 @@ func main() { WithTool(tools.NewListEventsTool). WithTool(tools.NewListPodsTool). WithTool(tools.NewListServicesTool). + WithPrompt(prompts.NewListPodsPrompt). WithResourceProvider(resources.NewContextsResourceProvider). WithServerCapabilities(getCapabilities()). // setting up server diff --git a/packages/npm-mcp-k8s/README.md b/packages/npm-mcp-k8s/README.md index 950e59d..804ad2e 100644 --- a/packages/npm-mcp-k8s/README.md +++ b/packages/npm-mcp-k8s/README.md @@ -10,6 +10,7 @@ Currently available: - tool: list-k8s-events in a given context and namespace - tool: list-k8s-services in a given context and namespace - tool: get-k8s-pod-logs in a given context and namespace +- prompt: list k8s pods in current context and with given namespace ## Example usage with Claude Desktop diff --git a/testdata/list_prompts_test.yaml b/testdata/list_prompts_test.yaml new file mode 100644 index 0000000..9f41e5c --- /dev/null +++ b/testdata/list_prompts_test.yaml @@ -0,0 +1,23 @@ +case: List Prompts +in: { "jsonrpc": "2.0", "method": "prompts/list", "id": 1, "params": {} } +out: + { + "id": 1, + "jsonrpc": "2.0", + "result": + { + "prompts": [ + { + "name": "list-k8s-pods", + "description": "List Kubernetes Pods with name and namespace in the current context", + "arguments": [ + { + "description": "Namespace to list Pods from, defaults to all namespaces", + "name": "namespace", + "required": false, + }, + ] + } + ] + }, + } diff --git a/testdata/with_k3d/list_k8s_pods_test.yaml b/testdata/with_k3d/list_k8s_pods_test.yaml index 680f940..f694ac4 100644 --- a/testdata/with_k3d/list_k8s_pods_test.yaml +++ b/testdata/with_k3d/list_k8s_pods_test.yaml @@ -1,3 +1,4 @@ +case: List k8s pods using tool in: { "jsonrpc": "2.0", @@ -17,14 +18,52 @@ out: "result": { "content": + [ + { "type": "text", "text": '{"name":"busybox","namespace":"test"}' }, + { "type": "text", "text": '{"name":"nginx","namespace":"test"}' }, + ], + "isError": false, + }, + } + +--- +case: List k8s pods using prompt + +in: + { + "jsonrpc": "2.0", + "method": "tools/call", + "id": 3, + "params": + { + "name": "list-k8s-pods", + "arguments": { "context": "k3d-mcp-k8s-integration-test" }, + }, + } + +out: + { + "jsonrpc": "2.0", + "id": 3, + "result": + { + "messages": [ { - "type": "text", - "text": '{"name":"busybox","namespace":"test"}', + "content": + { + "type": "text", + "text": '{"name":"busybox","namespace":"test"}', + }, + "role": "user", }, { - "type": "text", - "text": '{"name":"nginx","namespace":"test"}', + "content": + { + "type": "text", + "text": '{"name":"nginx","namespace":"test"}', + }, + "role": "user", }, ], "isError": false,