### Initialize and Start Portainer MCP Server Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Initializes the Portainer MCP server, checks for the existence of the tools.yaml file (creating it if necessary), and starts the server with configured features. This is the main entry point for the MCP server. ```go func main() { // We first check if the tools.yaml file exists // We'll create it from the embedded version if it doesn't exist exists, err := tooldef.CreateToolsFileIfNotExists(toolsPath) if err != nil { log.Fatal().Err(err).Msg("failed to create tools.yaml file") } if exists { log.Info().Msg("using existing tools.yaml file") } else { log.Info().Msg("created tools.yaml file") } log.Info(). Str("portainer-host", *serverFlag). Str("tools-path", toolsPath). Bool("read-only", *readOnlyFlag). Msg("starting MCP server") server, err := mcp.NewPortainerMCPServer(*serverFlag, *tokenFlag, toolsPath, mcp.WithReadOnly(*readOnlyFlag)) if err != nil { log.Fatal().Err(err).Msg("failed to create server") } server.AddEnvironmentFeatures() server.AddEnvironmentGroupFeatures() server.AddTagFeatures() server.AddStackFeatures() server.AddSettingsFeatures() server.AddUserFeatures() server.AddTeamFeatures() server.AddAccessGroupFeatures() server.AddDockerProxyFeatures() err = server.Start() if err != nil { log.Fatal().Err(err).Msg("failed to start server") } } ``` -------------------------------- ### Install Dependencies and Run Tests Source: https://github.com/portainer/portainer-mcp/blob/main/CLAUDE.md Use 'uv sync' to install dependencies from uv.lock. Run the full test suite with 'uv run pytest'. ```bash uv sync uv run pytest ``` -------------------------------- ### Start and Retrieve Portainer Container Details Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Creates and starts a Portainer container using the defined request. It then retrieves the container's host and mapped API port, performing cleanup if errors occur during these steps. ```go // 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 ``` -------------------------------- ### List Helm Releases and Get Manifest Source: https://github.com/portainer/portainer-mcp/blob/main/skills/portainer-mcp-hygiene/SKILL.md Use `HelmList` to get release status and chart metadata. Use `HelmGet` to retrieve the rendered manifest for a specific release. Project to release names and status when listing; fetch the manifest only when needed. ```Go HelmList() HelmGet(releaseName) ``` -------------------------------- ### Start Local Development Server Source: https://github.com/portainer/portainer-mcp/blob/main/CLAUDE.md Run the local HTTP server using 'make dev'. This requires a .env file copied from .env.example and runs on port 17717. ```bash make dev ``` -------------------------------- ### JSON Logging Example Source: https://github.com/portainer/portainer-mcp/blob/main/docs/configuration.md Demonstrates the structure of JSON-formatted log lines for Portainer MCP, including startup, audit, and request success events. ```json {"ts": "2026-05-25T12:00:00+0000", "level": "INFO", "logger": "portainer_mcp", "msg": "HTTP auth: per-user passthrough (gate abcd…wxyz, validation cache ttl=60s)"} {"ts": "2026-05-25T12:00:01+0000", "level": "INFO", "logger": "portainer_mcp.audit", "event": "auth", "outcome": "ok", "client_ip": "203.0.113.7", "user_agent": "Claude-Code/1.2.3", "portainer_user_id": 1, "portainer_username": "admin"} {"ts": "2026-05-25T12:00:01+0000", "level": "INFO", "logger": "fastmcp.middleware.structured_logging", "event": "request_success", "method": "tools/call", "tool": "EndpointList", "duration_ms": 42.3, "client_ip": "203.0.113.7", "user_agent": "Claude-Code/1.2.3", "session_id": "abf3…", "portainer_user_id": 1, "portainer_username": "admin"} ``` -------------------------------- ### Pod summary via kubernetes_proxy Source: https://github.com/portainer/portainer-mcp/blob/main/skills/portainer-mcp-hygiene/SKILL.md Get a summary of pods including name, namespace, phase, restart count, and node. Note that restartCount can be null for pods that have not started a container. ```go kubernetes_proxy(environment_id=N, path="/api/v1/pods", select="items[].{name:metadata.name,ns:metadata.namespace,phase:status.phase,restarts:status.containerStatuses[0].restartCount,node:spec.nodeName}") ``` -------------------------------- ### Get Portainer Settings Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Retrieves the Portainer settings from the server. ```go func (c *PortainerClient) GetSettings() (models.PortainerSettings, error) { settings, err := c.cli.GetSettings() if err != nil { return models.PortainerSettings{}, fmt.Errorf("failed to get settings: %w", err) } return models.ConvertSettingsToPortainerSettings(settings), nil } ``` -------------------------------- ### Generate API Key using Portainer Go SDK Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html This snippet demonstrates how to set up JWT authentication and generate an API key using the Portainer Go SDK. It handles the client setup and API call, noting a known issue with the SDK's error handling. ```go // 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) // Because of the issue with the client-api-go, this will return an error even though the API key is created if err != nil { return fmt.Errorf("failed to generate API key: %w", err) } pc.apiToken = createTokenResp.Payload.RawAPIKey ``` -------------------------------- ### Create New Portainer Container Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Creates and starts a new Portainer container using provided options. It applies default configurations and then applies any custom options passed. ```go 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) } // ... rest of the function ``` -------------------------------- ### Start Portainer MCP Server Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Begins listening for MCP protocol messages on standard input/output. This is a blocking call that will run until the connection is closed. ```go func (s *PortainerMCPServer) Start() error { return server.ServeStdio(s.srv) } ``` -------------------------------- ### Get All Environment Tags Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Retrieves all environment tags from the Portainer server. Environment tags are the equivalent of Tags in Portainer. ```go func (c *PortainerClient) GetEnvironmentTags() ([]models.EnvironmentTag, error) { tags, err := c.cli.ListTags() if err != nil { return nil, fmt.Errorf("failed to list environment tags: %w", err) } environmentTags := make([]models.EnvironmentTag, len(tags)) for i, tag := range tags { environmentTags[i] = models.ConvertTagToEnvironmentTag(tag) } return environmentTags, nil } ``` -------------------------------- ### Get All Portainer Environments Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Retrieves a list of all environments managed by Portainer. This is useful for obtaining an overview of all connected environments. ```go func (c *PortainerClient) GetEnvironments() ([]models.Environment, error) { endpoints, err := c.cli.ListEndpoints() if err != nil { return nil, fmt.Errorf("failed to list endpoints: %w", err) } environments := make([]models.Environment, len(endpoints)) for i, endpoint := range endpoints { environments[i] = models.ConvertEndpointToEnvironment(endpoint) } return environments, nil } ``` -------------------------------- ### Get Portainer API Host and Port Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Retrieves the host and port information for the Portainer API. ```go // GetHostAndPort returns the host and port for the Portainer API func (pc *PortainerContainer) GetHostAndPort() (string, string) { return pc.APIHost, pc.APIPort.Port() } ``` -------------------------------- ### Handle Get Teams in Go Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Handles retrieving all teams. Fetches teams from the client, marshals them to JSON, and returns the JSON string. ```go func (s *PortainerMCPServer) HandleGetTeams() server.ToolHandlerFunc { return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { teams, err := s.cli.GetTeams() if err != nil { return nil, fmt.Errorf("failed to get teams: %w", err) } data, err := json.Marshal(teams) if err != nil { return nil, fmt.Errorf("failed to marshal teams: %w", err) } return mcp.NewToolResultText(string(data)), nil } } ``` -------------------------------- ### NewTestEnv Function for Portainer Test Environment Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Creates a new test environment with a Portainer container and clients. It handles starting the container, initializing clients, and setting up the MCP server. ```go // NewTestEnv creates a new test environment with Portainer container and clients func NewTestEnv(t *testing.T, opts ...containers.PortainerContainerOption) *TestEnv { ctx := context.Background() portainer, err := containers.NewPortainerContainer(ctx, opts...) require.NoError(t, err, "Failed to start Portainer container") host, port := portainer.GetHostAndPort() serverURL := fmt.Sprintf("%s:%s", host, port) rawCli := client.NewPortainerClient( serverURL, portainer.GetAPIToken(), client.WithSkipTLSVerify(true), ) mcpServer, err := mcp.NewPortainerMCPServer(serverURL, portainer.GetAPIToken(), ToolsPath) require.NoError(t, err, "Failed to create MCP server") return &TestEnv{ Ctx: ctx, Portainer: portainer, RawClient: rawCli, MCPServer: mcpServer, } } ``` -------------------------------- ### Handle Get Environment Tags in Go Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Retrieves a list of all environment tags from the Portainer MCP. Handles potential errors during the retrieval process. ```Go func (s *PortainerMCPServer) HandleGetEnvironmentTags() server.ToolHandlerFunc { return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { environmentTags, err := s.cli.GetEnvironmentTags() if err != nil { return nil, fmt.Errorf("failed to get environment tags: %w", err) } ``` -------------------------------- ### Handle Get Environments Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Retrieves a list of environments from the client, marshals them into JSON, and returns them as a text result. Handles errors during environment retrieval or JSON marshaling. ```Go func (s *PortainerMCPServer) HandleGetEnvironments() server.ToolHandlerFunc { return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { environments, err := s.cli.GetEnvironments() if err != nil { return nil, fmt.Errorf("failed to get environments: %w", err) } data, err := json.Marshal(environments) if err != nil { return nil, fmt.Errorf("failed to marshal environments: %w", err) } return mcp.NewToolResultText(string(data)), nil } } ``` -------------------------------- ### Count Running Containers per Environment Source: https://github.com/portainer/portainer-mcp/blob/main/skills/portainer-mcp-hygiene/SKILL.md Use EndpointList to get the count of running and total containers for each environment. ```go EndpointList(select="[].{name:Name,type:Type,running:Snapshots[0].RunningContainerCount,total:Snapshots[0].ContainerCount}") ``` -------------------------------- ### Get Docker Dashboard Information Source: https://github.com/portainer/portainer-mcp/blob/main/skills/portainer-mcp-hygiene/SKILL.md Use `dockerDashboard` for aggregated information. This is a lightweight summary tool, preferable over `EndpointList` with projection for count or rollup questions. ```Go dockerDashboard() ``` -------------------------------- ### Handle Get Stack File in Go Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Fetches the content of a specific stack file based on its ID. It parses the 'id' parameter from the request. ```Go func (s *PortainerMCPServer) HandleGetStackFile() server.ToolHandlerFunc { return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { parser := toolgen.NewParameterParser(request) id, err := parser.GetInt("id", true) if err != nil { return nil, err } content, err := s.cli.GetStackFile(id) if err != nil { return nil, fmt.Errorf("failed to get stack file. Error: %w", err) } return mcp.NewToolResultText(content), nil } } ``` -------------------------------- ### Get All Users Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Retrieves a list of all users from the Portainer server. Converts the raw Portainer user data into the library's User model. ```go func (c *PortainerClient) GetUsers() ([]models.User, error) { portainerUsers, err := c.cli.ListUsers() if err != nil { return nil, fmt.Errorf("failed to list users: %w", err) } users := make([]models.User, len(portainerUsers)) for i, user := range portainerUsers { users[i] = models.ConvertToUser(user) } return users, nil } ``` -------------------------------- ### Get All Environment Groups Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Retrieves all environment groups, which are equivalent to Edge Groups in Portainer. Use this to manage or list group configurations. ```go func (c *PortainerClient) GetEnvironmentGroups() ([]models.Group, error) { edgeGroups, err := c.cli.ListEdgeGroups() if err != nil { return nil, fmt.Errorf("failed to list edge groups: %w", err) } groups := make([]models.Group, len(edgeGroups)) for i, eg := range edgeGroups { groups[i] = models.ConvertEdgeGroupToGroup(eg) } return groups, nil } ``` -------------------------------- ### Get Environment Groups Handler Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Handles requests to retrieve all environment groups. It fetches the groups using the CLI and returns them as a JSON string. ```go func (s *PortainerMCPServer) HandleGetEnvironmentGroups() server.ToolHandlerFunc { return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { edgeGroups, err := s.cli.GetEnvironmentGroups() if err != nil { return nil, fmt.Errorf("failed to get environment groups: %w", err) } data, err := json.Marshal(edgeGroups) if err != nil { return nil, fmt.Errorf("failed to marshal environment groups: %w", err) } return mcp.NewToolResultText(string(data)), nil } } ``` -------------------------------- ### Generate API Key using Direct HTTP Requests Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html This snippet shows an alternative implementation using Go's standard `http` package to generate an API key. It involves two steps: authenticating the admin user to get a JWT, and then using that JWT to request the API key. ```go // Direct HTTP implementation // alternative to the SDK implementation above httpClient := &http.Client{ Timeout: requestTimeout, Transport: &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, }, }, } baseURL := pc.GetAPIBaseURL() // Step 1: Authenticate admin user authPayload := map[string]string{ "username": "admin", "password": "adminpassword123", } authBody, err := json.Marshal(authPayload) if err != nil { return fmt.Errorf("failed to marshal auth payload: %w", err) } authReq, err := http.NewRequest(http.MethodPost, baseURL+"/api/auth", bytes.NewBuffer(authBody)) if err != nil { return fmt.Errorf("failed to create auth request: %w", err) } authReq.Header.Set("Content-Type", "application/json") authResp, err := httpClient.Do(authReq) if err != nil { return fmt.Errorf("failed to send auth request: %w", err) } defer authResp.Body.Close() if authResp.StatusCode != http.StatusOK { body, _ := io.ReadAll(authResp.Body) return fmt.Errorf("auth request failed with status %d: %s", authResp.StatusCode, string(body)) } var authResult struct { Jwt string `json:"jwt"` } if err := json.NewDecoder(authResp.Body).Decode(&authResult); err != nil { return fmt.Errorf("failed to decode auth response: %w", err) } // Step 2: Generate API key apiKeyPayload := map[string]string{ "description": "test-api-key", "password": "adminpassword123", } apiKeyBody, err := json.Marshal(apiKeyPayload) if err != nil { return fmt.Errorf("failed to marshal API key payload: %w", err) } apiKeyReq, err := http.NewRequest(http.MethodPost, baseURL+"/api/users/1/tokens", bytes.NewBuffer(apiKeyBody)) if err != nil { return fmt.Errorf("failed to create API key request: %w", err) } apiKeyReq.Header.Set("Content-Type", "application/json") apiKeyReq.Header.Set("Authorization", "Bearer "+authResult.Jwt) apiKeyResp, err := httpClient.Do(apiKeyReq) if err != nil { return fmt.Errorf("failed to send API key request: %w", err) } defer apiKeyResp.Body.Close() if apiKeyResp.StatusCode != http.StatusCreated && apiKeyResp.StatusCode != http.StatusOK { body, _ := io.ReadAll(apiKeyResp.Body) return fmt.Errorf("API key request failed with status %d: %s", apiKeyResp.StatusCode, string(body)) } var apiKeyResult struct { RawAPIKey string `json:"rawAPIKey"` } if err := json.NewDecoder(apiKeyResp.Body).Decode(&apiKeyResult); err != nil { return fmt.Errorf("failed to decode API key response: %w", err) } pc.apiToken = apiKeyResult.RawAPIKey return nil ``` -------------------------------- ### JMESPath Quoted Key Example Source: https://github.com/portainer/portainer-mcp/blob/main/skills/portainer-mcp-hygiene/SKILL.md Demonstrates how to select a field containing a dot (e.g., Kubernetes annotations) by quoting the key. This prevents JMESPath from misinterpreting the dot as a nested path. ```text Labels."com.docker.compose.project" ``` -------------------------------- ### Handle Get Users in Portainer MCP Server Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Implements the server-side logic to retrieve a list of users from the Portainer CLI. It handles potential errors during user retrieval and JSON marshaling. ```go func (s *PortainerMCPServer) HandleGetUsers() server.ToolHandlerFunc { return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { users, err := s.cli.GetUsers() if err != nil { return nil, fmt.Errorf("failed to get users: %w", err) } data, err := json.Marshal(users) if err != nil { return nil, fmt.Errorf("failed to marshal users: %w", err) } return mcp.NewToolResultText(string(data)), nil } } ``` -------------------------------- ### Initialize Portainer MCP Server Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Creates and configures a new Portainer MCP server instance. Use functional options like WithClient or WithReadOnly to customize behavior. Ensure the Portainer server version is compatible. ```go func NewPortainerMCPServer(serverURL, token, toolsPath string, options ...ServerOption) (*PortainerMCPServer, error) { opts := &serverOptions{} for _, option := range options { option(opts) } tools, err := toolgen.LoadToolsFromYAML(toolsPath, MinimumToolsVersion) if err != nil { return nil, fmt.Errorf("failed to load tools: %w", err) } var portainerClient PortainerClient if opts.client != nil { portainerClient = opts.client } else { portainerClient = client.NewPortainerClient(serverURL, token, client.WithSkipTLSVerify(true)) } version, err := portainerClient.GetVersion() if err != nil { return nil, fmt.Errorf("failed to get Portainer server version: %w", err) } if version != SupportedPortainerVersion { return nil, fmt.Errorf("unsupported Portainer server version: %s, only version %s is supported", version, SupportedPortainerVersion) } return &PortainerMCPServer{ srv: server.NewMCPServer( "Portainer MCP Server", "0.1.0", server.WithResourceCapabilities(true, true), server.WithLogging(), ), cli: portainerClient, tools: tools, readOnly: opts.readOnly, }, nil } ``` -------------------------------- ### Initialize ParameterParser Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Creates a new ParameterParser for a given tool request, initializing it with the request's arguments. ```Go // NewParameterParser creates a new parameter parser for the given request func NewParameterParser(request mcp.CallToolRequest) *ParameterParser { return &ParameterParser{ args: request.Params.Arguments, } } ``` -------------------------------- ### Get Environment Tags Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Retrieves a list of all environment tags. ```APIDOC ## GET /tags/environments ### Description Retrieves a list of all environment tags. ### Method GET ### Endpoint /tags/environments ### Response #### Success Response (200) - **environmentTags** (array) - A list of environment tag objects. ``` -------------------------------- ### Get Stacks Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Retrieves a list of all stacks managed by the system. ```APIDOC ## GET /stacks ### Description Retrieves a list of all stacks. ### Method GET ### Endpoint /stacks ### Response #### Success Response (200) - **stacks** (array) - A list of stack objects. ``` -------------------------------- ### Add Settings Tool Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Registers the 'GetSettings' tool with the MCP server. This handler retrieves Portainer settings and marshals them into JSON format. ```go func (s *PortainerMCPServer) AddSettingsFeatures() { s.addToolIfExists(ToolGetSettings, s.HandleGetSettings()) } ``` ```go func (s *PortainerMCPServer) HandleGetSettings() server.ToolHandlerFunc { return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { settings, err := s.cli.GetSettings() if err != nil { return nil, fmt.Errorf("failed to get settings: %w", err) } data, err := json.Marshal(settings) if err != nil { return nil, fmt.Errorf("failed to marshal settings: %w", err) } ``` -------------------------------- ### Deployment readiness via kubernetes_proxy Source: https://github.com/portainer/portainer-mcp/blob/main/skills/portainer-mcp-hygiene/SKILL.md Query deployment readiness, including name, namespace, desired replicas, and ready replicas. This helps in understanding the state of deployments. ```go kubernetes_proxy(environment_id=N, path="/apis/apps/v1/deployments", select="items[].{name:metadata.name,ns:metadata.namespace,replicas:spec.replicas,ready:status.readyReplicas}") ``` -------------------------------- ### Get Stack File Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Retrieves the content of a specific stack's file. ```APIDOC ## GET /stacks/{id}/file ### Description Retrieves the content of a specific stack file. ### Method GET ### Endpoint /stacks/{id}/file ### Parameters #### Path Parameters - **id** (integer) - Required - The ID of the stack. ### Response #### Success Response (200) - **content** (string) - The content of the stack file. ``` -------------------------------- ### Get Portainer API Token Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Returns the registered API token for the Portainer instance. ```go func (pc *PortainerContainer) GetAPIToken() string { return pc.apiToken } ``` -------------------------------- ### One-line Pod Summary in Environment N Source: https://github.com/portainer/portainer-mcp/blob/main/skills/portainer-mcp-hygiene/SKILL.md Retrieve a concise summary of pods in a Kubernetes environment, including name, namespace, phase, and node. ```go kubernetes_proxy(environment_id=N, path="/api/v1/pods", select="items[].{name:metadata.name,ns:metadata.namespace,phase:status.phase,node:spec.nodeName}") ``` -------------------------------- ### Handle Create Stack in Go Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Creates a new stack with the provided name, file content, and environment group IDs. It parses these parameters from the request. ```Go func (s *PortainerMCPServer) HandleCreateStack() server.ToolHandlerFunc { return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { parser := toolgen.NewParameterParser(request) name, err := parser.GetString("name", true) if err != nil { return nil, err } file, err := parser.GetString("file", true) if err != nil { return nil, err } environmentGroupIds, err := parser.GetArrayOfIntegers("environmentGroupIds", true) if err != nil { return nil, err } id, err := s.cli.CreateStack(name, file, environmentGroupIds) if err != nil { return nil, fmt.Errorf("error creating stack. Error: %w", err) } return mcp.NewToolResultText(fmt.Sprintf("Stack created successfully with ID: %d", id)), nil } } ``` -------------------------------- ### Get All Teams Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Retrieves all teams from the Portainer server, including their members by fetching team memberships. ```go func (c *PortainerClient) GetTeams() ([]models.Team, error) { portainerTeams, err := c.cli.ListTeams() if err != nil { return nil, fmt.Errorf("failed to list teams: %w", err) } memberships, err := c.cli.ListTeamMemberships() if err != nil { return nil, fmt.Errorf("failed to list team memberships: %w", err) } teams := make([]models.Team, len(portainerTeams)) for i, team := range portainerTeams { teams[i] = models.ConvertToTeam(team, memberships) } return teams, nil } ``` -------------------------------- ### Inspect Deployment X in Namespace Y Source: https://github.com/portainer/portainer-mcp/blob/main/skills/portainer-mcp-hygiene/SKILL.md Fetch detailed information about a specific deployment, including its name, desired replicas, ready replicas, and the primary container image. Optionally include reconciliation-related fields like managedFields and conditions. ```go kubernetes_proxy(environment_id=N, path="/apis/apps/v1/namespaces/Y/deployments/X", select="{name:metadata.name,replicas:spec.replicas,ready:status.readyReplicas,image:spec.template.spec.containers[0].image}") ``` -------------------------------- ### Get All Stacks Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Retrieves all stacks from the Portainer server. Stacks are the equivalent of Edge Stacks in Portainer. ```go func (c *PortainerClient) GetStacks() ([]models.Stack, error) { edgeStacks, err := c.cli.ListEdgeStacks() if err != nil { return nil, fmt.Errorf("failed to list edge stacks: %w", err) } stacks := make([]models.Stack, len(edgeStacks)) for i, es := range edgeStacks { stacks[i] = models.ConvertEdgeStackToStack(es) } return stacks, nil } ``` -------------------------------- ### ParameterParser GetInt Method Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Safely extracts an integer parameter by name by first getting it as a number (float64) and then converting it. ```Go // GetInt extracts an integer parameter from the request func (p *ParameterParser) GetInt(name string, required bool) (int, error) { num, err := p.GetNumber(name, required) if err != nil { return 0, err } return int(num), nil } ``` -------------------------------- ### Create Team Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Creates a new team on the Portainer server. ```go func (c *PortainerClient) CreateTeam(name string) (int, error) { id, err := c.cli.CreateTeam(name) if err != nil { return 0, fmt.Errorf("failed to create team: %w", err) } return int(id), nil } ``` -------------------------------- ### Get Portainer Version Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Retrieves the version string of the Portainer server. Handles potential errors during the API call. ```go func (c *PortainerClient) GetVersion() (string, error) { version, err := c.cli.GetVersion() if err != nil { return "", fmt.Errorf("failed to get version: %w", err) } return version, nil } ``` -------------------------------- ### Claude Desktop Manual JSON Configuration Source: https://github.com/portainer/portainer-mcp/blob/main/docs/distribution/claude-desktop.md This JSON configuration is used for manually setting up Claude Desktop to connect to Portainer. Ensure 'uv' is on your PATH and restart Claude Desktop after applying changes. ```json { "mcpServers": { "portainer": { "command": "uvx", "args": ["--from", "mcp-portainer~=2.43.0", "mcp-portainer"], "env": { "PORTAINER_URL": "https://portainer.example.com", "PORTAINER_API_KEY": "ptr_xxxxxxxxxxxxxxxx" } } } } ``` -------------------------------- ### GetSettings Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Retrieves the current Portainer settings. ```APIDOC ## GetSettings ### Description Retrieves the current Portainer settings. ### Method Signature func (c *PortainerAPIClient) GetSettings() (*apimodels.PortainereeSettings, error) ### Returns - The Portainer settings object and an error if the operation fails. ``` -------------------------------- ### Configure Server Options Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Defines functional options for configuring the Portainer MCP server. Use WithClient to inject custom clients (e.g., for testing) and WithReadOnly to enable read-only mode, preventing write tool registration. ```go // ServerOption is a function that configures the server type ServerOption func(*serverOptions) // serverOptions contains all configurable options for the server type serverOptions struct { client PortainerClient readOnly bool } // WithClient sets a custom client for the server. // This is primarily used for testing to inject mock clients. func WithClient(client PortainerClient) ServerOption { return func(opts *serverOptions) { opts.client = client } } // WithReadOnly sets the server to read-only mode. // This will prevent the server from registering write tools. func WithReadOnly(readOnly bool) ServerOption { return func(opts *serverOptions) { opts.readOnly = readOnly } } ``` -------------------------------- ### Portainer MCP Server Initialization Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Initializes the Portainer MCP server with version information and parses command-line flags for server URL, token, tools path, and read-only mode. It enforces the requirement for server and token flags. ```Go package main import ( "flag" "github.com/portainer-portainer-mcp/internal/mcp" "github.com/portainer-portainer-mcp/internal/tooldef" "github.com/rs/zerolog/log" ) const defaultToolsPath = "tools.yaml" var ( Version string BuildDate string Commit string ) func main() { log.Info(). Str("version", Version). Str("build-date", BuildDate). Str("commit", Commit). Msg("Portainer MCP server") serverFlag := flag.String("server", "", "The Portainer server URL") tokenFlag := flag.String("token", "", "The authentication token for the Portainer server") toolsFlag := flag.String("tools", "", "The path to the tools YAML file") readOnlyFlag := flag.Bool("read-only", false, "Run in read-only mode") flag.Parse() if *serverFlag == "" || *tokenFlag == "" { log.Fatal().Msg("Both -server and -token flags are required") } toolsPath := *toolsFlag if toolsPath == "" { toolsPath = defaultToolsPath } ``` -------------------------------- ### Create Tools File If Not Exists Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Checks if the tools.yaml file exists at the specified path. If it doesn't exist, it creates the file with default content. Returns true if the file already existed, false if it was created, or an error if any occurred during the process. ```go func CreateToolsFileIfNotExists(path string) (bool, error) { if _, err := os.Stat(path); os.IsNotExist(err) { err = os.WriteFile(path, ToolsFile, 0644) if err != nil { return false, err } return false, nil } return true, nil } ``` -------------------------------- ### Handle Get Stacks in Go Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Retrieves a list of all stacks from the Portainer MCP. It marshals the stack data into JSON format for the result. ```Go func (s *PortainerMCPServer) HandleGetStacks() server.ToolHandlerFunc { return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { stacks, err := s.cli.GetStacks() if err != nil { return nil, fmt.Errorf("failed to get stacks: %w", err) } data, err := json.Marshal(stacks) if err != nil { return nil, fmt.Errorf("failed to marshal stacks: %w", err) } return mcp.NewToolResultText(string(data)), nil } } ``` -------------------------------- ### NewPortainerClient Constructor Function (Go) Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Creates and returns a new PortainerClient instance. Accepts server URL, authentication token, and optional configuration options. ```go func NewPortainerClient(serverURL string, token string, opts ...ClientOption) *PortainerClient { options := clientOptions{ skipTLSVerify: false, // Default to secure TLS verification } for _, opt := range opts { opt(&options) } return &PortainerClient{ cli: client.NewPortainerClient(serverURL, token, client.WithSkipTLSVerify(options.skipTLSVerify)), } } ``` -------------------------------- ### Portainer MCP Server User Features Initialization in Go Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Initializes user-related features for the Portainer MCP server, adding a tool to handle listing users. ```go package mcp import ( "context" "encoding/json" "fmt" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" "github.com/portainer/portainer-mcp/pkg/toolgen" ) func (s *PortainerMCPServer) AddUserFeatures() { s.addToolIfExists(ToolListUsers, s.HandleGetUsers()) ``` -------------------------------- ### List Containers in Environment N Source: https://github.com/portainer/portainer-mcp/blob/main/skills/portainer-mcp-hygiene/SKILL.md Utilize docker_proxy to list containers within a specific environment, selecting key details like ID, name, state, image, and status. ```go docker_proxy(environment_id=N, path="/containers/json", select="[].{id:Id,name:Names[0],state:State,image:Image,status:Status}") ``` -------------------------------- ### Get Stack File Content Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Retrieves the file content of a specific stack from the Portainer server. Stacks are the equivalent of Edge Stacks in Portainer. ```go func (c *PortainerClient) GetStackFile(id int) (string, error) { file, err := c.cli.GetEdgeStackFile(int64(id)) if err != nil { return "", fmt.Errorf("failed to get edge stack file: %w", err) } return file, nil } ``` -------------------------------- ### CI Build and Test Command Source: https://github.com/portainer/portainer-mcp/blob/main/CLAUDE.md The CI process synchronizes dependencies in frozen mode and runs the full test suite. ```bash uv sync --frozen && uv run pytest ``` -------------------------------- ### Handle Create Environment Tag in Go Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Handles the creation of an environment tag. Parses the tag name from the request and returns the ID of the created tag. ```go func (s *PortainerMCPServer) HandleCreateEnvironmentTag() server.ToolHandlerFunc { return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { parser := toolgen.NewParameterParser(request) name, err := parser.GetString("name", true) if err != nil { return nil, err } id, err := s.cli.CreateEnvironmentTag(name) if err != nil { return nil, fmt.Errorf("error creating environment tag. Error: %w", err) } return mcp.NewToolResultText(fmt.Sprintf("Environment tag created successfully with ID: %d", id)), nil } } ``` -------------------------------- ### clientOptions Structure Definition (Go) Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Holds configuration options for the PortainerClient, such as TLS verification settings. ```go type clientOptions struct { skipTLSVerify bool } ``` -------------------------------- ### Load Tool Definitions from YAML Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Loads tool definitions from a YAML file and validates the version. It returns the tools and the version of the tools.yaml file. ```Go func LoadToolsFromYAML(filePath string, minimumVersion string) (map[string]mcp.Tool, error) { data, err := os.ReadFile(filePath) if err != nil { return nil, err } var config ToolsConfig if err := yaml.Unmarshal(data, &config); err != nil { return nil, err } if config.Version == "" { return nil, fmt.Errorf("missing version in tools.yaml") } if !semver.IsValid(config.Version) { return nil, fmt.Errorf("invalid version in tools.yaml: %s", config.Version) } if semver.Compare(config.Version, minimumVersion) < 0 { return nil, fmt.Errorf("tools.yaml version %s is below the minimum required version %s", config.Version, minimumVersion) } return convertToolDefinitions(config.Tools), nil } ``` -------------------------------- ### Get Portainer API Base URL Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Constructs the base URL for the Portainer API using the container's host and mapped API port. ```go // 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()) } ``` -------------------------------- ### List All Available User Roles Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Provides a slice containing all defined user role constants. Useful for checking against valid roles. ```go // All available user roles var AllUserRoles = []string{ UserRoleAdmin, UserRoleUser, UserRoleEdgeAdmin, } ``` -------------------------------- ### Create Environment Tag Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Creates a new environment tag on the Portainer server. Environment tags are the equivalent of Tags in Portainer. ```go func (c *PortainerClient) CreateEnvironmentTag(name string) (int, error) { id, err := c.cli.CreateTag(name) if err != nil { return 0, fmt.Errorf("failed to create environment tag: %w", err) } return int(id), nil } ``` -------------------------------- ### Validate HTTP Method Function Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Checks if a given HTTP method string is one of the predefined valid methods (GET, POST, PUT, DELETE, HEAD). ```go func isValidHTTPMethod(method string) bool { validMethods := []string{"GET", "POST", "PUT", "DELETE", "HEAD"} return slices.Contains(validMethods, method) } ``` -------------------------------- ### Connect Client to MCP with BYO Certificates (CLI) Source: https://github.com/portainer/portainer-mcp/blob/main/README.md Connects a client to the Portainer MCP server deployed with BYO certificates. Requires the gate token and the user's Portainer API key. ```bash claude mcp add portainer --transport http https://mcp.example.com:17717/mcp \ --header "Authorization: Bearer " \ --header "X-Portainer-API-Key: " ``` -------------------------------- ### List Images in Use per Environment Source: https://github.com/portainer/portainer-mcp/blob/main/skills/portainer-mcp-hygiene/SKILL.md Fetch all images used by containers in a specific environment using docker_proxy. Grouping by name is typically done client-side. ```go docker_proxy(environment_id=N, path="/containers/json", select="[].Image") ``` -------------------------------- ### Curated Tools, No Proxy Registration Source: https://github.com/portainer/portainer-mcp/blob/main/docs/profiles.md Enables BASE and DOCKER profiles for curated tools but skips the registration of `docker_proxy` and `kubernetes_proxy` using `PORTAINER_NO_PROXY=1`. ```bash # Curated tools only, no Docker/K8s proxy escape hatch PORTAINER_PROFILES=BASE,DOCKER PORTAINER_NO_PROXY=1 uv run portainer-mcp ``` -------------------------------- ### Add Environment Group Features Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Initializes and adds various tools related to environment group management to the server's tool list. Includes read-only and write operations based on server configuration. ```go func (s *PortainerMCPServer) AddEnvironmentGroupFeatures() { s.addToolIfExists(ToolListEnvironmentGroups, s.HandleGetEnvironmentGroups()) if !s.readOnly { s.addToolIfExists(ToolCreateEnvironmentGroup, s.HandleCreateEnvironmentGroup()) s.addToolIfExists(ToolUpdateEnvironmentGroupName, s.HandleUpdateEnvironmentGroupName()) s.addToolIfExists(ToolUpdateEnvironmentGroupEnvironments, s.HandleUpdateEnvironmentGroupEnvironments()) s.addToolIfExists(ToolUpdateEnvironmentGroupTags, s.HandleUpdateEnvironmentGroupTags()) } } ``` -------------------------------- ### Get Access Groups from Portainer Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Retrieves all access groups from the Portainer server, which are equivalent to Endpoint Groups. It fetches both groups and endpoints to map them correctly. Returns a slice of AccessGroup objects or an error. ```go func (c *PortainerClient) GetAccessGroups() ([]models.AccessGroup, error) { groups, err := c.cli.ListEndpointGroups() if err != nil { return nil, err } endpoints, err := c.cli.ListEndpoints() if err != nil { return nil, err } accessGroups := make([]models.AccessGroup, len(groups)) for i, group := range groups { accessGroups[i] = models.ConvertEndpointGroupToAccessGroup(group, endpoints) } return accessGroups, nil } ``` -------------------------------- ### Create Stack Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Creates a new stack with the provided details. ```APIDOC ## POST /stacks ### Description Creates a new stack. ### Method POST ### Endpoint /stacks ### Parameters #### Request Body - **name** (string) - Required - The name of the stack. - **file** (string) - Required - The content of the stack file. - **environmentGroupIds** (array of integers) - Required - The IDs of the environment groups to associate with the stack. ### Response #### Success Response (200) - **message** (string) - A confirmation message indicating the stack was created successfully, including its ID. ``` -------------------------------- ### Handle Get Access Groups API Call Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Implements the handler for the 'GetAccessGroups' tool. It fetches access groups from the client, marshals them into JSON, and returns the result. Errors during fetching or marshaling are propagated. ```go func (s *PortainerMCPServer) HandleGetAccessGroups() server.ToolHandlerFunc { return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { accessGroups, err := s.cli.GetAccessGroups() if err != nil { return nil, fmt.Errorf("failed to get access groups: %w", err) } data, err := json.Marshal(accessGroups) if err != nil { return nil, fmt.Errorf("failed to marshal access groups: %w", err) } return mcp.NewToolResultText(string(data)), nil } } ``` -------------------------------- ### Create Environment Tag Source: https://github.com/portainer/portainer-mcp/wiki/coverage.html Creates a new environment tag. ```APIDOC ## POST /tags/environments ### Description Creates a new environment tag. ### Method POST ### Endpoint /tags/environments ### Parameters #### Request Body - **tag** (object) - Required - The details of the environment tag to create. - **key** (string) - Required - The key of the tag. - **value** (string) - Required - The value of the tag. - **environmentId** (integer) - Required - The ID of the environment the tag belongs to. ### Response #### Success Response (200) - **message** (string) - A confirmation message indicating the tag was created successfully. ```