#
tokens: 20107/50000 37/37 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .github
│   ├── dependabot.yml
│   └── workflows
│       ├── build.yaml
│       └── release.yaml
├── .gitignore
├── cmd
│   └── podman-mcp-server
│       ├── main_test.go
│       └── main.go
├── go.mod
├── go.sum
├── LICENSE
├── Makefile
├── npm
│   ├── podman-mcp-server
│   │   ├── bin
│   │   │   └── index.js
│   │   └── package.json
│   ├── podman-mcp-server-darwin-amd64
│   │   └── package.json
│   ├── podman-mcp-server-darwin-arm64
│   │   └── package.json
│   ├── podman-mcp-server-linux-amd64
│   │   └── package.json
│   ├── podman-mcp-server-linux-arm64
│   │   └── package.json
│   ├── podman-mcp-server-windows-amd64
│   │   └── package.json
│   └── podman-mcp-server-windows-arm64
│       └── package.json
├── pkg
│   ├── mcp
│   │   ├── common_test.go
│   │   ├── mcp_test.go
│   │   ├── mcp.go
│   │   ├── podman_container_test.go
│   │   ├── podman_container.go
│   │   ├── podman_image_test.go
│   │   ├── podman_image.go
│   │   ├── podman_network_test.go
│   │   ├── podman_network.go
│   │   ├── podman_volume_test.go
│   │   └── podman_volume.go
│   ├── podman
│   │   ├── interface.go
│   │   └── podman_cli.go
│   ├── podman-mcp-server
│   │   └── cmd
│   │       ├── root_test.go
│   │       └── root.go
│   └── version
│       └── version.go
├── python
│   ├── podman_mcp_server
│   │   ├── __init__.py
│   │   ├── __main__.py
│   │   └── podman_mcp_server.py
│   ├── pyproject.toml
│   └── README.md
├── README.md
└── testdata
    └── podman
        └── main.go
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | .idea/
 2 | .docusaurus/
 3 | node_modules/
 4 | 
 5 | .npmrc
 6 | /podman-mcp-server
 7 | npm/podman-mcp-server/README.md
 8 | npm/podman-mcp-server/LICENSE
 9 | !npm/podman-mcp-server
