From 2b46da42a9e028bb270b270790e6f9660ecd262f Mon Sep 17 00:00:00 2001 From: iamsudip Date: Sat, 20 Sep 2025 14:24:51 +0530 Subject: [PATCH 1/3] feat(pods): add tailLines parameter to pod logs retrieval with default 256 lines Signed-off-by: iamsudip --- README.md | 1 + pkg/kubernetes/pods.go | 20 +++++++++++++++----- pkg/mcp/pods_test.go | 35 +++++++++++++++++++++++++++++++++++ pkg/toolsets/core/pods.go | 25 ++++++++++++++++++++++++- 4 files changed, 75 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index cea29431..b046e40b 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) + - `tailLines` (`number`) - Number of lines to retrieve from the end of the logs (Optional, default: 256) - **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..4d9a30ce 100644 --- a/pkg/kubernetes/pods.go +++ b/pkg/kubernetes/pods.go @@ -92,17 +92,27 @@ 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, tailLines 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 tailLines > 0 { + logOptions.TailLines = &tailLines + } else { + // Default to 256 lines when not specified + defaultLines := int64(256) + logOptions.TailLines = &defaultLines + } + + 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..7e30b27f 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 tailLines parameter + podsTailLines, err := c.callTool("pods_log", map[string]interface{}{ + "namespace": "ns-1", + "name": "a-pod-in-ns-1", + "tailLines": 100, + }) + t.Run("pods_log with tailLines=100 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 tailLines parameter + podsInvalidTailLines, _ := c.callTool("pods_log", map[string]interface{}{ + "namespace": "ns-1", + "name": "a-pod-in-ns-1", + "tailLines": "invalid", + }) + t.Run("pods_log with invalid tailLines returns error", func(t *testing.T) { + if !podsInvalidTailLines.IsError { + t.Fatalf("call tool should fail") + return + } + expectedErrorMsg := "failed to parse tailLines 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/toolsets/core/pods.go b/pkg/toolsets/core/pods.go index 5b6645ee..4ecb7f33 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)", }, + "tailLines": { + Type: "integer", + Description: "Number of lines to retrieve from the end of the logs (Optional, default: 256)", + Default: api.ToRawMessage(int64(256)), + 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 + tailLines := params.GetArguments()["tailLines"] + var tailLinesInt int64 + if tailLines != nil { + // Convert to int64 - safely handle both float64 (JSON number) and int types + switch v := tailLines.(type) { + case float64: + tailLinesInt = int64(v) + case int: + tailLinesInt = int64(v) + case int64: + tailLinesInt = v + default: + return api.NewToolCallResult("", fmt.Errorf("failed to parse tailLines parameter: expected integer, got %T", tailLines)), nil + } + } + + ret, err := params.PodsLog(params.Context, ns.(string), name.(string), container.(string), previousBool, tailLinesInt) 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 == "" { From aa0fd85607caafb5e8241f2da75687263b596b5b Mon Sep 17 00:00:00 2001 From: iamsudip Date: Tue, 23 Sep 2025 23:58:01 +0530 Subject: [PATCH 2/3] address review comments Signed-off-by: iamsudip --- README.md | 2 +- pkg/kubernetes/pods.go | 15 +++++++++------ pkg/mcp/pods_test.go | 14 +++++++------- pkg/toolsets/core/pods.go | 24 ++++++++++++------------ 4 files changed, 29 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index b046e40b..5ac9d2fb 100644 --- a/README.md +++ b/README.md @@ -261,7 +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) - - `tailLines` (`number`) - Number of lines to retrieve from the end of the logs (Optional, default: 256) + - `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 4d9a30ce..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,7 +96,7 @@ 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, tailLines int64) (string, error) { +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 @@ -104,12 +108,11 @@ func (k *Kubernetes) PodsLog(ctx context.Context, namespace, name, container str } // Only set tailLines if a value is provided (non-zero) - if tailLines > 0 { - logOptions.TailLines = &tailLines + if tail > 0 { + logOptions.TailLines = &tail } else { - // Default to 256 lines when not specified - defaultLines := int64(256) - logOptions.TailLines = &defaultLines + // Default to DefaultTailLines lines when not specified + logOptions.TailLines = ptr.To(DefaultTailLines) } req := pods.GetLogs(name, logOptions) diff --git a/pkg/mcp/pods_test.go b/pkg/mcp/pods_test.go index 7e30b27f..cfa20dcb 100644 --- a/pkg/mcp/pods_test.go +++ b/pkg/mcp/pods_test.go @@ -757,13 +757,13 @@ func TestPodsLog(t *testing.T) { } }) - // Test with tailLines parameter + // Test with tail parameter podsTailLines, err := c.callTool("pods_log", map[string]interface{}{ "namespace": "ns-1", "name": "a-pod-in-ns-1", - "tailLines": 100, + "tail": 50, }) - t.Run("pods_log with tailLines=100 returns pod log", func(t *testing.T) { + 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 @@ -774,18 +774,18 @@ func TestPodsLog(t *testing.T) { } }) - // Test with invalid tailLines parameter + // Test with invalid tail parameter podsInvalidTailLines, _ := c.callTool("pods_log", map[string]interface{}{ "namespace": "ns-1", "name": "a-pod-in-ns-1", - "tailLines": "invalid", + "tail": "invalid", }) - t.Run("pods_log with invalid tailLines returns error", func(t *testing.T) { + 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 tailLines parameter: expected integer" + 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/toolsets/core/pods.go b/pkg/toolsets/core/pods.go index 4ecb7f33..8744a974 100644 --- a/pkg/toolsets/core/pods.go +++ b/pkg/toolsets/core/pods.go @@ -201,10 +201,10 @@ func initPods() []api.ServerTool { Type: "string", Description: "Name of the Pod container to get the logs from (Optional)", }, - "tailLines": { + "tail": { Type: "integer", - Description: "Number of lines to retrieve from the end of the logs (Optional, default: 256)", - Default: api.ToRawMessage(int64(256)), + 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": { @@ -403,23 +403,23 @@ func podsLog(params api.ToolHandlerParams) (*api.ToolCallResult, error) { previousBool = previous.(bool) } // Extract tailLines parameter - tailLines := params.GetArguments()["tailLines"] - var tailLinesInt int64 - if tailLines != nil { + tail := params.GetArguments()["tail"] + var tailInt int64 + if tail != nil { // Convert to int64 - safely handle both float64 (JSON number) and int types - switch v := tailLines.(type) { + switch v := tail.(type) { case float64: - tailLinesInt = int64(v) + tailInt = int64(v) case int: - tailLinesInt = int64(v) + tailInt = int64(v) case int64: - tailLinesInt = v + tailInt = v default: - return api.NewToolCallResult("", fmt.Errorf("failed to parse tailLines parameter: expected integer, got %T", tailLines)), nil + 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, tailLinesInt) + 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 == "" { From 1694b342397e8444466701f02e321e7cc8b859ee Mon Sep 17 00:00:00 2001 From: Marc Nuri Date: Thu, 25 Sep 2025 09:11:16 +0200 Subject: [PATCH 3/3] test(pods): add tailLines parameter to pod logs retrieval with default 256 lines Signed-off-by: Marc Nuri --- pkg/mcp/testdata/toolsets-core-tools.json | 6 ++++++ pkg/mcp/testdata/toolsets-full-tools-openshift.json | 6 ++++++ pkg/mcp/testdata/toolsets-full-tools.json | 6 ++++++ 3 files changed, 18 insertions(+) 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": [