Skip to main content
Glama

Portainer MCP

Official
by portainer
zlib License
67
  • Linux
  • Apple
portainer.go6.26 kB
package containers import ( "context" "crypto/tls" "fmt" "net/http" "time" "github.com/docker/docker/api/types/container" "github.com/docker/go-connections/nat" "github.com/go-openapi/runtime" httptransport "github.com/go-openapi/runtime/client" "github.com/go-openapi/strfmt" "github.com/portainer/client-api-go/v2/pkg/client" "github.com/portainer/client-api-go/v2/pkg/client/auth" "github.com/portainer/client-api-go/v2/pkg/client/users" "github.com/portainer/client-api-go/v2/pkg/models" "github.com/portainer/portainer-mcp/internal/mcp" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" ) const ( defaultPortainerImage = "portainer/portainer-ee:" + mcp.SupportedPortainerVersion defaultAPIPortTCP = "9443/tcp" adminPassword = "$2y$05$CiHrhW6R6whDVlu7Wdgl0eccb3rg1NWl/mMiO93vQiRIF1SHNFRsS" // Bcrypt hash of "adminpassword123" // Timeout for the container to start and be ready to use startupTimeout = time.Second * 5 ) // PortainerContainer represents a Portainer container for testing type PortainerContainer struct { testcontainers.Container APIPort nat.Port APIHost string apiToken string } // portainerContainerConfig holds the configuration for creating a Portainer container type portainerContainerConfig struct { Image string BindDockerSocket bool } // PortainerContainerOption defines a function type for applying options to Portainer container configuration type PortainerContainerOption func(*portainerContainerConfig) // WithImage sets a custom Portainer image func WithImage(image string) PortainerContainerOption { return func(cfg *portainerContainerConfig) { cfg.Image = image } } // WithDockerSocketBind configures the container to bind mount the Docker socket func WithDockerSocketBind(bind bool) PortainerContainerOption { return func(cfg *portainerContainerConfig) { cfg.BindDockerSocket = bind } } // NewPortainerContainer creates and starts a new Portainer container with the specified options func NewPortainerContainer(ctx context.Context, opts ...PortainerContainerOption) (*PortainerContainer, error) { // Default configuration cfg := &portainerContainerConfig{ Image: defaultPortainerImage, BindDockerSocket: false, } // Apply provided options for _, opt := range opts { opt(cfg) } // Container request configuration req := testcontainers.ContainerRequest{ Image: cfg.Image, ExposedPorts: []string{defaultAPIPortTCP}, WaitingFor: wait.ForAll( // Wait for the HTTPS server to start wait.ForLog("starting HTTPS server"). WithStartupTimeout(startupTimeout), // Then wait for the API to be responsive wait.ForHTTP("/api/system/status"). WithTLS(true, nil). WithAllowInsecure(true). WithPort(defaultAPIPortTCP). WithStatusCodeMatcher( func(status int) bool { return status == http.StatusOK }, ). WithStartupTimeout(startupTimeout), ), Cmd: []string{ "--admin-password", adminPassword, "--log-level", "DEBUG", }, HostConfigModifier: func(hostConfig *container.HostConfig) { if cfg.BindDockerSocket { hostConfig.Binds = append(hostConfig.Binds, "/var/run/docker.sock:/var/run/docker.sock") } }, } // Create and start the container cntr, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: req, Started: true, }) if err != nil { return nil, fmt.Errorf("failed to start Portainer container: %w", err) } // Get the host and port mapping host, err := cntr.Host(ctx) if err != nil { cntr.Terminate(ctx) // Clean up if we fail post-start return nil, fmt.Errorf("failed to get container host: %w", err) } mappedPort, err := cntr.MappedPort(ctx, nat.Port(defaultAPIPortTCP)) if err != nil { cntr.Terminate(ctx) // Clean up if we fail post-start return nil, fmt.Errorf("failed to get mapped port: %w", err) } pc := &PortainerContainer{ Container: cntr, APIPort: mappedPort, APIHost: host, } // Register API token after successful container start and port mapping if err := pc.registerAPIToken(); err != nil { // Attempt to clean up the container if token registration fails cntr.Terminate(ctx) return nil, fmt.Errorf("failed to register API token: %w", err) } return pc, nil } // GetAPIBaseURL returns the base URL for the Portainer API func (pc *PortainerContainer) GetAPIBaseURL() string { return fmt.Sprintf("https://%s:%s", pc.APIHost, pc.APIPort.Port()) } // GetHostAndPort returns the host and port for the Portainer API func (pc *PortainerContainer) GetHostAndPort() (string, string) { return pc.APIHost, pc.APIPort.Port() } func (pc *PortainerContainer) GetAPIToken() string { return pc.apiToken } // registerAPIToken registers an API token for the admin user func (pc *PortainerContainer) registerAPIToken() error { transport := httptransport.New( fmt.Sprintf("%s:%s", pc.APIHost, pc.APIPort.Port()), "/api", []string{"https"}, ) transport.Transport = &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, }, } portainerClient := client.New(transport, strfmt.Default) username := "admin" password := "adminpassword123" params := auth.NewAuthenticateUserParams().WithBody(&models.AuthAuthenticatePayload{ Username: &username, Password: &password, }) authResp, err := portainerClient.Auth.AuthenticateUser(params) if err != nil { return fmt.Errorf("failed to authenticate user: %w", err) } token := authResp.Payload.Jwt // Setup JWT authentication jwtAuth := runtime.ClientAuthInfoWriterFunc(func(r runtime.ClientRequest, _ strfmt.Registry) error { return r.SetHeaderParam("Authorization", fmt.Sprintf("Bearer %s", token)) }) transport.DefaultAuthentication = jwtAuth description := "test-api-key" createTokenParams := users.NewUserGenerateAPIKeyParams().WithID(1).WithBody(&models.UsersUserAccessTokenCreatePayload{ Description: &description, Password: &password, }) createTokenResp, err := portainerClient.Users.UserGenerateAPIKey(createTokenParams, nil) if err != nil { return fmt.Errorf("failed to generate API key: %w", err) } pc.apiToken = createTokenResp.Payload.RawAPIKey return nil }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/portainer/portainer-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server