# 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)
}
})
})
}
```