Skip to content

feat(pods): pods_exec supports specifying container #65

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 26, 2025
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ Execute a command in a Kubernetes Pod in the current or provided namespace with
- Name of the Pod
- `namespace` (string, required)
- Namespace of the Pod
- `container` (`string`, optional)
- Name of the Pod container to get logs from

### `pods_get`

Expand Down
13 changes: 7 additions & 6 deletions pkg/kubernetes/pods.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,18 +184,19 @@ func (k *Kubernetes) PodsExec(ctx context.Context, namespace, name, container st
if pod.Status.Phase == v1.PodSucceeded || pod.Status.Phase == v1.PodFailed {
return "", fmt.Errorf("cannot exec into a container in a completed pod; current phase is %s", pod.Status.Phase)
}
if container == "" {
container = pod.Spec.Containers[0].Name
}
podExecOptions := &v1.PodExecOptions{
Command: command,
Stdout: true,
Stderr: true,
Container: container,
Command: command,
Stdout: true,
Stderr: true,
}
executor, err := k.createExecutor(namespace, name, podExecOptions)
if err != nil {
return "", err
}
if container == "" {
container = pod.Spec.Containers[0].Name
}
stdout := bytes.NewBuffer(make([]byte, 0))
stderr := bytes.NewBuffer(make([]byte, 0))
if err = executor.StreamWithContext(ctx, remotecommand.StreamOptions{
Expand Down
11 changes: 8 additions & 3 deletions pkg/mcp/pods.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ func (s *Server) initPods() []server.ServerTool {
), s.podsDelete},
{mcp.NewTool("pods_exec",
mcp.WithDescription("Execute a command in a Kubernetes Pod in the current or provided namespace with the provided name and command"),
mcp.WithString("namespace", mcp.Description("Namespace to get the Pod logs from")),
mcp.WithString("name", mcp.Description("Name of the Pod to get the logs from"), mcp.Required()),
mcp.WithString("namespace", mcp.Description("Namespace of the Pod where the command will be executed")),
mcp.WithString("name", mcp.Description("Name of the Pod where the command will be executed"), mcp.Required()),
mcp.WithArray("command", mcp.Description("Command to execute in the Pod container. "+
"The first item is the command to be run, and the rest are the arguments to that command. "+
`Example: ["ls", "-l", "/tmp"]`),
Expand All @@ -44,6 +44,7 @@ func (s *Server) initPods() []server.ServerTool {
},
mcp.Required(),
),
mcp.WithString("container", mcp.Description("Name of the Pod container where the command will be executed (Optional)")),
), s.podsExec},
{mcp.NewTool("pods_log",
mcp.WithDescription("Get the logs of a Kubernetes Pod in the current or provided namespace with the provided name"),
Expand Down Expand Up @@ -122,6 +123,10 @@ func (s *Server) podsExec(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.Ca
if name == nil {
return NewTextResult("", errors.New("failed to exec in pod, missing argument name")), nil
}
container := ctr.Params.Arguments["container"]
if container == nil {
container = ""
}
commandArg := ctr.Params.Arguments["command"]
command := make([]string, 0)
if _, ok := commandArg.([]interface{}); ok {
Expand All @@ -133,7 +138,7 @@ func (s *Server) podsExec(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.Ca
} else {
return NewTextResult("", errors.New("failed to exec in pod, invalid command argument")), nil
}
ret, err := s.k.PodsExec(ctx, ns.(string), name.(string), "", command)
ret, err := s.k.PodsExec(ctx, ns.(string), name.(string), container.(string), command)
if err != nil {
return NewTextResult("", fmt.Errorf("failed to exec in pod %s in namespace %s: %v", name, ns, err)), nil
} else if ret == "" {
Expand Down
51 changes: 43 additions & 8 deletions pkg/mcp/pods_exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ func TestPodsExec(t *testing.T) {
_, _ = w.Write([]byte(err.Error()))
return
}
defer ctx.conn.Close()
_, _ = io.WriteString(ctx.stdoutStream, strings.Join(req.URL.Query()["command"], " "))
_, _ = io.WriteString(ctx.stdoutStream, "\ntotal 0\n")
defer func(conn io.Closer) { _ = conn.Close() }(ctx.conn)
_, _ = io.WriteString(ctx.stdoutStream, "command:"+strings.Join(req.URL.Query()["command"], " ")+"\n")
_, _ = io.WriteString(ctx.stdoutStream, "container:"+strings.Join(req.URL.Query()["container"], " ")+"\n")
}))
mockServer.Handle(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if req.URL.Path != "/api/v1/namespaces/default/pods/pod-to-exec" {
Expand All @@ -46,20 +46,55 @@ func TestPodsExec(t *testing.T) {
Spec: v1.PodSpec{Containers: []v1.Container{{Name: "container-to-exec"}}},
})
}))
toolResult, err := c.callTool("pods_exec", map[string]interface{}{
podsExecNilNamespace, err := c.callTool("pods_exec", map[string]interface{}{
"name": "pod-to-exec",
"command": []interface{}{"ls", "-l"},
})
t.Run("pods_exec with name and nil namespace returns command output", func(t *testing.T) {
if err != nil {
t.Fatalf("call tool failed %v", err)
}
if podsExecNilNamespace.IsError {
t.Fatalf("call tool failed")
}
if !strings.Contains(podsExecNilNamespace.Content[0].(mcp.TextContent).Text, "command:ls -l\n") {
t.Errorf("unexpected result %v", podsExecNilNamespace.Content[0].(mcp.TextContent).Text)
}
})
podsExecInNamespace, err := c.callTool("pods_exec", map[string]interface{}{
"namespace": "default",
"name": "pod-to-exec",
"command": []interface{}{"ls", "-l"},
})
t.Run("pods_exec with name and namespace returns command output", func(t *testing.T) {
if err != nil {
t.Fatalf("call tool failed %v", err)
}
if podsExecInNamespace.IsError {
t.Fatalf("call tool failed")
}
if !strings.Contains(podsExecNilNamespace.Content[0].(mcp.TextContent).Text, "command:ls -l\n") {
t.Errorf("unexpected result %v", podsExecInNamespace.Content[0].(mcp.TextContent).Text)
}
})
podsExecInNamespaceAndContainer, err := c.callTool("pods_exec", map[string]interface{}{
"namespace": "default",
"name": "pod-to-exec",
"command": []interface{}{"ls", "-l"},
"container": "a-specific-container",
})
t.Run("pods_exec returns command output", func(t *testing.T) {
t.Run("pods_exec with name, namespace, and container returns command output", func(t *testing.T) {
if err != nil {
t.Fatalf("call tool failed %v", err)
}
if toolResult.IsError {
if podsExecInNamespaceAndContainer.IsError {
t.Fatalf("call tool failed")
}
if toolResult.Content[0].(mcp.TextContent).Text != "ls -l\ntotal 0\n" {
t.Errorf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text)
if !strings.Contains(podsExecInNamespaceAndContainer.Content[0].(mcp.TextContent).Text, "command:ls -l\n") {
t.Errorf("unexpected result %v", podsExecInNamespaceAndContainer.Content[0].(mcp.TextContent).Text)
}
if !strings.Contains(podsExecInNamespaceAndContainer.Content[0].(mcp.TextContent).Text, "container:a-specific-container\n") {
t.Errorf("expected container name not found %v", podsExecInNamespaceAndContainer.Content[0].(mcp.TextContent).Text)
}
})

Expand Down
Loading