Skip to content

Commit

Permalink
added: list k8s nodes
Browse files Browse the repository at this point in the history
  • Loading branch information
strowk committed Dec 15, 2024
1 parent df28745 commit 2e03d93
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 11 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
- 🗂️ resource: K8S contexts as read from kubeconfig configurations
- 🤖 tool: list-k8s-contexts
- 🤖 tool: list-k8s-namespaces in a given context
- 🤖 tool: list-k8s-nodes in a given context
- 🤖 tool: list-k8s-pods in a given context and namespace
- 🤖 tool: list-k8s-events in a given context and namespace
- 🤖 tool: list-k8s-services in a given context and namespace
Expand Down
124 changes: 124 additions & 0 deletions internal/tools/nodes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package tools

import (
"context"
"fmt"
"sort"
"time"

"github.com/strowk/mcp-k8s-go/internal/k8s"
"github.com/strowk/mcp-k8s-go/internal/utils"

"github.com/strowk/foxy-contexts/pkg/fxctx"
"github.com/strowk/foxy-contexts/pkg/mcp"
"github.com/strowk/foxy-contexts/pkg/toolinput"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func NewListNodesTool(pool k8s.ClientPool) fxctx.Tool {
contextProperty := "context"
schema := toolinput.NewToolInputSchema(
toolinput.WithString(contextProperty, "Name of the Kubernetes context to use, defaults to current context"),
)
return fxctx.NewTool(
&mcp.Tool{
Name: "list-k8s-nodes",
Description: utils.Ptr("List Kubernetes nodes using specific context"),
InputSchema: schema.GetMcpToolInputSchema(),
},
func(args map[string]interface{}) *mcp.CallToolResult {
input, err := schema.Validate(args)
if err != nil {
return errResponse(err)
}
k8sCtx := input.StringOr(contextProperty, "")

clientset, err := pool.GetClientset(k8sCtx)
if err != nil {
return errResponse(err)
}

nodes, err := clientset.
CoreV1().
Nodes().
List(context.Background(), metav1.ListOptions{})
if err != nil {
return errResponse(err)
}

sort.Slice(nodes.Items, func(i, j int) bool {
return nodes.Items[i].Name < nodes.Items[j].Name
})

var contents []interface{} = make([]interface{}, len(nodes.Items))
for i, ns := range nodes.Items {
// Calculate age
age := time.Since(ns.CreationTimestamp.Time)

// Determine status
status := "NotReady"
for _, condition := range ns.Status.Conditions {
if condition.Type == "Ready" {
if condition.Status == "True" {
status = "Ready"
} else {
status = "NotReady"
}
break
}
}

content, err := NewJsonContent(NodeInList{
Name: ns.Name,
Status: status,
Age: formatAge(age),
CreatedAt: ns.CreationTimestamp.Time,
})
if err != nil {
return errResponse(err)
}
contents[i] = content
}

return &mcp.CallToolResult{
Meta: map[string]interface{}{},
Content: contents,
IsError: utils.Ptr(false),
}
},
)
}

// NodeInList provides a structured representation of node information
type NodeInList struct {
Name string `json:"name"`
Status string `json:"status"`
Age string `json:"age"`
CreatedAt time.Time `json:"created_at"`
}

// formatAge converts a duration to a human-readable age string
func formatAge(duration time.Duration) string {
if duration.Hours() < 1 {
return duration.Round(time.Minute).String()
}
if duration.Hours() < 24 {
return duration.Round(time.Hour).String()
}
days := int(duration.Hours() / 24)
return formatDays(days)
}

// formatDays provides a concise representation of days
func formatDays(days int) string {
if days < 7 {
return fmt.Sprintf("%dd", days)
}
if days < 30 {
weeks := days / 7
return fmt.Sprintf("%dw", weeks)
}
months := days / 30
return fmt.Sprintf("%dmo", months)
}
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ func main() {
WithTool(tools.NewListEventsTool).
WithTool(tools.NewListPodsTool).
WithTool(tools.NewListServicesTool).
WithTool(tools.NewListNodesTool).
WithPrompt(prompts.NewListPodsPrompt).
WithPrompt(prompts.NewListNamespacesPrompt).
WithResourceProvider(resources.NewContextsResourceProvider).
Expand Down
20 changes: 11 additions & 9 deletions packages/npm-mcp-k8s/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@
This is a distribution of MCP server connecting to Kubernetes written in Golang and published to npm.

Currently available:
- resource: K8S contexts as read from kubeconfig configurations
- tool: list-k8s-contexts
- tool: list-k8s-namespaces in a given context
- tool: list-k8s-pods in a given context and namespace
- 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-namespaces in a given context
- prompt: list-k8s-pods in current context and with given namespace

- 🗂️ resource: K8S contexts as read from kubeconfig configurations
- 🤖 tool: list-k8s-contexts
- 🤖 tool: list-k8s-namespaces in a given context
- 🤖 tool: list-k8s-nodes in a given context
- 🤖 tool: list-k8s-pods in a given context and namespace
- 🤖 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-namespaces in a given context
- 💬 prompt: list-k8s-pods in current context and with given namespace

## Example usage with Claude Desktop

Expand Down
28 changes: 26 additions & 2 deletions testdata/list_tools_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,22 @@ out:
},
},
},
{
"name": "list-k8s-nodes",
"description": "List Kubernetes nodes using specific context",
"inputSchema":
{
"type": "object",
"properties":
{
"context":
{
"type": "string",
"description": "Name of the Kubernetes context to use, defaults to current context",
},
},
},
},
{
"name": "list-k8s-pods",
"description": "List Kubernetes pods using specific context in a specified namespace",
Expand All @@ -105,8 +121,16 @@ out:
"type": "object",
"properties":
{
"context": { "type": "string", "description": "Name of the Kubernetes context to use, defaults to current context" },
"namespace": { "type": "string", "description": "Namespace to list pods from, defaults to all namespaces" },
"context":
{
"type": "string",
"description": "Name of the Kubernetes context to use, defaults to current context",
},
"namespace":
{
"type": "string",
"description": "Namespace to list pods from, defaults to all namespaces",
},
},
},
},
Expand Down
32 changes: 32 additions & 0 deletions testdata/with_k3d/list_k8s_nodes_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
case: List nodes using tool

in:
{
"jsonrpc": "2.0",
"method": "tools/call",
"id": 2,
"params":
{
"name": "list-k8s-nodes",
"arguments":
{ "context": "k3d-mcp-k8s-integration-test" },
},
}
out:
{
"jsonrpc": "2.0",
"id": 2,
"result":
{
"content":
[
{
"type": "text",
"text": !!ere '{"name":"k3d-mcp-k8s-integration-test-server-0","status":"Ready","age":"/[0-9sm]{2,4}/","created_at":"/.+/"}',
# ^ this is a pattern, this ^ too
# this just to match a duration // and this is for timestamp
}
],
"isError": false,
},
}

0 comments on commit 2e03d93

Please sign in to comment.