Development Workflow
Environment Setup
Section titled “Environment Setup”-
Install Go 1.24+
Terminal window # macOSbrew install go# Linux (snap)sudo snap install go --classic# Or download from https://go.dev/dl/ -
Install Docker (for integration tests)
Follow Docker’s installation guide for your OS.
-
Clone the repository
Terminal window git clone https://github.com/jmrplens/portainer-mcp-enhanced.gitcd portainer-mcp-enhanced -
Verify setup
Terminal window go version # Should show 1.24+make build # Should produce dist/portainer-mcp-enhancedmake test # Should pass all unit tests
Makefile Targets
Section titled “Makefile Targets”| Target | Description | Duration |
|---|---|---|
make build | Build for current OS/arch | ~3s |
make release | Production build with -trimpath | ~3s |
make test | Unit tests only | ~5s |
make test-integration | Integration tests (requires Docker) | ~60s |
make test-all | Unit + integration | ~65s |
make test-coverage | Unit tests with coverage → coverage.out | ~5s |
make fmt | Format all Go files (gofmt -s -w .) | <1s |
make vet | Static analysis (go vet ./...) | ~2s |
make lint | go vet + golangci-lint (if installed) | ~5s |
make inspector | Build + launch MCP Inspector UI | ~5s |
make clean | Remove dist/ directory | <1s |
Cross-compilation
Section titled “Cross-compilation”make PLATFORM=linux ARCH=arm64 buildmake PLATFORM=darwin ARCH=amd64 buildmake PLATFORM=windows ARCH=amd64 buildTesting
Section titled “Testing”Unit Tests
Section titled “Unit Tests”All handlers have corresponding _test.go files in internal/mcp/. Tests use a shared mock in mocks_test.go.
# All unit testsmake test
# Specific packagego test -v ./internal/mcp/ -run TestHandleGetEnvironments
# With coveragemake test-coveragego tool cover -html=coverage.out # Open in browserTest 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
Section titled “Integration Tests”Integration tests spin up a real Portainer instance using testcontainers-go:
make test-integrationHow they work:
helpers.SetupTestEnv(t)starts a Portainer Docker container- Creates both a raw SDK client and an MCP server connected to it
- Tests call MCP handlers and compare results with direct API calls
- 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 // ...}MCP Inspector
Section titled “MCP Inspector”The MCP Inspector is a web UI for testing MCP servers interactively:
make inspectorThis 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
Debugging
Section titled “Debugging”Logging
Section titled “Logging”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:
export LOG_LEVEL=debug # trace, debug, info, warn, errorCommon Issues
Section titled “Common Issues”| Problem | Solution |
|---|---|
version mismatch error | Your Portainer version doesn’t match the supported version. Use -disable-version-check or upgrade Portainer |
TLS handshake failure | Use -skip-tls-verify for self-signed certificates |
| Empty tool list | Check that tools.yaml is embedded correctly (go build from repo root) |
| Integration tests fail | Ensure Docker daemon is running and you have network access |
permission denied on binary | Run chmod +x dist/portainer-mcp-enhanced |
Documentation Site
Section titled “Documentation Site”The docs are built with Starlight (Astro):
cd docspnpm installpnpm run dev # http://localhost:4321pnpm run build # Production build → dist/Content lives in docs/src/content/docs/ as .mdx files. The sidebar is configured in docs/astro.config.mjs.
Release Process
Section titled “Release Process”Releases are automated via GoReleaser:
- Tag a new version:
git tag v0.6.1 - Push the tag:
git push origin v0.6.1 - The
release.ymlworkflow builds binaries for 6 platforms, creates Docker images, and publishes a GitHub Release with checksums
See .goreleaser.yaml for the full configuration.