Skip to content

Development Workflow

  1. Install Go 1.24+

    Terminal window
    # macOS
    brew install go
    # Linux (snap)
    sudo snap install go --classic
    # Or download from https://go.dev/dl/
  2. Install Docker (for integration tests)

    Follow Docker’s installation guide for your OS.

  3. Clone the repository

    Terminal window
    git clone https://github.com/jmrplens/portainer-mcp-enhanced.git
    cd portainer-mcp-enhanced
  4. Verify setup

    Terminal window
    go version # Should show 1.24+
    make build # Should produce dist/portainer-mcp-enhanced
    make test # Should pass all unit tests
TargetDescriptionDuration
make buildBuild for current OS/arch~3s
make releaseProduction build with -trimpath~3s
make testUnit tests only~5s
make test-integrationIntegration tests (requires Docker)~60s
make test-allUnit + integration~65s
make test-coverageUnit tests with coverage → coverage.out~5s
make fmtFormat all Go files (gofmt -s -w .)<1s
make vetStatic analysis (go vet ./...)~2s
make lintgo vet + golangci-lint (if installed)~5s
make inspectorBuild + launch MCP Inspector UI~5s
make cleanRemove dist/ directory<1s
Terminal window
make PLATFORM=linux ARCH=arm64 build
make PLATFORM=darwin ARCH=amd64 build
make PLATFORM=windows ARCH=amd64 build

All handlers have corresponding _test.go files in internal/mcp/. Tests use a shared mock in mocks_test.go.

Terminal window
# All unit tests
make test
# Specific package
go test -v ./internal/mcp/ -run TestHandleGetEnvironments
# With coverage
make test-coverage
go tool cover -html=coverage.out # Open in browser

Test pattern: Table-driven tests with descriptive case names:

func TestHandleGetEnvironments(t *testing.T) {
tests := []struct {
name string
mockSetup func(*MockPortainerClient)
wantErr bool
}{
{
name: "returns all environments",
mockSetup: func(m *MockPortainerClient) {
m.On("GetEnvironments").Return([]models.Environment{{ID: 1, Name: "local"}}, nil)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockClient := &MockPortainerClient{}
if tt.mockSetup != nil {
tt.mockSetup(mockClient)
}
s := &PortainerMCPServer{cli: mockClient}
result, err := s.HandleGetEnvironments()(context.Background(), CreateMCPRequest(nil))
assert.NoError(t, err)
assert.NotNil(t, result)
mockClient.AssertExpectations(t)
})
}
}

Integration tests spin up a real Portainer instance using testcontainers-go:

Terminal window
make test-integration

How they work:

  1. helpers.SetupTestEnv(t) starts a Portainer Docker container
  2. Creates both a raw SDK client and an MCP server connected to it
  3. Tests call MCP handlers and compare results with direct API calls
  4. All resources are cleaned up on test completion
func TestIntegrationEnvironments(t *testing.T) {
env := helpers.SetupTestEnv(t)
defer env.Cleanup()
// Call MCP handler
result, err := env.MCPServer.HandleGetEnvironments(map[string]interface{}{})
require.NoError(t, err)
// Compare with raw client
rawEnvs, err := env.RawClient.GetEndpoints()
require.NoError(t, err)
// Assert equivalence
// ...
}

The MCP Inspector is a web UI for testing MCP servers interactively:

Terminal window
make inspector

This builds the binary and launches the inspector at http://localhost:5173. You can:

  • Browse all registered tools
  • Invoke tools with custom parameters
  • Inspect JSON responses
  • Test meta-tool action routing

The server uses zerolog for structured logging to stderr (stdout is reserved for MCP protocol):

import "github.com/rs/zerolog/log"
log.Info().Str("tool", toolName).Msg("executing tool")
log.Error().Err(err).Msg("failed to get environment")

Set log level via environment variable:

Terminal window
export LOG_LEVEL=debug # trace, debug, info, warn, error
ProblemSolution
version mismatch errorYour Portainer version doesn’t match the supported version. Use -disable-version-check or upgrade Portainer
TLS handshake failureUse -skip-tls-verify for self-signed certificates
Empty tool listCheck that tools.yaml is embedded correctly (go build from repo root)
Integration tests failEnsure Docker daemon is running and you have network access
permission denied on binaryRun chmod +x dist/portainer-mcp-enhanced

The docs are built with Starlight (Astro):

Terminal window
cd docs
pnpm install
pnpm run dev # http://localhost:4321
pnpm run build # Production build → dist/

Content lives in docs/src/content/docs/ as .mdx files. The sidebar is configured in docs/astro.config.mjs.

Releases are automated via GoReleaser:

  1. Tag a new version: git tag v0.6.1
  2. Push the tag: git push origin v0.6.1
  3. The release.yml workflow builds binaries for 6 platforms, creates Docker images, and publishes a GitHub Release with checksums

See .goreleaser.yaml for the full configuration.