10 | podman-mcp-server-darwin-amd64
11 | !npm/podman-mcp-server-darwin-amd64/
12 | podman-mcp-server-darwin-arm64
13 | !npm/podman-mcp-server-darwin-arm64
14 | podman-mcp-server-linux-amd64
15 | !npm/podman-mcp-server-linux-amd64
16 | podman-mcp-server-linux-arm64
17 | !npm/podman-mcp-server-linux-arm64
18 | podman-mcp-server-windows-amd64.exe
19 | podman-mcp-server-windows-arm64.exe
20 | 
21 | python/.venv/
22 | python/build/
23 | python/dist/
24 | python/podman-mcp-server.egg-info/
25 | !python/podman-mcp-server
26 | 
```

--------------------------------------------------------------------------------
/python/README.md:
--------------------------------------------------------------------------------

```markdown
1 | ../README.md
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Podman MCP Server
  2 | 
  3 | [![GitHub License](https://img.shields.io/github/license/manusa/podman-mcp-server)](https://github.com/manusa/podman-mcp-server/blob/main/LICENSE)
  4 | [![npm](https://img.shields.io/npm/v/podman-mcp-server)](https://www.npmjs.com/package/podman-mcp-server)
  5 | [![PyPI - Version](https://img.shields.io/pypi/v/podman-mcp-server)](https://pypi.org/project/podman-mcp-server/)
  6 | [![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/manusa/podman-mcp-server?sort=semver)](https://github.com/manusa/podman-mcp-server/releases/latest)
  7 | [![Build](https://github.com/manusa/podman-mcp-server/actions/workflows/build.yaml/badge.svg)](https://github.com/manusa/podman-mcp-server/actions/workflows/build.yaml)
  8 | 
  9 | [✨ Features](#features) | [🚀 Getting Started](#getting-started) | [🎥 Demos](#demos) | [⚙️ Configuration](#configuration) | [🧑‍💻 Development](#development)
 10 | 
 11 | ## ✨ Features <a id="features"></a>
 12 | 
 13 | A powerful and flexible MCP server for container runtimes supporting Podman and Docker.
 14 | 
 15 | 
 16 | ## 🚀 Getting Started <a id="getting-started"></a>
 17 | 
 18 | ### Claude Desktop
 19 | 
 20 | #### Using npx
 21 | 
 22 | If you have npm installed, this is the fastest way to get started with `podman-mcp-server` on Claude Desktop.
 23 | 
 24 | Open your `claude_desktop_config.json` and add the mcp server to the list of `mcpServers`:
 25 | ``` json
 26 | {
 27 |   "mcpServers": {
 28 |     "podman": {
 29 |       "command": "npx",
 30 |       "args": [
 31 |         "-y",
 32 |         "podman-mcp-server@latest"
 33 |       ]
 34 |     }
 35 |   }
 36 | }
 37 | ```
 38 | 
 39 | ### VS Code / VS Code Insiders
 40 | 
 41 | Install the Podman MCP server extension in VS Code Insiders by pressing the following link:
 42 | 
 43 | [<img src="https://img.shields.io/badge/VS_Code-VS_Code?style=flat-square&label=Install%20Server&color=0098FF" alt="Install in VS Code">](https://insiders.vscode.dev/redirect?url=vscode%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522podman%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522-y%2522%252C%2522podman-mcp-server%2540latest%2522%255D%257D)
 44 | [<img alt="Install in VS Code Insiders" src="https://img.shields.io/badge/VS_Code_Insiders-VS_Code_Insiders?style=flat-square&label=Install%20Server&color=24bfa5">](https://insiders.vscode.dev/redirect?url=vscode-insiders%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522podman%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522-y%2522%252C%2522podman-mcp-server%2540latest%2522%255D%257D)
 45 | 
 46 | Alternatively, you can install the extension manually by running the following command:
 47 | 
 48 | ```shell
 49 | # For VS Code
 50 | code --add-mcp '{"name":"podman","command":"npx","args":["podman-mcp-server@latest"]}'
 51 | # For VS Code Insiders
 52 | code-insiders --add-mcp '{"name":"podman","command":"npx","args":["podman-mcp-server@latest"]}'
 53 | ```
 54 | 
 55 | ### Goose CLI
 56 | 
 57 | [Goose CLI](https://blog.marcnuri.com/goose-on-machine-ai-agent-cli-introduction) is the easiest (and cheapest) way to get rolling with artificial intelligence (AI) agents.
 58 | 
 59 | #### Using npm
 60 | 
 61 | If you have npm installed, this is the fastest way to get started with `podman-mcp-server`.
 62 | 
 63 | Open your goose `config.yaml` and add the mcp server to the list of `mcpServers`:
 64 | ```yaml
 65 | extensions:
 66 |   podman:
 67 |     command: npx
 68 |     args:
 69 |       - -y
 70 |       - podman-mcp-server@latest
 71 | 
 72 | ```
 73 | 
 74 | ## 🎥 Demos <a id="demos"></a>
 75 | 
 76 | ## ⚙️ Configuration <a id="configuration"></a>
 77 | 
 78 | The Podman MCP server can be configured using command line (CLI) arguments.
 79 | 
 80 | You can run the CLI executable either by using `npx` or by downloading the [latest release binary](https://github.com/manusa/podman-mcp-server/releases/latest).
 81 | 
 82 | ```shell
 83 | # Run the Podman MCP server using npx (in case you have npm installed)
 84 | npx podman-mcp-server@latest --help
 85 | ```
 86 | 
 87 | ```shell
 88 | # Run the Podman MCP server using the latest release binary
 89 | ./podman-mcp-server --help
 90 | ```
 91 | 
 92 | ### Configuration Options
 93 | 
 94 | | Option       | Description                                                                              |
 95 | |--------------|------------------------------------------------------------------------------------------|
 96 | | `--sse-port` | Starts the MCP server in Server-Sent Event (SSE) mode and listens on the specified port. |
 97 | 
 98 | ## 🧑‍💻 Development <a id="development"></a>
 99 | 
100 | ### Running with mcp-inspector
101 | 
102 | Compile the project and run the Podman MCP server with [mcp-inspector](https://modelcontextprotocol.io/docs/tools/inspector) to inspect the MCP server.
103 | 
104 | ```shell
105 | # Compile the project
106 | make build
107 | # Run the Podman MCP server with mcp-inspector
108 | npx @modelcontextprotocol/inspector@latest $(pwd)/podman-mcp-server
109 | ```
110 | 
```

--------------------------------------------------------------------------------
/python/podman_mcp_server/__main__.py:
--------------------------------------------------------------------------------

```python
1 | from .podman_mcp_server import main
2 | 
3 | if __name__ == "__main__":
4 |     main()
5 | 
```

--------------------------------------------------------------------------------
/cmd/podman-mcp-server/main.go:
--------------------------------------------------------------------------------

```go
1 | package main
2 | 
3 | import "github.com/manusa/podman-mcp-server/pkg/podman-mcp-server/cmd"
4 | 
5 | func main() {
6 | 	cmd.Execute()
7 | }
8 | 
```

--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------

```yaml
1 | version: 2
2 | updates:
3 |   - package-ecosystem: "gomod"
4 |     directory: "/"
5 |     schedule:
6 |       interval: "daily"
7 |     open-pull-requests-limit: 10
8 | 
```

--------------------------------------------------------------------------------
/pkg/version/version.go:
--------------------------------------------------------------------------------

```go
1 | package version
2 | 
3 | var CommitHash = "unknown"
4 | var BuildTime = "1970-01-01T00:00:00Z"
5 | var Version = "0.0.0"
6 | var BinaryName = "podman-mcp-server"
7 | 
```

--------------------------------------------------------------------------------
/python/podman_mcp_server/__init__.py:
--------------------------------------------------------------------------------

```python
1 | """
2 | Model Context Protocol (MCP) server for container runtimes (Podman and Docker)
3 | """
4 | from .podman_mcp_server import main
5 | 
6 | __all__ = ['main']
7 | 
8 | 
```

--------------------------------------------------------------------------------
/cmd/podman-mcp-server/main_test.go:
--------------------------------------------------------------------------------

```go
 1 | package main
 2 | 
 3 | import (
 4 | 	"os"
 5 | )
 6 | 
 7 | func Example_version() {
 8 | 	oldArgs := os.Args
 9 | 	defer func() { os.Args = oldArgs }()
10 | 	os.Args = []string{"podman-mcp-server", "--version"}
11 | 	main()
12 | 	// Output: 0.0.0
13 | }
14 | 
```

--------------------------------------------------------------------------------
/npm/podman-mcp-server-linux-amd64/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "podman-mcp-server-linux-amd64",
 3 |   "version": "0.0.0",
 4 |   "description": "Model Context Protocol (MCP) server for container runtimes (Podman and Docker) ",
 5 |   "os": [
 6 |     "linux"
 7 |   ],
 8 |   "cpu": [
 9 |     "x64"
10 |   ]
11 | }
12 | 
```

--------------------------------------------------------------------------------
/npm/podman-mcp-server-darwin-amd64/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "podman-mcp-server-darwin-amd64",
 3 |   "version": "0.0.0",
 4 |   "description": "Model Context Protocol (MCP) server for container runtimes (Podman and Docker) ",
 5 |   "os": [
 6 |     "darwin"
 7 |   ],
 8 |   "cpu": [
 9 |     "x64"
10 |   ]
11 | }
12 | 
```

--------------------------------------------------------------------------------
/npm/podman-mcp-server-linux-arm64/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "podman-mcp-server-linux-arm64",
 3 |   "version": "0.0.0",
 4 |   "description": "Model Context Protocol (MCP) server for container runtimes (Podman and Docker) ",
 5 |   "os": [
 6 |     "linux"
 7 |   ],
 8 |   "cpu": [
 9 |     "arm64"
10 |   ]
11 | }
12 | 
```

--------------------------------------------------------------------------------
/npm/podman-mcp-server-windows-amd64/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "podman-mcp-server-windows-amd64",
 3 |   "version": "0.0.0",
 4 |   "description": "Model Context Protocol (MCP) server for container runtimes (Podman and Docker) ",
 5 |   "os": [
 6 |     "win32"
 7 |   ],
 8 |   "cpu": [
 9 |     "x64"
10 |   ]
11 | }
12 | 
```

--------------------------------------------------------------------------------
/npm/podman-mcp-server-darwin-arm64/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "podman-mcp-server-darwin-arm64",
 3 |   "version": "0.0.0",
 4 |   "description": "Model Context Protocol (MCP) server for container runtimes (Podman and Docker) ",
 5 |   "os": [
 6 |     "darwin"
 7 |   ],
 8 |   "cpu": [
 9 |     "arm64"
10 |   ]
11 | }
12 | 
```

--------------------------------------------------------------------------------
/npm/podman-mcp-server-windows-arm64/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "podman-mcp-server-windows-arm64",
 3 |   "version": "0.0.0",
 4 |   "description": "Model Context Protocol (MCP) server for container runtimes (Podman and Docker) ",
 5 |   "os": [
 6 |     "win32"
 7 |   ],
 8 |   "cpu": [
 9 |     "arm64"
10 |   ]
11 | }
12 | 
```

--------------------------------------------------------------------------------
/testdata/podman/main.go:
--------------------------------------------------------------------------------

```go
 1 | // Fake podman CLI binary
 2 | package main
 3 | 
 4 | import (
 5 | 	"os"
 6 | 	"path"
 7 | 	"path/filepath"
 8 | )
 9 | 
10 | func main() {
11 | 	print("podman")
12 | 	for _, arg := range os.Args[1:] {
13 | 		print(" " + arg)
14 | 	}
15 | 	println()
16 | 	ex, err := os.Executable()
17 | 	if err != nil {
18 | 		panic(err)
19 | 	}
20 | 	outputTxt := path.Join(filepath.Dir(ex), "output.txt")
21 | 	_, err = os.Stat(outputTxt)
22 | 	if err == nil {
23 | 		data, _ := os.ReadFile(outputTxt)
24 | 		_, _ = os.Stdout.Write(data)
25 | 	}
26 | 	os.Exit(0)
27 | }
28 | 
```

--------------------------------------------------------------------------------
/pkg/mcp/podman_volume.go:
--------------------------------------------------------------------------------

```go
 1 | package mcp
 2 | 
 3 | import (
 4 | 	"context"
 5 | 	"github.com/mark3labs/mcp-go/mcp"
 6 | 	"github.com/mark3labs/mcp-go/server"
 7 | )
 8 | 
 9 | func (s *Server) initPodmanVolume() []server.ServerTool {
10 | 	return []server.ServerTool{
11 | 		{mcp.NewTool("volume_list",
12 | 			mcp.WithDescription("List all the available Docker or Podman volumes"),
13 | 		), s.volumeList},
14 | 	}
15 | }
16 | 
17 | func (s *Server) volumeList(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
18 | 	return NewTextResult(s.podman.VolumeList()), nil
19 | }
20 | 
```

--------------------------------------------------------------------------------
/pkg/mcp/podman_network.go:
--------------------------------------------------------------------------------

```go
 1 | package mcp
 2 | 
 3 | import (
 4 | 	"context"
 5 | 	"github.com/mark3labs/mcp-go/mcp"
 6 | 	"github.com/mark3labs/mcp-go/server"
 7 | )
 8 | 
 9 | func (s *Server) initPodmanNetwork() []server.ServerTool {
10 | 	return []server.ServerTool{
11 | 		{mcp.NewTool("network_list",
12 | 			mcp.WithDescription("List all the available Docker or Podman networks"),
13 | 		), s.networkList},
14 | 	}
15 | }
16 | 
17 | func (s *Server) networkList(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
18 | 	return NewTextResult(s.podman.NetworkList()), nil
19 | }
20 | 
```

--------------------------------------------------------------------------------
/pkg/podman-mcp-server/cmd/root_test.go:
--------------------------------------------------------------------------------

```go
 1 | package cmd
 2 | 
 3 | import (
 4 | 	"io"
 5 | 	"os"
 6 | 	"testing"
 7 | )
 8 | 
 9 | func captureOutput(f func() error) (string, error) {
10 | 	originalOut := os.Stdout
11 | 	defer func() {
12 | 		os.Stdout = originalOut
13 | 	}()
14 | 	r, w, _ := os.Pipe()
15 | 	os.Stdout = w
16 | 	err := f()
17 | 	_ = w.Close()
18 | 	out, _ := io.ReadAll(r)
19 | 	return string(out), err
20 | }
21 | 
22 | func TestVersion(t *testing.T) {
23 | 	rootCmd.SetArgs([]string{"--version"})
24 | 	version, err := captureOutput(rootCmd.Execute)
25 | 	if version != "0.0.0\n" {
26 | 		t.Fatalf("Expected version 0.0.0, got %s %v", version, err)
27 | 		return
28 | 	}
29 | }
30 | 
```

--------------------------------------------------------------------------------
/pkg/mcp/podman_volume_test.go:
--------------------------------------------------------------------------------

```go
 1 | package mcp
 2 | 
 3 | import (
 4 | 	"github.com/mark3labs/mcp-go/mcp"
 5 | 	"strings"
 6 | 	"testing"
 7 | )
 8 | 
 9 | func TestVolumeList(t *testing.T) {
10 | 	testCase(t, func(c *mcpContext) {
11 | 		toolResult, err := c.callTool("volume_list", map[string]interface{}{})
12 | 		t.Run("volume_list returns OK", func(t *testing.T) {
13 | 			if err != nil {
14 | 				t.Fatalf("call tool failed %v", err)
15 | 			}
16 | 			if toolResult.IsError {
17 | 				t.Fatalf("call tool failed")
18 | 			}
19 | 		})
20 | 		t.Run("volume_list lists all available volumes", func(t *testing.T) {
21 | 			if !strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "podman volume ls") {
22 | 				t.Fatalf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text)
23 | 			}
24 | 		})
25 | 	})
26 | }
27 | 
```

--------------------------------------------------------------------------------
/pkg/mcp/podman_network_test.go:
--------------------------------------------------------------------------------

```go
 1 | package mcp
 2 | 
 3 | import (
 4 | 	"github.com/mark3labs/mcp-go/mcp"
 5 | 	"strings"
 6 | 	"testing"
 7 | )
 8 | 
 9 | func TestNetworkList(t *testing.T) {
10 | 	testCase(t, func(c *mcpContext) {
11 | 		toolResult, err := c.callTool("network_list", map[string]interface{}{})
12 | 		t.Run("network_list returns OK", func(t *testing.T) {
13 | 			if err != nil {
14 | 				t.Fatalf("call tool failed %v", err)
15 | 			}
16 | 			if toolResult.IsError {
17 | 				t.Fatalf("call tool failed")
18 | 			}
19 | 		})
20 | 		t.Run("network_list lists all available networks", func(t *testing.T) {
21 | 			if !strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "podman network ls") {
22 | 				t.Fatalf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text)
23 | 			}
24 | 		})
25 | 	})
26 | }
27 | 
```

--------------------------------------------------------------------------------
/python/pyproject.toml:
--------------------------------------------------------------------------------

```toml
 1 | [build-system]
 2 | requires = ["setuptools>=42", "wheel"]
 3 | build-backend = "setuptools.build_meta"
 4 | 
 5 | [project]
 6 | name = "podman-mcp-server"
 7 | version = "0.0.0"
 8 | description = "Model Context Protocol (MCP) server for container runtimes (Podman and Docker)"
 9 | readme = {file="README.md", content-type="text/markdown"}
10 | requires-python = ">=3.6"
11 | license = "Apache-2.0"
12 | authors = [
13 |     { name = "Marc Nuri", email = "[email protected]" }
14 | ]
15 | classifiers = [
16 |     "Programming Language :: Python :: 3",
17 |     "Operating System :: OS Independent",
18 | ]
19 | 
20 | [project.urls]
21 | Homepage = "https://github.com/manusa/podman-mcp-server"
22 | Repository = "https://github.com/manusa/podman-mcp-server"
23 | 
24 | [project.scripts]
25 | podman-mcp-server = "podman_mcp_server:main"
26 | 
```

--------------------------------------------------------------------------------
/pkg/mcp/mcp_test.go:
--------------------------------------------------------------------------------

```go
 1 | package mcp
 2 | 
 3 | import (
 4 | 	"github.com/mark3labs/mcp-go/mcp"
 5 | 	"testing"
 6 | )
 7 | 
 8 | func TestTools(t *testing.T) {
 9 | 	expectedNames := []string{
10 | 		"container_inspect",
11 | 		"container_list",
12 | 		"container_logs",
13 | 		"container_remove",
14 | 		"container_run",
15 | 		"container_stop",
16 | 		"image_build",
17 | 		"image_list",
18 | 		"image_pull",
19 | 		"image_push",
20 | 		"image_remove",
21 | 		"network_list",
22 | 		"volume_list",
23 | 	}
24 | 	testCase(t, func(c *mcpContext) {
25 | 		tools, err := c.mcpClient.ListTools(c.ctx, mcp.ListToolsRequest{})
26 | 		t.Run("ListTools returns tools", func(t *testing.T) {
27 | 			if err != nil {
28 | 				t.Fatalf("call ListTools failed %v", err)
29 | 			}
30 | 		})
31 | 		nameSet := make(map[string]bool)
32 | 		for _, tool := range tools.Tools {
33 | 			nameSet[tool.Name] = true
34 | 		}
35 | 		for _, name := range expectedNames {
36 | 			t.Run("ListTools has "+name+" tool", func(t *testing.T) {
37 | 				if nameSet[name] != true {
38 | 					t.Errorf("tool %s not found", name)
39 | 				}
40 | 			})
41 | 		}
42 | 	})
43 | }
44 | 
```

--------------------------------------------------------------------------------
/.github/workflows/build.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | name: Build
 2 | 
 3 | on:
 4 |   push:
 5 |     branches:
 6 |       - 'main'
 7 |     paths-ignore:
 8 |       - '.gitignore'
 9 |       - 'LICENSE'
10 |       - '*.md'
11 |   pull_request:
12 |     paths-ignore:
13 |       - '.gitignore'
14 |       - 'LICENSE'
15 |       - '*.md'
16 | 
17 | concurrency:
18 |   # Only run once for latest commit per ref and cancel other (previous) runs.
19 |   group: ${{ github.workflow }}-${{ github.ref }}
20 |   cancel-in-progress: true
21 | 
22 | env:
23 |   GO_VERSION: 1.23
24 | 
25 | defaults:
26 |   run:
27 |     shell: bash
28 | 
29 | jobs:
30 |   build:
31 |     name: Build on ${{ matrix.os }}
32 |     strategy:
33 |       fail-fast: false
34 |       matrix:
35 |         os:
36 |           - ubuntu-latest #x64
37 |           - ubuntu-24.04-arm #arm64
38 |           - windows-latest #x64
39 |           - macos-13 #x64
40 |           - macos-latest #arm64
41 |     runs-on: ${{ matrix.os }}
42 |     steps:
43 |       - name: Checkout
44 |         uses: actions/checkout@v4
45 |       - uses: actions/setup-go@v5
46 |         with:
47 |           go-version: ${{ env.GO_VERSION }}
48 |       - name: Build
49 |         run: make build
50 |       - name: Test
51 |         run: make test
52 | 
```

--------------------------------------------------------------------------------
/npm/podman-mcp-server/bin/index.js:
--------------------------------------------------------------------------------

```javascript
 1 | #!/usr/bin/env node
 2 | 
 3 | const childProcess = require('child_process');
 4 | 
 5 | const BINARY_MAP = {
 6 |   darwin_x64: {name: 'podman-mcp-server-darwin-amd64', suffix: ''},
 7 |   darwin_arm64: {name: 'podman-mcp-server-darwin-arm64', suffix: ''},
 8 |   linux_x64: {name: 'podman-mcp-server-linux-amd64', suffix: ''},
 9 |   linux_arm64: {name: 'podman-mcp-server-linux-arm64', suffix: ''},
10 |   win32_x64: {name: 'podman-mcp-server-windows-amd64', suffix: '.exe'},
11 |   win32_arm64: {name: 'podman-mcp-server-windows-arm64', suffix: '.exe'},
12 | };
13 | 
14 | // Resolving will fail if the optionalDependency was not installed or the platform/arch is not supported
15 | const resolveBinaryPath = () => {
16 |   try {
17 |     const binary = BINARY_MAP[`${process.platform}_${process.arch}`];
18 |     return require.resolve(`${binary.name}/bin/${binary.name}${binary.suffix}`);
19 |   } catch (e) {
20 |     throw new Error(`Could not resolve binary path for platform/arch: ${process.platform}/${process.arch}`);
21 |   }
22 | };
23 | 
24 | childProcess.execFileSync(resolveBinaryPath(), process.argv.slice(2), {
25 |   stdio: 'inherit',
26 | });
27 | 
28 | 
```

--------------------------------------------------------------------------------
/npm/podman-mcp-server/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "podman-mcp-server",
 3 |   "version": "0.0.0",
 4 |   "description": "Model Context Protocol (MCP) server for container runtimes (Podman and Docker) ",
 5 |   "main": "./bin/index.js",
 6 |   "bin": {
 7 |     "podman-mcp-server": "bin/index.js"
 8 |   },
 9 |   "optionalDependencies": {
10 |     "podman-mcp-server-darwin-amd64": "0.0.0",
11 |     "podman-mcp-server-darwin-arm64": "0.0.0",
12 |     "podman-mcp-server-linux-amd64": "0.0.0",
13 |     "podman-mcp-server-linux-arm64": "0.0.0",
14 |     "podman-mcp-server-windows-amd64": "0.0.0",
15 |     "podman-mcp-server-windows-arm64": "0.0.0"
16 |   },
17 |   "repository": {
18 |     "type": "git",
19 |     "url": "git+https://github.com/manusa/podman-mcp-server.git"
20 |   },
21 |   "keywords": [
22 |     "mcp",
23 |     "podman",
24 |     "docker",
25 |     "containers",
26 |     "container-runtime",
27 |     "model context protocol",
28 |     "model",
29 |     "context",
30 |     "protocol"
31 |   ],
32 |   "author": {
33 |     "name": "Marc Nuri",
34 |     "url": "https://www.marcnuri.com"
35 |   },
36 |   "license": "Apache-2.0",
37 |   "bugs": {
38 |     "url": "https://github.com/manusa/podman-mcp-server/issues"
39 |   },
40 |   "homepage": "https://github.com/manusa/podman-mcp-server#readme"
41 | }
42 | 
```

--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | name: Release
 2 | 
 3 | on:
 4 |   push:
 5 |     tags:
 6 |       - '*'
 7 | 
 8 | concurrency:
 9 |   # Only run once for latest commit per ref and cancel other (previous) runs.
10 |   group: ${{ github.workflow }}-${{ github.ref }}
11 |   cancel-in-progress: true
12 | 
13 | env:
14 |   GO_VERSION: 1.23
15 |   NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
16 |   UV_PUBLISH_TOKEN: ${{ secrets.UV_PUBLISH_TOKEN }}
17 | 
18 | permissions:
19 |   contents: write
20 |   discussions: write
21 | 
22 | jobs:
23 |   release:
24 |     name: Release
25 |     runs-on: macos-latest
26 |     steps:
27 |       - name: Checkout
28 |         uses: actions/checkout@v4
29 |       - uses: actions/setup-go@v5
30 |         with:
31 |           go-version: ${{ env.GO_VERSION }}
32 |       - name: Build
33 |         run: make build-all-platforms
34 |       - name: Upload artifacts
35 |         uses: softprops/action-gh-release@v2
36 |         with:
37 |           generate_release_notes: true
38 |           make_latest: true
39 |           files: |
40 |             LICENSE
41 |             podman-mcp-server-*
42 |       - name: Publish npm
43 |         run:
44 |           make npm-publish
45 |   python:
46 |     name: Release Python
47 |     # Python logic requires the tag/release version to be available from GitHub
48 |     needs: release
49 |     runs-on: ubuntu-latest
50 |     steps:
51 |       - name: Checkout
52 |         uses: actions/checkout@v4
53 |       - uses: astral-sh/setup-uv@v5
54 |       - name: Publish Python
55 |         run:
56 |           make python-publish
57 | 
```

--------------------------------------------------------------------------------
/pkg/mcp/mcp.go:
--------------------------------------------------------------------------------

```go
 1 | package mcp
 2 | 
 3 | import (
 4 | 	"github.com/manusa/podman-mcp-server/pkg/podman"
 5 | 	"github.com/manusa/podman-mcp-server/pkg/version"
 6 | 	"github.com/mark3labs/mcp-go/mcp"
 7 | 	"github.com/mark3labs/mcp-go/server"
 8 | 	"slices"
 9 | )
10 | 
11 | type Server struct {
12 | 	server *server.MCPServer
13 | 	podman podman.Podman
14 | }
15 | 
16 | func NewSever() (*Server, error) {
17 | 	s := &Server{
18 | 		server: server.NewMCPServer(
19 | 			version.BinaryName,
20 | 			version.Version,
21 | 			server.WithResourceCapabilities(true, true),
22 | 			server.WithPromptCapabilities(true),
23 | 			server.WithToolCapabilities(true),
24 | 			server.WithLogging(),
25 | 		),
26 | 	}
27 | 	var err error
28 | 	if s.podman, err = podman.NewPodman(); err != nil {
29 | 		return nil, err
30 | 	}
31 | 	s.server.AddTools(slices.Concat(
32 | 		s.initPodmanContainer(),
33 | 		s.initPodmanImage(),
34 | 		s.initPodmanNetwork(),
35 | 		s.initPodmanVolume(),
36 | 	)...)
37 | 	return s, nil
38 | }
39 | 
40 | func (s *Server) ServeStdio() error {
41 | 	return server.ServeStdio(s.server)
42 | }
43 | 
44 | func (s *Server) ServeSse(baseUrl string) *server.SSEServer {
45 | 	options := make([]server.SSEOption, 0)
46 | 	if baseUrl != "" {
47 | 		options = append(options, server.WithBaseURL(baseUrl))
48 | 	}
49 | 	return server.NewSSEServer(s.server, options...)
50 | }
51 | 
52 | func NewTextResult(content string, err error) *mcp.CallToolResult {
53 | 	if err != nil {
54 | 		return &mcp.CallToolResult{
55 | 			IsError: true,
56 | 			Content: []mcp.Content{
57 | 				mcp.TextContent{
58 | 					Type: "text",
59 | 					Text: err.Error(),
60 | 				},
61 | 			},
62 | 		}
63 | 	}
64 | 	return &mcp.CallToolResult{
65 | 		Content: []mcp.Content{
66 | 			mcp.TextContent{
67 | 				Type: "text",
68 | 				Text: content,
69 | 			},
70 | 		},
71 | 	}
72 | }
73 | 
```

--------------------------------------------------------------------------------
/pkg/podman/interface.go:
--------------------------------------------------------------------------------

```go
 1 | package podman
 2 | 
 3 | // Podman interface
 4 | type Podman interface {
 5 | 	// ContainerInspect displays the low-level information on containers identified by the ID or name
 6 | 	ContainerInspect(name string) (string, error)
 7 | 	// ContainerList lists all the containers on the system
 8 | 	ContainerList() (string, error)
 9 | 	// ContainerLogs Display the logs of a container
10 | 	ContainerLogs(name string) (string, error)
11 | 	// ContainerRemove removes a container
12 | 	ContainerRemove(name string) (string, error)
13 | 	// ContainerRun pulls an image from a registry
14 | 	ContainerRun(imageName string, portMappings map[int]int, envVariables []string) (string, error)
15 | 	// ContainerStop stops a running container using the ID or name
16 | 	ContainerStop(name string) (string, error)
17 | 	// ImageBuild builds an image from a Dockerfile, Podmanfile, or Containerfile
18 | 	ImageBuild(containerFile string, imageName string) (string, error)
19 | 	// ImageList list the container images on the system
20 | 	ImageList() (string, error)
21 | 	// ImagePull pulls an image from a registry
22 | 	ImagePull(imageName string) (string, error)
23 | 	// ImagePush pushes an image to a registry
24 | 	ImagePush(imageName string) (string, error)
25 | 	// ImageRemove removes an image from the system
26 | 	ImageRemove(imageName string) (string, error)
27 | 	// NetworkList lists all the networks on the system
28 | 	NetworkList() (string, error)
29 | 	// VolumeList lists all the volumes on the system
30 | 	VolumeList() (string, error)
31 | }
32 | 
33 | func NewPodman() (Podman, error) {
34 | 	// TODO: add implementations for Podman bindings and Docker CLI
35 | 	return newPodmanCli()
36 | }
37 | 
```

--------------------------------------------------------------------------------
/pkg/podman-mcp-server/cmd/root.go:
--------------------------------------------------------------------------------

```go
 1 | package cmd
 2 | 
 3 | import (
 4 | 	"errors"
 5 | 	"fmt"
 6 | 	"github.com/manusa/podman-mcp-server/pkg/mcp"
 7 | 	"github.com/manusa/podman-mcp-server/pkg/version"
 8 | 	"github.com/mark3labs/mcp-go/server"
 9 | 	"github.com/spf13/cobra"
10 | 	"github.com/spf13/viper"
11 | 	"golang.org/x/net/context"
12 | )
13 | 
14 | var rootCmd = &cobra.Command{
15 | 	Use:   "podman-mcp-server [command] [options]",
16 | 	Short: "Podman Model Context Protocol (MCP) server",
17 | 	Long: `
18 | Podman Model Context Protocol (MCP) server
19 | 
20 |   # show this help
21 |   podman-mcp-server -h
22 | 
23 |   # shows version information
24 |   podman-mcp-server --version
25 | 
26 |   # start STDIO server
27 |   podman-mcp-server
28 | 
29 |   # start a SSE server on port 8080
30 |   podman-mcp-server --sse-port 8080
31 | 
32 |   # start a SSE server on port 8443 with a public HTTPS host of example.com
33 |   podman-mcp-server --sse-port 8443 --sse-base-url https://example.com:8443
34 | 
35 |   # TODO: add more examples`,
36 | 	Run: func(cmd *cobra.Command, args []string) {
37 | 		if viper.GetBool("version") {
38 | 			fmt.Println(version.Version)
39 | 			return
40 | 		}
41 | 		mcpServer, err := mcp.NewSever()
42 | 		if err != nil {
43 | 			panic(err)
44 | 		}
45 | 
46 | 		var sseServer *server.SSEServer
47 | 		if ssePort := viper.GetInt("sse-port"); ssePort > 0 {
48 | 			sseServer = mcpServer.ServeSse(viper.GetString("sse-base-url"))
49 | 			if err := sseServer.Start(fmt.Sprintf(":%d", ssePort)); err != nil {
50 | 				panic(err)
51 | 			}
52 | 		}
53 | 		if err := mcpServer.ServeStdio(); err != nil && !errors.Is(err, context.Canceled) {
54 | 			panic(err)
55 | 		}
56 | 		if sseServer != nil {
57 | 			_ = sseServer.Shutdown(cmd.Context())
58 | 		}
59 | 	},
60 | }
61 | 
62 | func init() {
63 | 	rootCmd.Flags().BoolP("version", "v", false, "Print version information and quit")
64 | 	rootCmd.Flags().IntP("sse-port", "", 0, "Start a SSE server on the specified port")
65 | 	rootCmd.Flags().StringP("sse-base-url", "", "", "SSE public base URL to use when sending the endpoint message (e.g. https://example.com)")
66 | 	_ = viper.BindPFlags(rootCmd.Flags())
67 | }
68 | 
69 | func Execute() {
70 | 	if err := rootCmd.Execute(); err != nil {
71 | 		panic(err)
72 | 	}
73 | }
74 | 
```

--------------------------------------------------------------------------------
/pkg/mcp/common_test.go:
--------------------------------------------------------------------------------

```go
  1 | package mcp
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"fmt"
  6 | 	"github.com/mark3labs/mcp-go/client"
  7 | 	"github.com/mark3labs/mcp-go/mcp"
  8 | 	"github.com/mark3labs/mcp-go/server"
  9 | 	"net/http/httptest"
 10 | 	"os"
 11 | 	"os/exec"
 12 | 	"path"
 13 | 	"runtime"
 14 | 	"testing"
 15 | )
 16 | 
 17 | type mcpContext struct {
 18 | 	podmanBinaryDir string
 19 | 	ctx             context.Context
 20 | 	cancel          context.CancelFunc
 21 | 	mcpServer       *Server
 22 | 	mcpHttpServer   *httptest.Server
 23 | 	mcpClient       *client.Client
 24 | }
 25 | 
 26 | func (c *mcpContext) beforeEach(t *testing.T) {
 27 | 	var err error
 28 | 	c.ctx, c.cancel = context.WithCancel(context.Background())
 29 | 	if c.mcpServer, err = NewSever(); err != nil {
 30 | 		t.Fatal(err)
 31 | 		return
 32 | 	}
 33 | 	c.mcpHttpServer = server.NewTestServer(c.mcpServer.server)
 34 | 	if c.mcpClient, err = client.NewSSEMCPClient(c.mcpHttpServer.URL + "/sse"); err != nil {
 35 | 		t.Fatal(err)
 36 | 		return
 37 | 	}
 38 | 	if err = c.mcpClient.Start(c.ctx); err != nil {
 39 | 		t.Fatal(err)
 40 | 		return
 41 | 	}
 42 | 	initRequest := mcp.InitializeRequest{}
 43 | 	initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
 44 | 	initRequest.Params.ClientInfo = mcp.Implementation{Name: "test", Version: "1.33.7"}
 45 | 	_, err = c.mcpClient.Initialize(c.ctx, initRequest)
 46 | 	if err != nil {
 47 | 		t.Fatal(err)
 48 | 		return
 49 | 	}
 50 | }
 51 | 
 52 | func (c *mcpContext) afterEach() {
 53 | 	c.cancel()
 54 | 	_ = c.mcpClient.Close()
 55 | 	c.mcpHttpServer.Close()
 56 | }
 57 | 
 58 | func testCase(t *testing.T, test func(c *mcpContext)) {
 59 | 	mcpCtx := &mcpContext{
 60 | 		podmanBinaryDir: withPodmanBinary(t),
 61 | 	}
 62 | 	mcpCtx.beforeEach(t)
 63 | 	defer mcpCtx.afterEach()
 64 | 	test(mcpCtx)
 65 | }
 66 | 
 67 | // callTool helper function to call a tool by name with arguments
 68 | func (c *mcpContext) callTool(name string, args map[string]interface{}) (*mcp.CallToolResult, error) {
 69 | 	callToolRequest := mcp.CallToolRequest{}
 70 | 	callToolRequest.Params.Name = name
 71 | 	callToolRequest.Params.Arguments = args
 72 | 	return c.mcpClient.CallTool(c.ctx, callToolRequest)
 73 | }
 74 | 
 75 | func (c *mcpContext) withPodmanOutput(outputLines ...string) {
 76 | 	if len(outputLines) > 0 {
 77 | 		f, _ := os.Create(path.Join(c.podmanBinaryDir, "output.txt"))
 78 | 		defer f.Close()
 79 | 		for _, line := range outputLines {
 80 | 			_, _ = f.WriteString(line + "\n")
 81 | 		}
 82 | 	}
 83 | }
 84 | 
 85 | func withPodmanBinary(t *testing.T) string {
 86 | 	binDir := t.TempDir()
 87 | 	binary := "podman"
 88 | 	if runtime.GOOS == "windows" {
 89 | 		binary += ".exe"
 90 | 	}
 91 | 	output, err := exec.
 92 | 		Command("go", "build", "-o", path.Join(binDir, binary),
 93 | 			path.Join("..", "..", "testdata", "podman", "main.go")).
 94 | 		CombinedOutput()
 95 | 	if err != nil {
 96 | 		panic(fmt.Errorf("failed to generate podman binary: %w, output: %s", err, string(output)))
 97 | 	}
 98 | 	if os.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH")) != nil {
 99 | 		panic("failed to set PATH")
100 | 	}
101 | 	return binDir
102 | }
103 | 
```

--------------------------------------------------------------------------------
/pkg/mcp/podman_image.go:
--------------------------------------------------------------------------------

```go
 1 | package mcp
 2 | 
 3 | import (
 4 | 	"context"
 5 | 	"github.com/mark3labs/mcp-go/mcp"
 6 | 	"github.com/mark3labs/mcp-go/server"
 7 | )
 8 | 
 9 | func (s *Server) initPodmanImage() []server.ServerTool {
10 | 	return []server.ServerTool{
11 | 		{mcp.NewTool("image_build",
12 | 			mcp.WithDescription("Build a Docker or Podman image from a Dockerfile, Podmanfile, or Containerfile"),
13 | 			mcp.WithString("containerFile", mcp.Description("The absolute path to the Dockerfile, Podmanfile, or Containerfile to build the image from"), mcp.Required()),
14 | 			mcp.WithString("imageName", mcp.Description("Specifies the name which is assigned to the resulting image if the build process completes successfully (--tag, -t)")),
15 | 		), s.imageBuild},
16 | 		{mcp.NewTool("image_list",
17 | 			mcp.WithDescription("List the Docker or Podman images on the local machine"),
18 | 		), s.imageList},
19 | 		{mcp.NewTool("image_pull",
20 | 			mcp.WithDescription("Copies (pulls) a Docker or Podman container image from a registry onto the local machine storage"),
21 | 			mcp.WithString("imageName", mcp.Description("Docker or Podman container image name to pull"), mcp.Required()),
22 | 		), s.imagePull},
23 | 		{mcp.NewTool("image_push",
24 | 			mcp.WithDescription("Pushes a Docker or Podman container image, manifest list or image index from local machine storage to a registry"),
25 | 			mcp.WithString("imageName", mcp.Description("Docker or Podman container image name to push"), mcp.Required()),
26 | 		), s.imagePush},
27 | 		{mcp.NewTool("image_remove",
28 | 			mcp.WithDescription("Removes a Docker or Podman image from the local machine storage"),
29 | 			mcp.WithString("imageName", mcp.Description("Docker or Podman container image name to remove"), mcp.Required()),
30 | 		), s.imageRemove},
31 | 	}
32 | }
33 | 
34 | func (s *Server) imageBuild(_ context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
35 | 	imageName := ctr.GetArguments()["imageName"]
36 | 	if _, ok := imageName.(string); !ok {
37 | 		imageName = ""
38 | 	}
39 | 	return NewTextResult(s.podman.ImageBuild(ctr.GetArguments()["containerFile"].(string), imageName.(string))), nil
40 | }
41 | 
42 | func (s *Server) imageList(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
43 | 	return NewTextResult(s.podman.ImageList()), nil
44 | }
45 | 
46 | func (s *Server) imagePull(_ context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
47 | 	return NewTextResult(s.podman.ImagePull(ctr.GetArguments()["imageName"].(string))), nil
48 | }
49 | 
50 | func (s *Server) imagePush(_ context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
51 | 	return NewTextResult(s.podman.ImagePush(ctr.GetArguments()["imageName"].(string))), nil
52 | }
53 | 
54 | func (s *Server) imageRemove(_ context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
55 | 	return NewTextResult(s.podman.ImageRemove(ctr.GetArguments()["imageName"].(string))), nil
56 | }
57 | 
```

--------------------------------------------------------------------------------
/python/podman_mcp_server/podman_mcp_server.py:
--------------------------------------------------------------------------------

```python
 1 | import os
 2 | import platform
 3 | import subprocess
 4 | import sys
 5 | from pathlib import Path
 6 | import shutil
 7 | import tempfile
 8 | import urllib.request
 9 | 
10 | if sys.version_info >= (3, 8):
11 |     from importlib.metadata import version
12 | else:
13 |     from importlib_metadata import version
14 | 
15 | __version__ = version("podman-mcp-server")
16 | 
17 | def get_platform_binary():
18 |     """Determine the correct binary for the current platform."""
19 |     system = platform.system().lower()
20 |     arch = platform.machine().lower()
21 | 
22 |     # Normalize architecture names
23 |     if arch in ["x86_64", "amd64"]:
24 |         arch = "amd64"
25 |     elif arch in ["arm64", "aarch64"]:
26 |         arch = "arm64"
27 |     else:
28 |         raise RuntimeError(f"Unsupported architecture: {arch}")
29 | 
30 |     if system == "darwin":
31 |         return f"podman-mcp-server-darwin-{arch}"
32 |     elif system == "linux":
33 |         return f"podman-mcp-server-linux-{arch}"
34 |     elif system == "windows":
35 |         return f"podman-mcp-server-windows-{arch}.exe"
36 |     else:
37 |         raise RuntimeError(f"Unsupported operating system: {system}")
38 | 
39 | def download_binary(binary_version="latest", destination=None):
40 |     """Download the correct binary for the current platform."""
41 |     binary_name = get_platform_binary()
42 |     if destination is None:
43 |         destination = Path.home() / ".podman-mcp-server" / "bin" / binary_version
44 | 
45 |     destination = Path(destination)
46 |     destination.mkdir(parents=True, exist_ok=True)
47 |     binary_path = destination / binary_name
48 | 
49 |     if binary_path.exists():
50 |         return binary_path
51 | 
52 |     base_url = "https://github.com/manusa/podman-mcp-server/releases"
53 |     if binary_version == "latest":
54 |         release_url = f"{base_url}/latest/download/{binary_name}"
55 |     else:
56 |         release_url = f"{base_url}/download/v{binary_version}/{binary_name}"
57 | 
58 |     # Download the binary
59 |     print(f"Downloading {binary_name} from {release_url}")
60 |     with tempfile.NamedTemporaryFile(delete=False) as temp_file:
61 |         try:
62 |             with urllib.request.urlopen(release_url) as response:
63 |                 shutil.copyfileobj(response, temp_file)
64 |             temp_file.close()
65 | 
66 |             # Move to destination and make executable
67 |             shutil.move(temp_file.name, binary_path)
68 |             binary_path.chmod(binary_path.stat().st_mode | 0o755)  # Make executable
69 | 
70 |             return binary_path
71 |         except Exception as e:
72 |             os.unlink(temp_file.name)
73 |             raise RuntimeError(f"Failed to download binary: {e}")
74 | 
75 | def execute(args=None):
76 |     """Download and execute the podman-mcp-server binary."""
77 |     if args is None:
78 |         args = []
79 | 
80 |     try:
81 |         binary_path = download_binary(binary_version=__version__)
82 |         cmd = [str(binary_path)] + args
83 | 
84 |         # Execute the binary with the provided arguments
85 |         process = subprocess.run(cmd)
86 |         return process.returncode
87 |     except Exception as e:
88 |         print(f"Error executing podman-mcp-server: {e}", file=sys.stderr)
89 |         return 1
90 | 
91 | if __name__ == "__main__":
92 |     sys.exit(execute(sys.argv[1:]))
93 | 
94 | 
95 | def main():
96 |     """Main function to execute the podman-mcp-server binary."""
97 |     args = sys.argv[1:] if len(sys.argv) > 1 else []
98 |     return execute(args)
99 | 
```

--------------------------------------------------------------------------------
/pkg/mcp/podman_image_test.go:
--------------------------------------------------------------------------------

```go
  1 | package mcp
  2 | 
  3 | import (
  4 | 	"github.com/mark3labs/mcp-go/mcp"
  5 | 	"strings"
  6 | 	"testing"
  7 | )
  8 | 
  9 | func TestImageBuild(t *testing.T) {
 10 | 	testCase(t, func(c *mcpContext) {
 11 | 		toolResult, err := c.callTool("image_build", map[string]interface{}{
 12 | 			"containerFile": "/tmp/Containerfile",
 13 | 		})
 14 | 		t.Run("image_build returns OK", func(t *testing.T) {
 15 | 			if err != nil {
 16 | 				t.Fatalf("call tool failed %v", err)
 17 | 			}
 18 | 			if toolResult.IsError {
 19 | 				t.Fatalf("call tool failed")
 20 | 			}
 21 | 			if !strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "podman build -f /tmp/Containerfile") {
 22 | 				t.Errorf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text)
 23 | 			}
 24 | 		})
 25 | 		toolResult, err = c.callTool("image_build", map[string]interface{}{
 26 | 			"containerFile": "/tmp/Containerfile",
 27 | 			"imageName":     "example.com/org/image:tag",
 28 | 		})
 29 | 		t.Run("image_build with imageName returns OK", func(t *testing.T) {
 30 | 			if err != nil {
 31 | 				t.Fatalf("call tool failed %v", err)
 32 | 			}
 33 | 			if toolResult.IsError {
 34 | 				t.Fatalf("call tool failed")
 35 | 			}
 36 | 			if !strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "podman build -t example.com/org/image:tag -f /tmp/Containerfile") {
 37 | 				t.Errorf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text)
 38 | 			}
 39 | 		})
 40 | 	})
 41 | }
 42 | 
 43 | func TestImageList(t *testing.T) {
 44 | 	testCase(t, func(c *mcpContext) {
 45 | 		c.withPodmanOutput(
 46 | 			"REPOSITORY\tTAG\tDIGEST\tIMAGE ID\tCREATED\tSIZE",
 47 | 			"docker.io/marcnuri/chuck-norris\nlatest\nsha256:1337\nb8f22a2b8410\n1 day ago\n37 MB",
 48 | 		)
 49 | 		toolResult, err := c.callTool("image_list", map[string]interface{}{})
 50 | 		t.Run("image_list returns OK", func(t *testing.T) {
 51 | 			if err != nil {
 52 | 				t.Fatalf("call tool failed %v", err)
 53 | 				return
 54 | 			}
 55 | 			if toolResult.IsError {
 56 | 				t.Fatalf("call tool failed")
 57 | 				return
 58 | 			}
 59 | 			if !strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "podman images --digests") {
 60 | 				t.Fatalf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text)
 61 | 				return
 62 | 			}
 63 | 		})
 64 | 	})
 65 | }
 66 | 
 67 | func TestImagePull(t *testing.T) {
 68 | 	testCase(t, func(c *mcpContext) {
 69 | 		toolResult, err := c.callTool("image_pull", map[string]interface{}{
 70 | 			"imageName": "example.com/org/image:tag",
 71 | 		})
 72 | 		t.Run("image_pull returns OK", func(t *testing.T) {
 73 | 			if err != nil {
 74 | 				t.Fatalf("call tool failed %v", err)
 75 | 			}
 76 | 			if toolResult.IsError {
 77 | 				t.Fatalf("call tool failed")
 78 | 			}
 79 | 			if !strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "podman image pull example.com/org/image:tag") {
 80 | 				t.Errorf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text)
 81 | 			}
 82 | 			if !strings.HasSuffix(toolResult.Content[0].(mcp.TextContent).Text, "example.com/org/image:tag pulled successfully") {
 83 | 				t.Errorf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text)
 84 | 			}
 85 | 		})
 86 | 	})
 87 | }
 88 | 
 89 | func TestImagePush(t *testing.T) {
 90 | 	testCase(t, func(c *mcpContext) {
 91 | 		toolResult, err := c.callTool("image_push", map[string]interface{}{
 92 | 			"imageName": "example.com/org/image:tag",
 93 | 		})
 94 | 		t.Run("image_push returns OK", func(t *testing.T) {
 95 | 			if err != nil {
 96 | 				t.Fatalf("call tool failed %v", err)
 97 | 			}
 98 | 			if toolResult.IsError {
 99 | 				t.Fatalf("call tool failed")
100 | 			}
101 | 			if !strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "podman image push example.com/org/image:tag") {
102 | 				t.Errorf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text)
103 | 			}
104 | 			if !strings.HasSuffix(toolResult.Content[0].(mcp.TextContent).Text, "example.com/org/image:tag pushed successfully") {
105 | 				t.Errorf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text)
106 | 			}
107 | 		})
108 | 	})
109 | }
110 | 
111 | func TestImageRemove(t *testing.T) {
112 | 	testCase(t, func(c *mcpContext) {
113 | 		toolResult, err := c.callTool("image_remove", map[string]interface{}{
114 | 			"imageName": "example.com/org/image:tag",
115 | 		})
116 | 		t.Run("image_remove returns OK", func(t *testing.T) {
117 | 			if err != nil {
118 | 				t.Fatalf("call tool failed %v", err)
119 | 			}
120 | 			if toolResult.IsError {
121 | 				t.Fatalf("call tool failed")
122 | 			}
123 | 			if !strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "podman image rm example.com/org/image:tag") {
124 | 				t.Errorf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text)
125 | 			}
126 | 		})
127 | 	})
128 | }
129 | 
```

--------------------------------------------------------------------------------
/pkg/podman/podman_cli.go:
--------------------------------------------------------------------------------

```go
  1 | package podman
  2 | 
  3 | import (
  4 | 	"errors"
  5 | 	"fmt"
  6 | 	"os/exec"
  7 | 	"strings"
  8 | )
  9 | 
 10 | type podmanCli struct {
 11 | 	filePath string
 12 | }
 13 | 
 14 | // ContainerInspect
 15 | // https://docs.podman.io/en/stable/markdown/podman-inspect.1.html
 16 | func (p *podmanCli) ContainerInspect(name string) (string, error) {
 17 | 	return p.exec("inspect", name)
 18 | }
 19 | 
 20 | // ContainerList
 21 | // https://docs.podman.io/en/stable/markdown/podman-ps.1.html
 22 | func (p *podmanCli) ContainerList() (string, error) {
 23 | 	return p.exec("container", "list", "-a")
 24 | }
 25 | 
 26 | // ContainerLogs
 27 | // https://docs.podman.io/en/stable/markdown/podman-logs.1.html
 28 | func (p *podmanCli) ContainerLogs(name string) (string, error) {
 29 | 	return p.exec("logs", name)
 30 | }
 31 | 
 32 | // ContainerRemove
 33 | // https://docs.podman.io/en/stable/markdown/podman-rm.1.html
 34 | func (p *podmanCli) ContainerRemove(name string) (string, error) {
 35 | 	return p.exec("container", "rm", name)
 36 | }
 37 | 
 38 | // ContainerRun
 39 | // https://docs.podman.io/en/stable/markdown/podman-run.1.html
 40 | func (p *podmanCli) ContainerRun(imageName string, portMappings map[int]int, envVariables []string) (string, error) {
 41 | 	args := []string{"run", "--rm", "-d"}
 42 | 	if len(portMappings) > 0 {
 43 | 		for hostPort, containerPort := range portMappings {
 44 | 			args = append(args, fmt.Sprintf("--publish=%d:%d", hostPort, containerPort))
 45 | 		}
 46 | 	} else {
 47 | 		args = append(args, "--publish-all")
 48 | 	}
 49 | 	for _, env := range envVariables {
 50 | 		args = append(args, "--env", env)
 51 | 	}
 52 | 	output, err := p.exec(append(args, imageName)...)
 53 | 	if err == nil {
 54 | 		return output, nil
 55 | 	}
 56 | 	if strings.Contains(output, "Error: short-name") {
 57 | 		imageName = "docker.io/" + imageName
 58 | 		if output, err = p.exec(append(args, imageName)...); err == nil {
 59 | 			return output, nil
 60 | 		}
 61 | 	}
 62 | 	return "", err
 63 | }
 64 | 
 65 | // ContainerStop
 66 | // https://docs.podman.io/en/stable/markdown/podman-stop.1.html
 67 | func (p *podmanCli) ContainerStop(name string) (string, error) {
 68 | 	return p.exec("container", "stop", name)
 69 | }
 70 | 
 71 | // ImageBuild
 72 | // https://docs.podman.io/en/stable/markdown/podman-build.1.html
 73 | func (p *podmanCli) ImageBuild(containerFile string, imageName string) (string, error) {
 74 | 	args := []string{"build"}
 75 | 	if imageName != "" {
 76 | 		args = append(args, "-t", imageName)
 77 | 	}
 78 | 	return p.exec(append(args, "-f", containerFile)...)
 79 | }
 80 | 
 81 | // ImageList
 82 | // https://docs.podman.io/en/stable/markdown/podman-images.1.html
 83 | func (p *podmanCli) ImageList() (string, error) {
 84 | 	return p.exec("images", "--digests")
 85 | }
 86 | 
 87 | // ImagePull
 88 | // https://docs.podman.io/en/stable/markdown/podman-pull.1.html
 89 | func (p *podmanCli) ImagePull(imageName string) (string, error) {
 90 | 	output, err := p.exec("image", "pull", imageName)
 91 | 	if err == nil {
 92 | 		return fmt.Sprintf("%s\n%s pulled successfully", output, imageName), nil
 93 | 	}
 94 | 	if strings.Contains(output, "Error: short-name") {
 95 | 		imageName = "docker.io/" + imageName
 96 | 		if output, err = p.exec("pull", imageName); err == nil {
 97 | 			return fmt.Sprintf("%s\n%s pulled successfully", output, imageName), nil
 98 | 		}
 99 | 	}
100 | 	return "", err
101 | }
102 | 
103 | // ImagePush
104 | // https://docs.podman.io/en/stable/markdown/podman-push.1.html
105 | func (p *podmanCli) ImagePush(imageName string) (string, error) {
106 | 	output, err := p.exec("image", "push", imageName)
107 | 	if err == nil {
108 | 		return fmt.Sprintf("%s\n%s pushed successfully", output, imageName), nil
109 | 	}
110 | 	return "", err
111 | }
112 | 
113 | // ImageRemove
114 | // https://docs.podman.io/en/stable/markdown/podman-rmi.1.html
115 | func (p *podmanCli) ImageRemove(imageName string) (string, error) {
116 | 	return p.exec("image", "rm", imageName)
117 | }
118 | 
119 | // NetworkList
120 | // https://docs.podman.io/en/stable/markdown/podman-network-ls.1.html
121 | func (p *podmanCli) NetworkList() (string, error) {
122 | 	return p.exec("network", "ls")
123 | }
124 | 
125 | // VolumeList
126 | // https://docs.podman.io/en/stable/markdown/podman-volume-ls.1.html
127 | func (p *podmanCli) VolumeList() (string, error) {
128 | 	return p.exec("volume", "ls")
129 | }
130 | 
131 | func (p *podmanCli) exec(args ...string) (string, error) {
132 | 	output, err := exec.Command(p.filePath, args...).CombinedOutput()
133 | 	return string(output), err
134 | }
135 | 
136 | func newPodmanCli() (*podmanCli, error) {
137 | 	for _, cmd := range []string{"podman", "podman.exe"} {
138 | 		filePath, err := exec.LookPath(cmd)
139 | 		if err != nil {
140 | 			continue
141 | 		}
142 | 		if _, err = exec.Command(filePath, "version").CombinedOutput(); err == nil {
143 | 			return &podmanCli{filePath}, nil
144 | 		}
145 | 	}
146 | 	return nil, errors.New("podman CLI not found")
147 | }
148 | 
```

--------------------------------------------------------------------------------
/pkg/mcp/podman_container.go:
--------------------------------------------------------------------------------

```go
  1 | package mcp
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"github.com/mark3labs/mcp-go/mcp"
  6 | 	"github.com/mark3labs/mcp-go/server"
  7 | 	"strconv"
  8 | 	"strings"
  9 | )
 10 | 
 11 | func (s *Server) initPodmanContainer() []server.ServerTool {
 12 | 	return []server.ServerTool{
 13 | 		{mcp.NewTool("container_inspect",
 14 | 			mcp.WithDescription("Displays the low-level information and configuration of a Docker or Podman container with the specified container ID or name"),
 15 | 			mcp.WithString("name", mcp.Description("Docker or Podman container ID or name to displays the information"), mcp.Required()),
 16 | 		), s.containerInspect},
 17 | 		{mcp.NewTool("container_list",
 18 | 			mcp.WithDescription("Prints out information about the running Docker or Podman containers"),
 19 | 		), s.containerList},
 20 | 		{mcp.NewTool("container_logs",
 21 | 			mcp.WithDescription("Displays the logs of a Docker or Podman container with the specified container ID or name"),
 22 | 			mcp.WithString("name", mcp.Description("Docker or Podman container ID or name to displays the logs"), mcp.Required()),
 23 | 		), s.containerLogs},
 24 | 		{mcp.NewTool("container_remove",
 25 | 			mcp.WithDescription("Removes a Docker or Podman container with the specified container ID or name (rm)"),
 26 | 			mcp.WithString("name", mcp.Description("Docker or Podman container ID or name to remove"), mcp.Required()),
 27 | 		), s.containerRemove},
 28 | 		{mcp.NewTool("container_run",
 29 | 			mcp.WithDescription("Runs a Docker or Podman container with the specified image name"),
 30 | 			mcp.WithString("imageName", mcp.Description("Docker or Podman container image name to pull"), mcp.Required()),
 31 | 			mcp.WithArray("ports", mcp.Description("Port mappings to expose on the host. "+
 32 | 				"Format: <hostPort>:<containerPort>. "+
 33 | 				"Example: 8080:80. "+
 34 | 				"(Optional, add only to expose ports)"),
 35 | 				// TODO: manual fix to ensure that the items property gets initialized (Gemini)
 36 | 				// https://www.googlecloudcommunity.com/gc/AI-ML/Gemini-API-400-Bad-Request-Array-fields-breaks-function-calling/m-p/769835?nobounce
 37 | 				func(schema map[string]interface{}) {
 38 | 					schema["type"] = "array"
 39 | 					schema["items"] = map[string]interface{}{
 40 | 						"type": "string",
 41 | 					}
 42 | 				},
 43 | 			),
 44 | 			mcp.WithArray("environment", mcp.Description("Environment variables to set in the container. "+
 45 | 				"Format: <key>=<value>. "+
 46 | 				"Example: FOO=bar. "+
 47 | 				"(Optional, add only to set environment variables)"),
 48 | 				// TODO: manual fix to ensure that the items property gets initialized (Gemini)
 49 | 				// https://www.googlecloudcommunity.com/gc/AI-ML/Gemini-API-400-Bad-Request-Array-fields-breaks-function-calling/m-p/769835?nobounce
 50 | 				func(schema map[string]interface{}) {
 51 | 					schema["type"] = "array"
 52 | 					schema["items"] = map[string]interface{}{
 53 | 						"type": "string",
 54 | 					}
 55 | 				},
 56 | 			),
 57 | 		), s.containerRun},
 58 | 		{mcp.NewTool("container_stop",
 59 | 			mcp.WithDescription("Stops a Docker or Podman running container with the specified container ID or name"),
 60 | 			mcp.WithString("name", mcp.Description("Docker or Podman container ID or name to stop"), mcp.Required()),
 61 | 		), s.containerStop},
 62 | 	}
 63 | }
 64 | 
 65 | func (s *Server) containerInspect(_ context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 66 | 	return NewTextResult(s.podman.ContainerInspect(ctr.GetArguments()["name"].(string))), nil
 67 | }
 68 | 
 69 | func (s *Server) containerList(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 70 | 	return NewTextResult(s.podman.ContainerList()), nil
 71 | }
 72 | 
 73 | func (s *Server) containerLogs(_ context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 74 | 	return NewTextResult(s.podman.ContainerLogs(ctr.GetArguments()["name"].(string))), nil
 75 | }
 76 | 
 77 | func (s *Server) containerRemove(_ context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 78 | 	return NewTextResult(s.podman.ContainerRemove(ctr.GetArguments()["name"].(string))), nil
 79 | }
 80 | 
 81 | func (s *Server) containerRun(_ context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 82 | 	ports := ctr.GetArguments()["ports"]
 83 | 	portMappings := make(map[int]int)
 84 | 	if _, ok := ports.([]interface{}); ok {
 85 | 		for _, port := range ports.([]interface{}) {
 86 | 			if _, ok := port.(string); !ok {
 87 | 				continue
 88 | 			}
 89 | 			hostPort, _ := strconv.Atoi(strings.Split(port.(string), ":")[0])
 90 | 			containerPort, _ := strconv.Atoi(strings.Split(port.(string), ":")[1])
 91 | 			if hostPort > 0 && containerPort > 0 {
 92 | 				portMappings[hostPort] = containerPort
 93 | 			}
 94 | 		}
 95 | 	}
 96 | 	environment := ctr.GetArguments()["environment"]
 97 | 	envVariables := make([]string, 0)
 98 | 	if _, ok := environment.([]interface{}); ok && len(environment.([]interface{})) > 0 {
 99 | 		for _, env := range environment.([]interface{}) {
100 | 			if _, ok = env.(string); !ok {
101 | 				continue
102 | 			}
103 | 			envVariables = append(envVariables, env.(string))
104 | 		}
105 | 	}
106 | 	return NewTextResult(s.podman.ContainerRun(ctr.GetArguments()["imageName"].(string), portMappings, envVariables)), nil
107 | }
108 | 
109 | func (s *Server) containerStop(_ context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
110 | 	return NewTextResult(s.podman.ContainerStop(ctr.GetArguments()["name"].(string))), nil
111 | }
112 | 
```

--------------------------------------------------------------------------------
/pkg/mcp/podman_container_test.go:
--------------------------------------------------------------------------------

```go
  1 | package mcp
  2 | 
  3 | import (
  4 | 	"github.com/mark3labs/mcp-go/mcp"
  5 | 	"strings"
  6 | 	"testing"
  7 | )
  8 | 
  9 | func TestContainerInspect(t *testing.T) {
 10 | 	testCase(t, func(c *mcpContext) {
 11 | 		toolResult, err := c.callTool("container_inspect", map[string]interface{}{
 12 | 			"name": "example-container",
 13 | 		})
 14 | 		t.Run("container_inspect returns OK", func(t *testing.T) {
 15 | 			if err != nil {
 16 | 				t.Fatalf("call tool failed %v", err)
 17 | 			}
 18 | 			if toolResult.IsError {
 19 | 				t.Fatalf("call tool failed")
 20 | 			}
 21 | 		})
 22 | 		t.Run("container_inspect inspects provided container", func(t *testing.T) {
 23 | 			if !strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "podman inspect example-container") {
 24 | 				t.Fatalf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text)
 25 | 			}
 26 | 		})
 27 | 	})
 28 | }
 29 | 
 30 | func TestContainerList(t *testing.T) {
 31 | 	testCase(t, func(c *mcpContext) {
 32 | 		toolResult, err := c.callTool("container_list", map[string]interface{}{})
 33 | 		t.Run("container_list returns OK", func(t *testing.T) {
 34 | 			if err != nil {
 35 | 				t.Fatalf("call tool failed %v", err)
 36 | 			}
 37 | 			if toolResult.IsError {
 38 | 				t.Fatalf("call tool failed")
 39 | 			}
 40 | 			if !strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "podman container list -a") {
 41 | 				t.Fatalf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text)
 42 | 			}
 43 | 		})
 44 | 	})
 45 | }
 46 | 
 47 | func TestContainerLogs(t *testing.T) {
 48 | 	testCase(t, func(c *mcpContext) {
 49 | 		toolResult, err := c.callTool("container_logs", map[string]interface{}{
 50 | 			"name": "example-container",
 51 | 		})
 52 | 		t.Run("container_logs returns OK", func(t *testing.T) {
 53 | 			if err != nil {
 54 | 				t.Fatalf("call tool failed %v", err)
 55 | 			}
 56 | 			if toolResult.IsError {
 57 | 				t.Fatalf("call tool failed")
 58 | 			}
 59 | 		})
 60 | 		t.Run("container_logs retrieves logs from provided container", func(t *testing.T) {
 61 | 			if !strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "podman logs example-container") {
 62 | 				t.Fatalf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text)
 63 | 			}
 64 | 		})
 65 | 	})
 66 | }
 67 | 
 68 | func TestContainerRemove(t *testing.T) {
 69 | 	testCase(t, func(c *mcpContext) {
 70 | 		toolResult, err := c.callTool("container_remove", map[string]interface{}{
 71 | 			"name": "example-container",
 72 | 		})
 73 | 		t.Run("container_remove returns OK", func(t *testing.T) {
 74 | 			if err != nil {
 75 | 				t.Fatalf("call tool failed %v", err)
 76 | 			}
 77 | 			if toolResult.IsError {
 78 | 				t.Fatalf("call tool failed")
 79 | 			}
 80 | 		})
 81 | 		t.Run("container_remove removes provided container", func(t *testing.T) {
 82 | 			if !strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "podman container rm example-container") {
 83 | 				t.Fatalf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text)
 84 | 			}
 85 | 		})
 86 | 	})
 87 | }
 88 | 
 89 | func TestContainerRun(t *testing.T) {
 90 | 	testCase(t, func(c *mcpContext) {
 91 | 		toolResult, err := c.callTool("container_run", map[string]interface{}{
 92 | 			"imageName": "example.com/org/image:tag",
 93 | 		})
 94 | 		t.Run("container_run returns OK", func(t *testing.T) {
 95 | 			if err != nil {
 96 | 				t.Fatalf("call tool failed %v", err)
 97 | 			}
 98 | 			if toolResult.IsError {
 99 | 				t.Fatalf("call tool failed")
100 | 			}
101 | 		})
102 | 		t.Run("container_run runs provided image", func(t *testing.T) {
103 | 			if !strings.HasSuffix(toolResult.Content[0].(mcp.TextContent).Text, " example.com/org/image:tag\n") {
104 | 				t.Fatalf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text)
105 | 			}
106 | 		})
107 | 		t.Run("container_run runs in detached mode", func(t *testing.T) {
108 | 			if !strings.Contains(toolResult.Content[0].(mcp.TextContent).Text, " -d ") {
109 | 				t.Fatalf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text)
110 | 			}
111 | 		})
112 | 		t.Run("container_run publishes all exposed ports", func(t *testing.T) {
113 | 			if !strings.Contains(toolResult.Content[0].(mcp.TextContent).Text, " --publish-all ") {
114 | 				t.Fatalf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text)
115 | 			}
116 | 		})
117 | 		toolResult, err = c.callTool("container_run", map[string]interface{}{
118 | 			"imageName": "example.com/org/image:tag",
119 | 			"ports": []interface{}{
120 | 				1337, // Invalid entry to test
121 | 				"8080:80",
122 | 				"8082:8082",
123 | 				"8443:443",
124 | 			},
125 | 		})
126 | 		t.Run("container_run with ports returns OK", func(t *testing.T) {
127 | 			if err != nil {
128 | 				t.Fatalf("call tool failed %v", err)
129 | 			}
130 | 			if toolResult.IsError {
131 | 				t.Fatalf("call tool failed")
132 | 			}
133 | 		})
134 | 		t.Run("container_run with ports publishes provided ports", func(t *testing.T) {
135 | 			if !strings.Contains(toolResult.Content[0].(mcp.TextContent).Text, " --publish=8080:80 ") {
136 | 				t.Fatalf("expected port --publish=8080:80, got %v", toolResult.Content[0].(mcp.TextContent).Text)
137 | 			}
138 | 			if !strings.Contains(toolResult.Content[0].(mcp.TextContent).Text, " --publish=8082:8082 ") {
139 | 				t.Fatalf("expected port --publish=8082:8082, got %v", toolResult.Content[0].(mcp.TextContent).Text)
140 | 			}
141 | 			if !strings.Contains(toolResult.Content[0].(mcp.TextContent).Text, " --publish=8443:443 ") {
142 | 				t.Fatalf("expected port --publish=8443:443, got %v", toolResult.Content[0].(mcp.TextContent).Text)
143 | 			}
144 | 		})
145 | 		toolResult, err = c.callTool("container_run", map[string]interface{}{
146 | 			"imageName": "example.com/org/image:tag",
147 | 			"ports":     []interface{}{"8080:80"},
148 | 			"environment": []interface{}{
149 | 				"KEY=VALUE",
150 | 				"FOO=BAR",
151 | 			},
152 | 		})
153 | 		t.Run("container_run with environment returns OK", func(t *testing.T) {
154 | 			if err != nil {
155 | 				t.Fatalf("call tool failed %v", err)
156 | 			}
157 | 			if toolResult.IsError {
158 | 				t.Fatalf("call tool failed")
159 | 			}
160 | 		})
161 | 		t.Run("container_run with environment sets provided environment variables", func(t *testing.T) {
162 | 			if !strings.Contains(toolResult.Content[0].(mcp.TextContent).Text, " --env KEY=VALUE ") {
163 | 				t.Fatalf("expected env --env KEY=VALUE, got %v", toolResult.Content[0].(mcp.TextContent).Text)
164 | 			}
165 | 			if !strings.Contains(toolResult.Content[0].(mcp.TextContent).Text, " --env FOO=BAR ") {
166 | 				t.Fatalf("expected env --env FOO=BAR, got %v", toolResult.Content[0].(mcp.TextContent).Text)
167 | 			}
168 | 		})
169 | 	})
170 | }
171 | 
172 | func TestContainerStop(t *testing.T) {
173 | 	testCase(t, func(c *mcpContext) {
174 | 		toolResult, err := c.callTool("container_stop", map[string]interface{}{
175 | 			"name": "example-container",
176 | 		})
177 | 		t.Run("container_stop returns OK", func(t *testing.T) {
178 | 			if err != nil {
179 | 				t.Fatalf("call tool failed %v", err)
180 | 			}
181 | 			if toolResult.IsError {
182 | 				t.Fatalf("call tool failed")
183 | 			}
184 | 		})
185 | 		t.Run("container_stop stops provided container", func(t *testing.T) {
186 | 			if !strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "podman container stop example-container") {
187 | 				t.Fatalf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text)
188 | 			}
189 | 		})
190 | 	})
191 | }
192 | 
```