# 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: -------------------------------------------------------------------------------- ``` .idea/ .docusaurus/ node_modules/ .npmrc /podman-mcp-server npm/podman-mcp-server/README.md npm/podman-mcp-server/LICENSE !npm/podman-mcp-server podman-mcp-server-darwin-amd64 !npm/podman-mcp-server-darwin-amd64/ podman-mcp-server-darwin-arm64 !npm/podman-mcp-server-darwin-arm64 podman-mcp-server-linux-amd64 !npm/podman-mcp-server-linux-amd64 podman-mcp-server-linux-arm64 !npm/podman-mcp-server-linux-arm64 podman-mcp-server-windows-amd64.exe podman-mcp-server-windows-arm64.exe python/.venv/ python/build/ python/dist/ python/podman-mcp-server.egg-info/ !python/podman-mcp-server ``` -------------------------------------------------------------------------------- /python/README.md: -------------------------------------------------------------------------------- ```markdown ../README.md ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # Podman MCP Server [](https://github.com/manusa/podman-mcp-server/blob/main/LICENSE) [](https://www.npmjs.com/package/podman-mcp-server) [](https://pypi.org/project/podman-mcp-server/) [](https://github.com/manusa/podman-mcp-server/releases/latest) [](https://github.com/manusa/podman-mcp-server/actions/workflows/build.yaml) [✨ Features](#features) | [🚀 Getting Started](#getting-started) | [🎥 Demos](#demos) | [⚙️ Configuration](#configuration) | [🧑💻 Development](#development) ## ✨ Features <a id="features"></a> A powerful and flexible MCP server for container runtimes supporting Podman and Docker. ## 🚀 Getting Started <a id="getting-started"></a> ### Claude Desktop #### Using npx If you have npm installed, this is the fastest way to get started with `podman-mcp-server` on Claude Desktop. Open your `claude_desktop_config.json` and add the mcp server to the list of `mcpServers`: ``` json { "mcpServers": { "podman": { "command": "npx", "args": [ "-y", "podman-mcp-server@latest" ] } } } ``` ### VS Code / VS Code Insiders Install the Podman MCP server extension in VS Code Insiders by pressing the following link: [<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) [<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) Alternatively, you can install the extension manually by running the following command: ```shell # For VS Code code --add-mcp '{"name":"podman","command":"npx","args":["podman-mcp-server@latest"]}' # For VS Code Insiders code-insiders --add-mcp '{"name":"podman","command":"npx","args":["podman-mcp-server@latest"]}' ``` ### Goose CLI [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. #### Using npm If you have npm installed, this is the fastest way to get started with `podman-mcp-server`. Open your goose `config.yaml` and add the mcp server to the list of `mcpServers`: ```yaml extensions: podman: command: npx args: - -y - podman-mcp-server@latest ``` ## 🎥 Demos <a id="demos"></a> ## ⚙️ Configuration <a id="configuration"></a> The Podman MCP server can be configured using command line (CLI) arguments. 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). ```shell # Run the Podman MCP server using npx (in case you have npm installed) npx podman-mcp-server@latest --help ``` ```shell # Run the Podman MCP server using the latest release binary ./podman-mcp-server --help ``` ### Configuration Options | Option | Description | |--------------|------------------------------------------------------------------------------------------| | `--sse-port` | Starts the MCP server in Server-Sent Event (SSE) mode and listens on the specified port. | ## 🧑💻 Development <a id="development"></a> ### Running with mcp-inspector Compile the project and run the Podman MCP server with [mcp-inspector](https://modelcontextprotocol.io/docs/tools/inspector) to inspect the MCP server. ```shell # Compile the project make build # Run the Podman MCP server with mcp-inspector npx @modelcontextprotocol/inspector@latest $(pwd)/podman-mcp-server ``` ``` -------------------------------------------------------------------------------- /python/podman_mcp_server/__main__.py: -------------------------------------------------------------------------------- ```python from .podman_mcp_server import main if __name__ == "__main__": main() ``` -------------------------------------------------------------------------------- /cmd/podman-mcp-server/main.go: -------------------------------------------------------------------------------- ```go package main import "github.com/manusa/podman-mcp-server/pkg/podman-mcp-server/cmd" func main() { cmd.Execute() } ``` -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- ```yaml version: 2 updates: - package-ecosystem: "gomod" directory: "/" schedule: interval: "daily" open-pull-requests-limit: 10 ``` -------------------------------------------------------------------------------- /pkg/version/version.go: -------------------------------------------------------------------------------- ```go package version var CommitHash = "unknown" var BuildTime = "1970-01-01T00:00:00Z" var Version = "0.0.0" var BinaryName = "podman-mcp-server" ``` -------------------------------------------------------------------------------- /python/podman_mcp_server/__init__.py: -------------------------------------------------------------------------------- ```python """ Model Context Protocol (MCP) server for container runtimes (Podman and Docker) """ from .podman_mcp_server import main __all__ = ['main'] ``` -------------------------------------------------------------------------------- /cmd/podman-mcp-server/main_test.go: -------------------------------------------------------------------------------- ```go package main import ( "os" ) func Example_version() { oldArgs := os.Args defer func() { os.Args = oldArgs }() os.Args = []string{"podman-mcp-server", "--version"} main() // Output: 0.0.0 } ``` -------------------------------------------------------------------------------- /npm/podman-mcp-server-linux-amd64/package.json: -------------------------------------------------------------------------------- ```json { "name": "podman-mcp-server-linux-amd64", "version": "0.0.0", "description": "Model Context Protocol (MCP) server for container runtimes (Podman and Docker) ", "os": [ "linux" ], "cpu": [ "x64" ] } ``` -------------------------------------------------------------------------------- /npm/podman-mcp-server-darwin-amd64/package.json: -------------------------------------------------------------------------------- ```json { "name": "podman-mcp-server-darwin-amd64", "version": "0.0.0", "description": "Model Context Protocol (MCP) server for container runtimes (Podman and Docker) ", "os": [ "darwin" ], "cpu": [ "x64" ] } ``` -------------------------------------------------------------------------------- /npm/podman-mcp-server-linux-arm64/package.json: -------------------------------------------------------------------------------- ```json { "name": "podman-mcp-server-linux-arm64", "version": "0.0.0", "description": "Model Context Protocol (MCP) server for container runtimes (Podman and Docker) ", "os": [ "linux" ], "cpu": [ "arm64" ] } ``` -------------------------------------------------------------------------------- /npm/podman-mcp-server-windows-amd64/package.json: -------------------------------------------------------------------------------- ```json { "name": "podman-mcp-server-windows-amd64", "version": "0.0.0", "description": "Model Context Protocol (MCP) server for container runtimes (Podman and Docker) ", "os": [ "win32" ], "cpu": [ "x64" ] } ``` -------------------------------------------------------------------------------- /npm/podman-mcp-server-darwin-arm64/package.json: -------------------------------------------------------------------------------- ```json { "name": "podman-mcp-server-darwin-arm64", "version": "0.0.0", "description": "Model Context Protocol (MCP) server for container runtimes (Podman and Docker) ", "os": [ "darwin" ], "cpu": [ "arm64" ] } ``` -------------------------------------------------------------------------------- /npm/podman-mcp-server-windows-arm64/package.json: -------------------------------------------------------------------------------- ```json { "name": "podman-mcp-server-windows-arm64", "version": "0.0.0", "description": "Model Context Protocol (MCP) server for container runtimes (Podman and Docker) ", "os": [ "win32" ], "cpu": [ "arm64" ] } ``` -------------------------------------------------------------------------------- /testdata/podman/main.go: -------------------------------------------------------------------------------- ```go // Fake podman CLI binary package main import ( "os" "path" "path/filepath" ) func main() { print("podman") for _, arg := range os.Args[1:] { print(" " + arg) } println() ex, err := os.Executable() if err != nil { panic(err) } outputTxt := path.Join(filepath.Dir(ex), "output.txt") _, err = os.Stat(outputTxt) if err == nil { data, _ := os.ReadFile(outputTxt) _, _ = os.Stdout.Write(data) } os.Exit(0) } ``` -------------------------------------------------------------------------------- /pkg/mcp/podman_volume.go: -------------------------------------------------------------------------------- ```go package mcp import ( "context" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) func (s *Server) initPodmanVolume() []server.ServerTool { return []server.ServerTool{ {mcp.NewTool("volume_list", mcp.WithDescription("List all the available Docker or Podman volumes"), ), s.volumeList}, } } func (s *Server) volumeList(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { return NewTextResult(s.podman.VolumeList()), nil } ``` -------------------------------------------------------------------------------- /pkg/mcp/podman_network.go: -------------------------------------------------------------------------------- ```go package mcp import ( "context" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) func (s *Server) initPodmanNetwork() []server.ServerTool { return []server.ServerTool{ {mcp.NewTool("network_list", mcp.WithDescription("List all the available Docker or Podman networks"), ), s.networkList}, } } func (s *Server) networkList(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { return NewTextResult(s.podman.NetworkList()), nil } ``` -------------------------------------------------------------------------------- /pkg/podman-mcp-server/cmd/root_test.go: -------------------------------------------------------------------------------- ```go package cmd import ( "io" "os" "testing" ) func captureOutput(f func() error) (string, error) { originalOut := os.Stdout defer func() { os.Stdout = originalOut }() r, w, _ := os.Pipe() os.Stdout = w err := f() _ = w.Close() out, _ := io.ReadAll(r) return string(out), err } func TestVersion(t *testing.T) { rootCmd.SetArgs([]string{"--version"}) version, err := captureOutput(rootCmd.Execute) if version != "0.0.0\n" { t.Fatalf("Expected version 0.0.0, got %s %v", version, err) return } } ``` -------------------------------------------------------------------------------- /pkg/mcp/podman_volume_test.go: -------------------------------------------------------------------------------- ```go package mcp import ( "github.com/mark3labs/mcp-go/mcp" "strings" "testing" ) func TestVolumeList(t *testing.T) { testCase(t, func(c *mcpContext) { toolResult, err := c.callTool("volume_list", map[string]interface{}{}) t.Run("volume_list returns OK", func(t *testing.T) { if err != nil { t.Fatalf("call tool failed %v", err) } if toolResult.IsError { t.Fatalf("call tool failed") } }) t.Run("volume_list lists all available volumes", func(t *testing.T) { if !strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "podman volume ls") { t.Fatalf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text) } }) }) } ``` -------------------------------------------------------------------------------- /pkg/mcp/podman_network_test.go: -------------------------------------------------------------------------------- ```go package mcp import ( "github.com/mark3labs/mcp-go/mcp" "strings" "testing" ) func TestNetworkList(t *testing.T) { testCase(t, func(c *mcpContext) { toolResult, err := c.callTool("network_list", map[string]interface{}{}) t.Run("network_list returns OK", func(t *testing.T) { if err != nil { t.Fatalf("call tool failed %v", err) } if toolResult.IsError { t.Fatalf("call tool failed") } }) t.Run("network_list lists all available networks", func(t *testing.T) { if !strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "podman network ls") { t.Fatalf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text) } }) }) } ``` -------------------------------------------------------------------------------- /python/pyproject.toml: -------------------------------------------------------------------------------- ```toml [build-system] requires = ["setuptools>=42", "wheel"] build-backend = "setuptools.build_meta" [project] name = "podman-mcp-server" version = "0.0.0" description = "Model Context Protocol (MCP) server for container runtimes (Podman and Docker)" readme = {file="README.md", content-type="text/markdown"} requires-python = ">=3.6" license = "Apache-2.0" authors = [ { name = "Marc Nuri", email = "[email protected]" } ] classifiers = [ "Programming Language :: Python :: 3", "Operating System :: OS Independent", ] [project.urls] Homepage = "https://github.com/manusa/podman-mcp-server" Repository = "https://github.com/manusa/podman-mcp-server" [project.scripts] podman-mcp-server = "podman_mcp_server:main" ``` -------------------------------------------------------------------------------- /pkg/mcp/mcp_test.go: -------------------------------------------------------------------------------- ```go package mcp import ( "github.com/mark3labs/mcp-go/mcp" "testing" ) func TestTools(t *testing.T) { expectedNames := []string{ "container_inspect", "container_list", "container_logs", "container_remove", "container_run", "container_stop", "image_build", "image_list", "image_pull", "image_push", "image_remove", "network_list", "volume_list", } testCase(t, func(c *mcpContext) { tools, err := c.mcpClient.ListTools(c.ctx, mcp.ListToolsRequest{}) t.Run("ListTools returns tools", func(t *testing.T) { if err != nil { t.Fatalf("call ListTools failed %v", err) } }) nameSet := make(map[string]bool) for _, tool := range tools.Tools { nameSet[tool.Name] = true } for _, name := range expectedNames { t.Run("ListTools has "+name+" tool", func(t *testing.T) { if nameSet[name] != true { t.Errorf("tool %s not found", name) } }) } }) } ``` -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- ```yaml name: Build on: push: branches: - 'main' paths-ignore: - '.gitignore' - 'LICENSE' - '*.md' pull_request: paths-ignore: - '.gitignore' - 'LICENSE' - '*.md' concurrency: # Only run once for latest commit per ref and cancel other (previous) runs. group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: GO_VERSION: 1.23 defaults: run: shell: bash jobs: build: name: Build on ${{ matrix.os }} strategy: fail-fast: false matrix: os: - ubuntu-latest #x64 - ubuntu-24.04-arm #arm64 - windows-latest #x64 - macos-13 #x64 - macos-latest #arm64 runs-on: ${{ matrix.os }} steps: - name: Checkout uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} - name: Build run: make build - name: Test run: make test ``` -------------------------------------------------------------------------------- /npm/podman-mcp-server/bin/index.js: -------------------------------------------------------------------------------- ```javascript #!/usr/bin/env node const childProcess = require('child_process'); const BINARY_MAP = { darwin_x64: {name: 'podman-mcp-server-darwin-amd64', suffix: ''}, darwin_arm64: {name: 'podman-mcp-server-darwin-arm64', suffix: ''}, linux_x64: {name: 'podman-mcp-server-linux-amd64', suffix: ''}, linux_arm64: {name: 'podman-mcp-server-linux-arm64', suffix: ''}, win32_x64: {name: 'podman-mcp-server-windows-amd64', suffix: '.exe'}, win32_arm64: {name: 'podman-mcp-server-windows-arm64', suffix: '.exe'}, }; // Resolving will fail if the optionalDependency was not installed or the platform/arch is not supported const resolveBinaryPath = () => { try { const binary = BINARY_MAP[`${process.platform}_${process.arch}`]; return require.resolve(`${binary.name}/bin/${binary.name}${binary.suffix}`); } catch (e) { throw new Error(`Could not resolve binary path for platform/arch: ${process.platform}/${process.arch}`); } }; childProcess.execFileSync(resolveBinaryPath(), process.argv.slice(2), { stdio: 'inherit', }); ``` -------------------------------------------------------------------------------- /npm/podman-mcp-server/package.json: -------------------------------------------------------------------------------- ```json { "name": "podman-mcp-server", "version": "0.0.0", "description": "Model Context Protocol (MCP) server for container runtimes (Podman and Docker) ", "main": "./bin/index.js", "bin": { "podman-mcp-server": "bin/index.js" }, "optionalDependencies": { "podman-mcp-server-darwin-amd64": "0.0.0", "podman-mcp-server-darwin-arm64": "0.0.0", "podman-mcp-server-linux-amd64": "0.0.0", "podman-mcp-server-linux-arm64": "0.0.0", "podman-mcp-server-windows-amd64": "0.0.0", "podman-mcp-server-windows-arm64": "0.0.0" }, "repository": { "type": "git", "url": "git+https://github.com/manusa/podman-mcp-server.git" }, "keywords": [ "mcp", "podman", "docker", "containers", "container-runtime", "model context protocol", "model", "context", "protocol" ], "author": { "name": "Marc Nuri", "url": "https://www.marcnuri.com" }, "license": "Apache-2.0", "bugs": { "url": "https://github.com/manusa/podman-mcp-server/issues" }, "homepage": "https://github.com/manusa/podman-mcp-server#readme" } ``` -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- ```yaml name: Release on: push: tags: - '*' concurrency: # Only run once for latest commit per ref and cancel other (previous) runs. group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: GO_VERSION: 1.23 NPM_TOKEN: ${{ secrets.NPM_TOKEN }} UV_PUBLISH_TOKEN: ${{ secrets.UV_PUBLISH_TOKEN }} permissions: contents: write discussions: write jobs: release: name: Release runs-on: macos-latest steps: - name: Checkout uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} - name: Build run: make build-all-platforms - name: Upload artifacts uses: softprops/action-gh-release@v2 with: generate_release_notes: true make_latest: true files: | LICENSE podman-mcp-server-* - name: Publish npm run: make npm-publish python: name: Release Python # Python logic requires the tag/release version to be available from GitHub needs: release runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - uses: astral-sh/setup-uv@v5 - name: Publish Python run: make python-publish ``` -------------------------------------------------------------------------------- /pkg/mcp/mcp.go: -------------------------------------------------------------------------------- ```go package mcp import ( "github.com/manusa/podman-mcp-server/pkg/podman" "github.com/manusa/podman-mcp-server/pkg/version" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" "slices" ) type Server struct { server *server.MCPServer podman podman.Podman } func NewSever() (*Server, error) { s := &Server{ server: server.NewMCPServer( version.BinaryName, version.Version, server.WithResourceCapabilities(true, true), server.WithPromptCapabilities(true), server.WithToolCapabilities(true), server.WithLogging(), ), } var err error if s.podman, err = podman.NewPodman(); err != nil { return nil, err } s.server.AddTools(slices.Concat( s.initPodmanContainer(), s.initPodmanImage(), s.initPodmanNetwork(), s.initPodmanVolume(), )...) return s, nil } func (s *Server) ServeStdio() error { return server.ServeStdio(s.server) } func (s *Server) ServeSse(baseUrl string) *server.SSEServer { options := make([]server.SSEOption, 0) if baseUrl != "" { options = append(options, server.WithBaseURL(baseUrl)) } return server.NewSSEServer(s.server, options...) } func NewTextResult(content string, err error) *mcp.CallToolResult { if err != nil { return &mcp.CallToolResult{ IsError: true, Content: []mcp.Content{ mcp.TextContent{ Type: "text", Text: err.Error(), }, }, } } return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ Type: "text", Text: content, }, }, } } ``` -------------------------------------------------------------------------------- /pkg/podman/interface.go: -------------------------------------------------------------------------------- ```go package podman // Podman interface type Podman interface { // ContainerInspect displays the low-level information on containers identified by the ID or name ContainerInspect(name string) (string, error) // ContainerList lists all the containers on the system ContainerList() (string, error) // ContainerLogs Display the logs of a container ContainerLogs(name string) (string, error) // ContainerRemove removes a container ContainerRemove(name string) (string, error) // ContainerRun pulls an image from a registry ContainerRun(imageName string, portMappings map[int]int, envVariables []string) (string, error) // ContainerStop stops a running container using the ID or name ContainerStop(name string) (string, error) // ImageBuild builds an image from a Dockerfile, Podmanfile, or Containerfile ImageBuild(containerFile string, imageName string) (string, error) // ImageList list the container images on the system ImageList() (string, error) // ImagePull pulls an image from a registry ImagePull(imageName string) (string, error) // ImagePush pushes an image to a registry ImagePush(imageName string) (string, error) // ImageRemove removes an image from the system ImageRemove(imageName string) (string, error) // NetworkList lists all the networks on the system NetworkList() (string, error) // VolumeList lists all the volumes on the system VolumeList() (string, error) } func NewPodman() (Podman, error) { // TODO: add implementations for Podman bindings and Docker CLI return newPodmanCli() } ``` -------------------------------------------------------------------------------- /pkg/podman-mcp-server/cmd/root.go: -------------------------------------------------------------------------------- ```go package cmd import ( "errors" "fmt" "github.com/manusa/podman-mcp-server/pkg/mcp" "github.com/manusa/podman-mcp-server/pkg/version" "github.com/mark3labs/mcp-go/server" "github.com/spf13/cobra" "github.com/spf13/viper" "golang.org/x/net/context" ) var rootCmd = &cobra.Command{ Use: "podman-mcp-server [command] [options]", Short: "Podman Model Context Protocol (MCP) server", Long: ` Podman Model Context Protocol (MCP) server # show this help podman-mcp-server -h # shows version information podman-mcp-server --version # start STDIO server podman-mcp-server # start a SSE server on port 8080 podman-mcp-server --sse-port 8080 # start a SSE server on port 8443 with a public HTTPS host of example.com podman-mcp-server --sse-port 8443 --sse-base-url https://example.com:8443 # TODO: add more examples`, Run: func(cmd *cobra.Command, args []string) { if viper.GetBool("version") { fmt.Println(version.Version) return } mcpServer, err := mcp.NewSever() if err != nil { panic(err) } var sseServer *server.SSEServer if ssePort := viper.GetInt("sse-port"); ssePort > 0 { sseServer = mcpServer.ServeSse(viper.GetString("sse-base-url")) if err := sseServer.Start(fmt.Sprintf(":%d", ssePort)); err != nil { panic(err) } } if err := mcpServer.ServeStdio(); err != nil && !errors.Is(err, context.Canceled) { panic(err) } if sseServer != nil { _ = sseServer.Shutdown(cmd.Context()) } }, } func init() { rootCmd.Flags().BoolP("version", "v", false, "Print version information and quit") rootCmd.Flags().IntP("sse-port", "", 0, "Start a SSE server on the specified port") rootCmd.Flags().StringP("sse-base-url", "", "", "SSE public base URL to use when sending the endpoint message (e.g. https://example.com)") _ = viper.BindPFlags(rootCmd.Flags()) } func Execute() { if err := rootCmd.Execute(); err != nil { panic(err) } } ``` -------------------------------------------------------------------------------- /pkg/mcp/common_test.go: -------------------------------------------------------------------------------- ```go package mcp import ( "context" "fmt" "github.com/mark3labs/mcp-go/client" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" "net/http/httptest" "os" "os/exec" "path" "runtime" "testing" ) type mcpContext struct { podmanBinaryDir string ctx context.Context cancel context.CancelFunc mcpServer *Server mcpHttpServer *httptest.Server mcpClient *client.Client } func (c *mcpContext) beforeEach(t *testing.T) { var err error c.ctx, c.cancel = context.WithCancel(context.Background()) if c.mcpServer, err = NewSever(); err != nil { t.Fatal(err) return } c.mcpHttpServer = server.NewTestServer(c.mcpServer.server) if c.mcpClient, err = client.NewSSEMCPClient(c.mcpHttpServer.URL + "/sse"); err != nil { t.Fatal(err) return } if err = c.mcpClient.Start(c.ctx); err != nil { t.Fatal(err) return } initRequest := mcp.InitializeRequest{} initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION initRequest.Params.ClientInfo = mcp.Implementation{Name: "test", Version: "1.33.7"} _, err = c.mcpClient.Initialize(c.ctx, initRequest) if err != nil { t.Fatal(err) return } } func (c *mcpContext) afterEach() { c.cancel() _ = c.mcpClient.Close() c.mcpHttpServer.Close() } func testCase(t *testing.T, test func(c *mcpContext)) { mcpCtx := &mcpContext{ podmanBinaryDir: withPodmanBinary(t), } mcpCtx.beforeEach(t) defer mcpCtx.afterEach() test(mcpCtx) } // callTool helper function to call a tool by name with arguments func (c *mcpContext) callTool(name string, args map[string]interface{}) (*mcp.CallToolResult, error) { callToolRequest := mcp.CallToolRequest{} callToolRequest.Params.Name = name callToolRequest.Params.Arguments = args return c.mcpClient.CallTool(c.ctx, callToolRequest) } func (c *mcpContext) withPodmanOutput(outputLines ...string) { if len(outputLines) > 0 { f, _ := os.Create(path.Join(c.podmanBinaryDir, "output.txt")) defer f.Close() for _, line := range outputLines { _, _ = f.WriteString(line + "\n") } } } func withPodmanBinary(t *testing.T) string { binDir := t.TempDir() binary := "podman" if runtime.GOOS == "windows" { binary += ".exe" } output, err := exec. Command("go", "build", "-o", path.Join(binDir, binary), path.Join("..", "..", "testdata", "podman", "main.go")). CombinedOutput() if err != nil { panic(fmt.Errorf("failed to generate podman binary: %w, output: %s", err, string(output))) } if os.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH")) != nil { panic("failed to set PATH") } return binDir } ``` -------------------------------------------------------------------------------- /pkg/mcp/podman_image.go: -------------------------------------------------------------------------------- ```go package mcp import ( "context" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) func (s *Server) initPodmanImage() []server.ServerTool { return []server.ServerTool{ {mcp.NewTool("image_build", mcp.WithDescription("Build a Docker or Podman image from a Dockerfile, Podmanfile, or Containerfile"), mcp.WithString("containerFile", mcp.Description("The absolute path to the Dockerfile, Podmanfile, or Containerfile to build the image from"), mcp.Required()), mcp.WithString("imageName", mcp.Description("Specifies the name which is assigned to the resulting image if the build process completes successfully (--tag, -t)")), ), s.imageBuild}, {mcp.NewTool("image_list", mcp.WithDescription("List the Docker or Podman images on the local machine"), ), s.imageList}, {mcp.NewTool("image_pull", mcp.WithDescription("Copies (pulls) a Docker or Podman container image from a registry onto the local machine storage"), mcp.WithString("imageName", mcp.Description("Docker or Podman container image name to pull"), mcp.Required()), ), s.imagePull}, {mcp.NewTool("image_push", mcp.WithDescription("Pushes a Docker or Podman container image, manifest list or image index from local machine storage to a registry"), mcp.WithString("imageName", mcp.Description("Docker or Podman container image name to push"), mcp.Required()), ), s.imagePush}, {mcp.NewTool("image_remove", mcp.WithDescription("Removes a Docker or Podman image from the local machine storage"), mcp.WithString("imageName", mcp.Description("Docker or Podman container image name to remove"), mcp.Required()), ), s.imageRemove}, } } func (s *Server) imageBuild(_ context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { imageName := ctr.GetArguments()["imageName"] if _, ok := imageName.(string); !ok { imageName = "" } return NewTextResult(s.podman.ImageBuild(ctr.GetArguments()["containerFile"].(string), imageName.(string))), nil } func (s *Server) imageList(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { return NewTextResult(s.podman.ImageList()), nil } func (s *Server) imagePull(_ context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { return NewTextResult(s.podman.ImagePull(ctr.GetArguments()["imageName"].(string))), nil } func (s *Server) imagePush(_ context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { return NewTextResult(s.podman.ImagePush(ctr.GetArguments()["imageName"].(string))), nil } func (s *Server) imageRemove(_ context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { return NewTextResult(s.podman.ImageRemove(ctr.GetArguments()["imageName"].(string))), nil } ``` -------------------------------------------------------------------------------- /python/podman_mcp_server/podman_mcp_server.py: -------------------------------------------------------------------------------- ```python import os import platform import subprocess import sys from pathlib import Path import shutil import tempfile import urllib.request if sys.version_info >= (3, 8): from importlib.metadata import version else: from importlib_metadata import version __version__ = version("podman-mcp-server") def get_platform_binary(): """Determine the correct binary for the current platform.""" system = platform.system().lower() arch = platform.machine().lower() # Normalize architecture names if arch in ["x86_64", "amd64"]: arch = "amd64" elif arch in ["arm64", "aarch64"]: arch = "arm64" else: raise RuntimeError(f"Unsupported architecture: {arch}") if system == "darwin": return f"podman-mcp-server-darwin-{arch}" elif system == "linux": return f"podman-mcp-server-linux-{arch}" elif system == "windows": return f"podman-mcp-server-windows-{arch}.exe" else: raise RuntimeError(f"Unsupported operating system: {system}") def download_binary(binary_version="latest", destination=None): """Download the correct binary for the current platform.""" binary_name = get_platform_binary() if destination is None: destination = Path.home() / ".podman-mcp-server" / "bin" / binary_version destination = Path(destination) destination.mkdir(parents=True, exist_ok=True) binary_path = destination / binary_name if binary_path.exists(): return binary_path base_url = "https://github.com/manusa/podman-mcp-server/releases" if binary_version == "latest": release_url = f"{base_url}/latest/download/{binary_name}" else: release_url = f"{base_url}/download/v{binary_version}/{binary_name}" # Download the binary print(f"Downloading {binary_name} from {release_url}") with tempfile.NamedTemporaryFile(delete=False) as temp_file: try: with urllib.request.urlopen(release_url) as response: shutil.copyfileobj(response, temp_file) temp_file.close() # Move to destination and make executable shutil.move(temp_file.name, binary_path) binary_path.chmod(binary_path.stat().st_mode | 0o755) # Make executable return binary_path except Exception as e: os.unlink(temp_file.name) raise RuntimeError(f"Failed to download binary: {e}") def execute(args=None): """Download and execute the podman-mcp-server binary.""" if args is None: args = [] try: binary_path = download_binary(binary_version=__version__) cmd = [str(binary_path)] + args # Execute the binary with the provided arguments process = subprocess.run(cmd) return process.returncode except Exception as e: print(f"Error executing podman-mcp-server: {e}", file=sys.stderr) return 1 if __name__ == "__main__": sys.exit(execute(sys.argv[1:])) def main(): """Main function to execute the podman-mcp-server binary.""" args = sys.argv[1:] if len(sys.argv) > 1 else [] return execute(args) ``` -------------------------------------------------------------------------------- /pkg/mcp/podman_image_test.go: -------------------------------------------------------------------------------- ```go package mcp import ( "github.com/mark3labs/mcp-go/mcp" "strings" "testing" ) func TestImageBuild(t *testing.T) { testCase(t, func(c *mcpContext) { toolResult, err := c.callTool("image_build", map[string]interface{}{ "containerFile": "/tmp/Containerfile", }) t.Run("image_build returns OK", func(t *testing.T) { if err != nil { t.Fatalf("call tool failed %v", err) } if toolResult.IsError { t.Fatalf("call tool failed") } if !strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "podman build -f /tmp/Containerfile") { t.Errorf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text) } }) toolResult, err = c.callTool("image_build", map[string]interface{}{ "containerFile": "/tmp/Containerfile", "imageName": "example.com/org/image:tag", }) t.Run("image_build with imageName returns OK", func(t *testing.T) { if err != nil { t.Fatalf("call tool failed %v", err) } if toolResult.IsError { t.Fatalf("call tool failed") } if !strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "podman build -t example.com/org/image:tag -f /tmp/Containerfile") { t.Errorf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text) } }) }) } func TestImageList(t *testing.T) { testCase(t, func(c *mcpContext) { c.withPodmanOutput( "REPOSITORY\tTAG\tDIGEST\tIMAGE ID\tCREATED\tSIZE", "docker.io/marcnuri/chuck-norris\nlatest\nsha256:1337\nb8f22a2b8410\n1 day ago\n37 MB", ) toolResult, err := c.callTool("image_list", map[string]interface{}{}) t.Run("image_list returns OK", func(t *testing.T) { if err != nil { t.Fatalf("call tool failed %v", err) return } if toolResult.IsError { t.Fatalf("call tool failed") return } if !strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "podman images --digests") { t.Fatalf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text) return } }) }) } func TestImagePull(t *testing.T) { testCase(t, func(c *mcpContext) { toolResult, err := c.callTool("image_pull", map[string]interface{}{ "imageName": "example.com/org/image:tag", }) t.Run("image_pull returns OK", func(t *testing.T) { if err != nil { t.Fatalf("call tool failed %v", err) } if toolResult.IsError { t.Fatalf("call tool failed") } if !strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "podman image pull example.com/org/image:tag") { t.Errorf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text) } if !strings.HasSuffix(toolResult.Content[0].(mcp.TextContent).Text, "example.com/org/image:tag pulled successfully") { t.Errorf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text) } }) }) } func TestImagePush(t *testing.T) { testCase(t, func(c *mcpContext) { toolResult, err := c.callTool("image_push", map[string]interface{}{ "imageName": "example.com/org/image:tag", }) t.Run("image_push returns OK", func(t *testing.T) { if err != nil { t.Fatalf("call tool failed %v", err) } if toolResult.IsError { t.Fatalf("call tool failed") } if !strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "podman image push example.com/org/image:tag") { t.Errorf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text) } if !strings.HasSuffix(toolResult.Content[0].(mcp.TextContent).Text, "example.com/org/image:tag pushed successfully") { t.Errorf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text) } }) }) } func TestImageRemove(t *testing.T) { testCase(t, func(c *mcpContext) { toolResult, err := c.callTool("image_remove", map[string]interface{}{ "imageName": "example.com/org/image:tag", }) t.Run("image_remove returns OK", func(t *testing.T) { if err != nil { t.Fatalf("call tool failed %v", err) } if toolResult.IsError { t.Fatalf("call tool failed") } if !strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "podman image rm example.com/org/image:tag") { t.Errorf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text) } }) }) } ``` -------------------------------------------------------------------------------- /pkg/podman/podman_cli.go: -------------------------------------------------------------------------------- ```go package podman import ( "errors" "fmt" "os/exec" "strings" ) type podmanCli struct { filePath string } // ContainerInspect // https://docs.podman.io/en/stable/markdown/podman-inspect.1.html func (p *podmanCli) ContainerInspect(name string) (string, error) { return p.exec("inspect", name) } // ContainerList // https://docs.podman.io/en/stable/markdown/podman-ps.1.html func (p *podmanCli) ContainerList() (string, error) { return p.exec("container", "list", "-a") } // ContainerLogs // https://docs.podman.io/en/stable/markdown/podman-logs.1.html func (p *podmanCli) ContainerLogs(name string) (string, error) { return p.exec("logs", name) } // ContainerRemove // https://docs.podman.io/en/stable/markdown/podman-rm.1.html func (p *podmanCli) ContainerRemove(name string) (string, error) { return p.exec("container", "rm", name) } // ContainerRun // https://docs.podman.io/en/stable/markdown/podman-run.1.html func (p *podmanCli) ContainerRun(imageName string, portMappings map[int]int, envVariables []string) (string, error) { args := []string{"run", "--rm", "-d"} if len(portMappings) > 0 { for hostPort, containerPort := range portMappings { args = append(args, fmt.Sprintf("--publish=%d:%d", hostPort, containerPort)) } } else { args = append(args, "--publish-all") } for _, env := range envVariables { args = append(args, "--env", env) } output, err := p.exec(append(args, imageName)...) if err == nil { return output, nil } if strings.Contains(output, "Error: short-name") { imageName = "docker.io/" + imageName if output, err = p.exec(append(args, imageName)...); err == nil { return output, nil } } return "", err } // ContainerStop // https://docs.podman.io/en/stable/markdown/podman-stop.1.html func (p *podmanCli) ContainerStop(name string) (string, error) { return p.exec("container", "stop", name) } // ImageBuild // https://docs.podman.io/en/stable/markdown/podman-build.1.html func (p *podmanCli) ImageBuild(containerFile string, imageName string) (string, error) { args := []string{"build"} if imageName != "" { args = append(args, "-t", imageName) } return p.exec(append(args, "-f", containerFile)...) } // ImageList // https://docs.podman.io/en/stable/markdown/podman-images.1.html func (p *podmanCli) ImageList() (string, error) { return p.exec("images", "--digests") } // ImagePull // https://docs.podman.io/en/stable/markdown/podman-pull.1.html func (p *podmanCli) ImagePull(imageName string) (string, error) { output, err := p.exec("image", "pull", imageName) if err == nil { return fmt.Sprintf("%s\n%s pulled successfully", output, imageName), nil } if strings.Contains(output, "Error: short-name") { imageName = "docker.io/" + imageName if output, err = p.exec("pull", imageName); err == nil { return fmt.Sprintf("%s\n%s pulled successfully", output, imageName), nil } } return "", err } // ImagePush // https://docs.podman.io/en/stable/markdown/podman-push.1.html func (p *podmanCli) ImagePush(imageName string) (string, error) { output, err := p.exec("image", "push", imageName) if err == nil { return fmt.Sprintf("%s\n%s pushed successfully", output, imageName), nil } return "", err } // ImageRemove // https://docs.podman.io/en/stable/markdown/podman-rmi.1.html func (p *podmanCli) ImageRemove(imageName string) (string, error) { return p.exec("image", "rm", imageName) } // NetworkList // https://docs.podman.io/en/stable/markdown/podman-network-ls.1.html func (p *podmanCli) NetworkList() (string, error) { return p.exec("network", "ls") } // VolumeList // https://docs.podman.io/en/stable/markdown/podman-volume-ls.1.html func (p *podmanCli) VolumeList() (string, error) { return p.exec("volume", "ls") } func (p *podmanCli) exec(args ...string) (string, error) { output, err := exec.Command(p.filePath, args...).CombinedOutput() return string(output), err } func newPodmanCli() (*podmanCli, error) { for _, cmd := range []string{"podman", "podman.exe"} { filePath, err := exec.LookPath(cmd) if err != nil { continue } if _, err = exec.Command(filePath, "version").CombinedOutput(); err == nil { return &podmanCli{filePath}, nil } } return nil, errors.New("podman CLI not found") } ``` -------------------------------------------------------------------------------- /pkg/mcp/podman_container.go: -------------------------------------------------------------------------------- ```go package mcp import ( "context" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" "strconv" "strings" ) func (s *Server) initPodmanContainer() []server.ServerTool { return []server.ServerTool{ {mcp.NewTool("container_inspect", mcp.WithDescription("Displays the low-level information and configuration of a Docker or Podman container with the specified container ID or name"), mcp.WithString("name", mcp.Description("Docker or Podman container ID or name to displays the information"), mcp.Required()), ), s.containerInspect}, {mcp.NewTool("container_list", mcp.WithDescription("Prints out information about the running Docker or Podman containers"), ), s.containerList}, {mcp.NewTool("container_logs", mcp.WithDescription("Displays the logs of a Docker or Podman container with the specified container ID or name"), mcp.WithString("name", mcp.Description("Docker or Podman container ID or name to displays the logs"), mcp.Required()), ), s.containerLogs}, {mcp.NewTool("container_remove", mcp.WithDescription("Removes a Docker or Podman container with the specified container ID or name (rm)"), mcp.WithString("name", mcp.Description("Docker or Podman container ID or name to remove"), mcp.Required()), ), s.containerRemove}, {mcp.NewTool("container_run", mcp.WithDescription("Runs a Docker or Podman container with the specified image name"), mcp.WithString("imageName", mcp.Description("Docker or Podman container image name to pull"), mcp.Required()), mcp.WithArray("ports", mcp.Description("Port mappings to expose on the host. "+ "Format: <hostPort>:<containerPort>. "+ "Example: 8080:80. "+ "(Optional, add only to expose ports)"), // TODO: manual fix to ensure that the items property gets initialized (Gemini) // https://www.googlecloudcommunity.com/gc/AI-ML/Gemini-API-400-Bad-Request-Array-fields-breaks-function-calling/m-p/769835?nobounce func(schema map[string]interface{}) { schema["type"] = "array" schema["items"] = map[string]interface{}{ "type": "string", } }, ), mcp.WithArray("environment", mcp.Description("Environment variables to set in the container. "+ "Format: <key>=<value>. "+ "Example: FOO=bar. "+ "(Optional, add only to set environment variables)"), // TODO: manual fix to ensure that the items property gets initialized (Gemini) // https://www.googlecloudcommunity.com/gc/AI-ML/Gemini-API-400-Bad-Request-Array-fields-breaks-function-calling/m-p/769835?nobounce func(schema map[string]interface{}) { schema["type"] = "array" schema["items"] = map[string]interface{}{ "type": "string", } }, ), ), s.containerRun}, {mcp.NewTool("container_stop", mcp.WithDescription("Stops a Docker or Podman running container with the specified container ID or name"), mcp.WithString("name", mcp.Description("Docker or Podman container ID or name to stop"), mcp.Required()), ), s.containerStop}, } } func (s *Server) containerInspect(_ context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { return NewTextResult(s.podman.ContainerInspect(ctr.GetArguments()["name"].(string))), nil } func (s *Server) containerList(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { return NewTextResult(s.podman.ContainerList()), nil } func (s *Server) containerLogs(_ context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { return NewTextResult(s.podman.ContainerLogs(ctr.GetArguments()["name"].(string))), nil } func (s *Server) containerRemove(_ context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { return NewTextResult(s.podman.ContainerRemove(ctr.GetArguments()["name"].(string))), nil } func (s *Server) containerRun(_ context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { ports := ctr.GetArguments()["ports"] portMappings := make(map[int]int) if _, ok := ports.([]interface{}); ok { for _, port := range ports.([]interface{}) { if _, ok := port.(string); !ok { continue } hostPort, _ := strconv.Atoi(strings.Split(port.(string), ":")[0]) containerPort, _ := strconv.Atoi(strings.Split(port.(string), ":")[1]) if hostPort > 0 && containerPort > 0 { portMappings[hostPort] = containerPort } } } environment := ctr.GetArguments()["environment"] envVariables := make([]string, 0) if _, ok := environment.([]interface{}); ok && len(environment.([]interface{})) > 0 { for _, env := range environment.([]interface{}) { if _, ok = env.(string); !ok { continue } envVariables = append(envVariables, env.(string)) } } return NewTextResult(s.podman.ContainerRun(ctr.GetArguments()["imageName"].(string), portMappings, envVariables)), nil } func (s *Server) containerStop(_ context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { return NewTextResult(s.podman.ContainerStop(ctr.GetArguments()["name"].(string))), nil } ``` -------------------------------------------------------------------------------- /pkg/mcp/podman_container_test.go: -------------------------------------------------------------------------------- ```go package mcp import ( "github.com/mark3labs/mcp-go/mcp" "strings" "testing" ) func TestContainerInspect(t *testing.T) { testCase(t, func(c *mcpContext) { toolResult, err := c.callTool("container_inspect", map[string]interface{}{ "name": "example-container", }) t.Run("container_inspect returns OK", func(t *testing.T) { if err != nil { t.Fatalf("call tool failed %v", err) } if toolResult.IsError { t.Fatalf("call tool failed") } }) t.Run("container_inspect inspects provided container", func(t *testing.T) { if !strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "podman inspect example-container") { t.Fatalf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text) } }) }) } func TestContainerList(t *testing.T) { testCase(t, func(c *mcpContext) { toolResult, err := c.callTool("container_list", map[string]interface{}{}) t.Run("container_list returns OK", func(t *testing.T) { if err != nil { t.Fatalf("call tool failed %v", err) } if toolResult.IsError { t.Fatalf("call tool failed") } if !strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "podman container list -a") { t.Fatalf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text) } }) }) } func TestContainerLogs(t *testing.T) { testCase(t, func(c *mcpContext) { toolResult, err := c.callTool("container_logs", map[string]interface{}{ "name": "example-container", }) t.Run("container_logs returns OK", func(t *testing.T) { if err != nil { t.Fatalf("call tool failed %v", err) } if toolResult.IsError { t.Fatalf("call tool failed") } }) t.Run("container_logs retrieves logs from provided container", func(t *testing.T) { if !strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "podman logs example-container") { t.Fatalf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text) } }) }) } func TestContainerRemove(t *testing.T) { testCase(t, func(c *mcpContext) { toolResult, err := c.callTool("container_remove", map[string]interface{}{ "name": "example-container", }) t.Run("container_remove returns OK", func(t *testing.T) { if err != nil { t.Fatalf("call tool failed %v", err) } if toolResult.IsError { t.Fatalf("call tool failed") } }) t.Run("container_remove removes provided container", func(t *testing.T) { if !strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "podman container rm example-container") { t.Fatalf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text) } }) }) } func TestContainerRun(t *testing.T) { testCase(t, func(c *mcpContext) { toolResult, err := c.callTool("container_run", map[string]interface{}{ "imageName": "example.com/org/image:tag", }) t.Run("container_run returns OK", func(t *testing.T) { if err != nil { t.Fatalf("call tool failed %v", err) } if toolResult.IsError { t.Fatalf("call tool failed") } }) t.Run("container_run runs provided image", func(t *testing.T) { if !strings.HasSuffix(toolResult.Content[0].(mcp.TextContent).Text, " example.com/org/image:tag\n") { t.Fatalf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text) } }) t.Run("container_run runs in detached mode", func(t *testing.T) { if !strings.Contains(toolResult.Content[0].(mcp.TextContent).Text, " -d ") { t.Fatalf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text) } }) t.Run("container_run publishes all exposed ports", func(t *testing.T) { if !strings.Contains(toolResult.Content[0].(mcp.TextContent).Text, " --publish-all ") { t.Fatalf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text) } }) toolResult, err = c.callTool("container_run", map[string]interface{}{ "imageName": "example.com/org/image:tag", "ports": []interface{}{ 1337, // Invalid entry to test "8080:80", "8082:8082", "8443:443", }, }) t.Run("container_run with ports returns OK", func(t *testing.T) { if err != nil { t.Fatalf("call tool failed %v", err) } if toolResult.IsError { t.Fatalf("call tool failed") } }) t.Run("container_run with ports publishes provided ports", func(t *testing.T) { if !strings.Contains(toolResult.Content[0].(mcp.TextContent).Text, " --publish=8080:80 ") { t.Fatalf("expected port --publish=8080:80, got %v", toolResult.Content[0].(mcp.TextContent).Text) } if !strings.Contains(toolResult.Content[0].(mcp.TextContent).Text, " --publish=8082:8082 ") { t.Fatalf("expected port --publish=8082:8082, got %v", toolResult.Content[0].(mcp.TextContent).Text) } if !strings.Contains(toolResult.Content[0].(mcp.TextContent).Text, " --publish=8443:443 ") { t.Fatalf("expected port --publish=8443:443, got %v", toolResult.Content[0].(mcp.TextContent).Text) } }) toolResult, err = c.callTool("container_run", map[string]interface{}{ "imageName": "example.com/org/image:tag", "ports": []interface{}{"8080:80"}, "environment": []interface{}{ "KEY=VALUE", "FOO=BAR", }, }) t.Run("container_run with environment returns OK", func(t *testing.T) { if err != nil { t.Fatalf("call tool failed %v", err) } if toolResult.IsError { t.Fatalf("call tool failed") } }) t.Run("container_run with environment sets provided environment variables", func(t *testing.T) { if !strings.Contains(toolResult.Content[0].(mcp.TextContent).Text, " --env KEY=VALUE ") { t.Fatalf("expected env --env KEY=VALUE, got %v", toolResult.Content[0].(mcp.TextContent).Text) } if !strings.Contains(toolResult.Content[0].(mcp.TextContent).Text, " --env FOO=BAR ") { t.Fatalf("expected env --env FOO=BAR, got %v", toolResult.Content[0].(mcp.TextContent).Text) } }) }) } func TestContainerStop(t *testing.T) { testCase(t, func(c *mcpContext) { toolResult, err := c.callTool("container_stop", map[string]interface{}{ "name": "example-container", }) t.Run("container_stop returns OK", func(t *testing.T) { if err != nil { t.Fatalf("call tool failed %v", err) } if toolResult.IsError { t.Fatalf("call tool failed") } }) t.Run("container_stop stops provided container", func(t *testing.T) { if !strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "podman container stop example-container") { t.Fatalf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text) } }) }) } ```