Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
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