diff --git a/README.md b/README.md index cea29431..5ac9d2fb 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/pkg/kubernetes/pods.go b/pkg/kubernetes/pods.go index 180eb720..4d333ea8 100644 --- a/pkg/kubernetes/pods.go +++ b/pkg/kubernetes/pods.go @@ -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 @@ -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() diff --git a/pkg/mcp/pods_test.go b/pkg/mcp/pods_test.go index a83e44dd..cfa20dcb 100644 --- a/pkg/mcp/pods_test.go +++ b/pkg/mcp/pods_test.go @@ -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 + } + }) }) } diff --git a/pkg/mcp/testdata/toolsets-core-tools.json b/pkg/mcp/testdata/toolsets-core-tools.json index 62f0c348..43680dae 100644 --- a/pkg/mcp/testdata/toolsets-core-tools.json +++ b/pkg/mcp/testdata/toolsets-core-tools.json @@ -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": [ diff --git a/pkg/mcp/testdata/toolsets-full-tools-openshift.json b/pkg/mcp/testdata/toolsets-full-tools-openshift.json index dae607c8..b5018945 100644 --- a/pkg/mcp/testdata/toolsets-full-tools-openshift.json +++ b/pkg/mcp/testdata/toolsets-full-tools-openshift.json @@ -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": [ diff --git a/pkg/mcp/testdata/toolsets-full-tools.json b/pkg/mcp/testdata/toolsets-full-tools.json index a6065395..7b9f471d 100644 --- a/pkg/mcp/testdata/toolsets-full-tools.json +++ b/pkg/mcp/testdata/toolsets-full-tools.json @@ -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": [ diff --git a/pkg/toolsets/core/pods.go b/pkg/toolsets/core/pods.go index 5b6645ee..8744a974 100644 --- a/pkg/toolsets/core/pods.go +++ b/pkg/toolsets/core/pods.go @@ -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)", @@ -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 == "" {