Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ The following sets of tools are available (all on by default):
- `name` (`string`) **(required)** - Name of the Pod to get the logs from
- `namespace` (`string`) - Namespace to get the Pod logs from
- `previous` (`boolean`) - Return previous terminated container logs (Optional)
- `tail` (`number`) - Number of lines to retrieve from the end of the logs (Optional, default: 100)

- **pods_run** - Run a Kubernetes Pod in the current or provided namespace with the provided container image and optional name
- `image` (`string`) **(required)** - Container Image to run in the Pod
Expand Down
23 changes: 18 additions & 5 deletions pkg/kubernetes/pods.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,14 @@ import (
"k8s.io/client-go/tools/remotecommand"
"k8s.io/metrics/pkg/apis/metrics"
metricsv1beta1api "k8s.io/metrics/pkg/apis/metrics/v1beta1"
"k8s.io/utils/ptr"

"github.com/containers/kubernetes-mcp-server/pkg/version"
)

// Default number of lines to retrieve from the end of the logs
const DefaultTailLines = int64(100)

type PodsTopOptions struct {
metav1.ListOptions
AllNamespaces bool
Expand Down Expand Up @@ -92,17 +96,26 @@ func (k *Kubernetes) PodsDelete(ctx context.Context, namespace, name string) (st
k.ResourcesDelete(ctx, &schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}, namespace, name)
}

func (k *Kubernetes) PodsLog(ctx context.Context, namespace, name, container string, previous bool) (string, error) {
tailLines := int64(256)
func (k *Kubernetes) PodsLog(ctx context.Context, namespace, name, container string, previous bool, tail int64) (string, error) {
pods, err := k.manager.accessControlClientSet.Pods(k.NamespaceOrDefault(namespace))
if err != nil {
return "", err
}
req := pods.GetLogs(name, &v1.PodLogOptions{
TailLines: &tailLines,

logOptions := &v1.PodLogOptions{
Container: container,
Previous: previous,
})
}

// Only set tailLines if a value is provided (non-zero)
if tail > 0 {
logOptions.TailLines = &tail
} else {
// Default to DefaultTailLines lines when not specified
logOptions.TailLines = ptr.To(DefaultTailLines)
}

req := pods.GetLogs(name, logOptions)
res := req.Do(ctx)
if res.Error() != nil {
return "", res.Error()
Expand Down
35 changes: 35 additions & 0 deletions pkg/mcp/pods_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,41 @@ func TestPodsLog(t *testing.T) {
return
}
})

// Test with tail parameter
podsTailLines, err := c.callTool("pods_log", map[string]interface{}{
"namespace": "ns-1",
"name": "a-pod-in-ns-1",
"tail": 50,
})
t.Run("pods_log with tail=50 returns pod log", func(t *testing.T) {
if err != nil {
t.Fatalf("call tool failed %v", err)
return
}
if podsTailLines.IsError {
t.Fatalf("call tool failed")
return
}
})

// Test with invalid tail parameter
podsInvalidTailLines, _ := c.callTool("pods_log", map[string]interface{}{
"namespace": "ns-1",
"name": "a-pod-in-ns-1",
"tail": "invalid",
})
t.Run("pods_log with invalid tail returns error", func(t *testing.T) {
if !podsInvalidTailLines.IsError {
t.Fatalf("call tool should fail")
return
}
expectedErrorMsg := "failed to parse tail parameter: expected integer"
if errMsg := podsInvalidTailLines.Content[0].(mcp.TextContent).Text; !strings.Contains(errMsg, expectedErrorMsg) {
t.Fatalf("unexpected error message, expected to contain '%s', got '%s'", expectedErrorMsg, errMsg)
return
}
})
})
}

Expand Down
6 changes: 6 additions & 0 deletions pkg/mcp/testdata/toolsets-core-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,12 @@
"previous": {
"description": "Return previous terminated container logs (Optional)",
"type": "boolean"
},
"tail": {
"default": 100,
"description": "Number of lines to retrieve from the end of the logs (Optional, default: 100)",
"minimum": 0,
"type": "integer"
}
},
"required": [
Expand Down
6 changes: 6 additions & 0 deletions pkg/mcp/testdata/toolsets-full-tools-openshift.json
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,12 @@
"previous": {
"description": "Return previous terminated container logs (Optional)",
"type": "boolean"
},
"tail": {
"default": 100,
"description": "Number of lines to retrieve from the end of the logs (Optional, default: 100)",
"minimum": 0,
"type": "integer"
}
},
"required": [
Expand Down
6 changes: 6 additions & 0 deletions pkg/mcp/testdata/toolsets-full-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,12 @@
"previous": {
"description": "Return previous terminated container logs (Optional)",
"type": "boolean"
},
"tail": {
"default": 100,
"description": "Number of lines to retrieve from the end of the logs (Optional, default: 100)",
"minimum": 0,
"type": "integer"
}
},
"required": [
Expand Down
25 changes: 24 additions & 1 deletion pkg/toolsets/core/pods.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,12 @@ func initPods() []api.ServerTool {
Type: "string",
Description: "Name of the Pod container to get the logs from (Optional)",
},
"tail": {
Type: "integer",
Description: "Number of lines to retrieve from the end of the logs (Optional, default: 100)",
Default: api.ToRawMessage(kubernetes.DefaultTailLines),
Minimum: ptr.To(float64(0)),
},
"previous": {
Type: "boolean",
Description: "Return previous terminated container logs (Optional)",
Expand Down Expand Up @@ -396,7 +402,24 @@ func podsLog(params api.ToolHandlerParams) (*api.ToolCallResult, error) {
if previous != nil {
previousBool = previous.(bool)
}
ret, err := params.PodsLog(params, ns.(string), name.(string), container.(string), previousBool)
// Extract tailLines parameter
tail := params.GetArguments()["tail"]
var tailInt int64
if tail != nil {
// Convert to int64 - safely handle both float64 (JSON number) and int types
switch v := tail.(type) {
case float64:
tailInt = int64(v)
case int:
tailInt = int64(v)
case int64:
tailInt = v
default:
return api.NewToolCallResult("", fmt.Errorf("failed to parse tail parameter: expected integer, got %T", tail)), nil
}
}

ret, err := params.PodsLog(params.Context, ns.(string), name.(string), container.(string), previousBool, tailInt)
if err != nil {
return api.NewToolCallResult("", fmt.Errorf("failed to get pod %s log in namespace %s: %v", name, ns, err)), nil
} else if ret == "" {
Expand Down
Loading