Skip to content

Commit

Permalink
added: list pods prompt
Browse files Browse the repository at this point in the history
  • Loading branch information
strowk committed Dec 14, 2024
1 parent 36b9b83 commit 35db694
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 4 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
9 changes: 9 additions & 0 deletions internal/k8s/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
143 changes: 143 additions & 0 deletions internal/prompts/pods.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
5 changes: 5 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -25,6 +26,9 @@ func getCapabilities() *mcp.ServerCapabilities {
ListChanged: utils.Ptr(false),
Subscribe: utils.Ptr(false),
},
Prompts: &mcp.ServerCapabilitiesPrompts{
ListChanged: utils.Ptr(false),
},
}
}

Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/npm-mcp-k8s/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
23 changes: 23 additions & 0 deletions testdata/list_prompts_test.yaml
Original file line number Diff line number Diff line change
@@ -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,
},
]
}
]
},
}
47 changes: 43 additions & 4 deletions testdata/with_k3d/list_k8s_pods_test.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
case: List k8s pods using tool
in:
{
"jsonrpc": "2.0",
Expand All @@ -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,
Expand Down

0 comments on commit 35db694

Please sign in to comment.