#
tokens: 48296/50000 26/115 files (page 2/4)
lines: off (toggle) GitHub
raw markdown copy
This is page 2 of 4. Use http://codebase.md/portainer/portainer-mcp?page={x} to view the full context.

# Directory Structure

```
├── .cursor
│   └── rules
│       └── integration-test.mdc
├── .github
│   └── workflows
│       ├── ci.yml
│       └── release.yml
├── .gitignore
├── CLAUDE.md
├── cloc.sh
├── cmd
│   ├── portainer-mcp
│   │   └── mcp.go
│   └── token-count
│       └── token.go
├── docs
│   ├── clients_and_models.md
│   ├── design
│   │   ├── 202503-1-external-tools-file.md
│   │   ├── 202503-2-tools-vs-mcp-resources.md
│   │   ├── 202503-3-specific-update-tools.md
│   │   ├── 202504-1-embedded-tools-yaml.md
│   │   ├── 202504-2-tools-yaml-versioning.md
│   │   ├── 202504-3-portainer-version-compatibility.md
│   │   └── 202504-4-read-only-mode.md
│   └── design_summary.md
├── go.mod
├── go.sum
├── internal
│   ├── k8sutil
│   │   ├── stripper_test.go
│   │   └── stripper.go
│   ├── mcp
│   │   ├── access_group_test.go
│   │   ├── access_group.go
│   │   ├── docker_test.go
│   │   ├── docker.go
│   │   ├── environment_test.go
│   │   ├── environment.go
│   │   ├── group_test.go
│   │   ├── group.go
│   │   ├── kubernetes_test.go
│   │   ├── kubernetes.go
│   │   ├── mocks_test.go
│   │   ├── schema_test.go
│   │   ├── schema.go
│   │   ├── server_test.go
│   │   ├── server.go
│   │   ├── settings_test.go
│   │   ├── settings.go
│   │   ├── stack_test.go
│   │   ├── stack.go
│   │   ├── tag_test.go
│   │   ├── tag.go
│   │   ├── team_test.go
│   │   ├── team.go
│   │   ├── testdata
│   │   │   ├── invalid_tools.yaml
│   │   │   └── valid_tools.yaml
│   │   ├── user_test.go
│   │   ├── user.go
│   │   ├── utils_test.go
│   │   └── utils.go
│   └── tooldef
│       ├── tooldef_test.go
│       ├── tooldef.go
│       └── tools.yaml
├── LICENSE
├── Makefile
├── pkg
│   ├── portainer
│   │   ├── client
│   │   │   ├── access_group_test.go
│   │   │   ├── access_group.go
│   │   │   ├── client_test.go
│   │   │   ├── client.go
│   │   │   ├── docker_test.go
│   │   │   ├── docker.go
│   │   │   ├── environment_test.go
│   │   │   ├── environment.go
│   │   │   ├── group_test.go
│   │   │   ├── group.go
│   │   │   ├── kubernetes_test.go
│   │   │   ├── kubernetes.go
│   │   │   ├── mocks_test.go
│   │   │   ├── settings_test.go
│   │   │   ├── settings.go
│   │   │   ├── stack_test.go
│   │   │   ├── stack.go
│   │   │   ├── tag_test.go
│   │   │   ├── tag.go
│   │   │   ├── team_test.go
│   │   │   ├── team.go
│   │   │   ├── user_test.go
│   │   │   ├── user.go
│   │   │   ├── version_test.go
│   │   │   └── version.go
│   │   ├── models
│   │   │   ├── access_group_test.go
│   │   │   ├── access_group.go
│   │   │   ├── access_policy_test.go
│   │   │   ├── access_policy.go
│   │   │   ├── docker.go
│   │   │   ├── environment_test.go
│   │   │   ├── environment.go
│   │   │   ├── group_test.go
│   │   │   ├── group.go
│   │   │   ├── kubernetes.go
│   │   │   ├── settings_test.go
│   │   │   ├── settings.go
│   │   │   ├── stack_test.go
│   │   │   ├── stack.go
│   │   │   ├── tag_test.go
│   │   │   ├── tag.go
│   │   │   ├── team_test.go
│   │   │   ├── team.go
│   │   │   ├── user_test.go
│   │   │   └── user.go
│   │   └── utils
│   │       ├── utils_test.go
│   │       └── utils.go
│   └── toolgen
│       ├── param_test.go
│       ├── param.go
│       ├── yaml_test.go
│       └── yaml.go
├── README.md
├── tests
│   └── integration
│       ├── access_group_test.go
│       ├── containers
│       │   └── portainer.go
│       ├── docker_test.go
│       ├── environment_test.go
│       ├── group_test.go
│       ├── helpers
│       │   └── test_env.go
│       ├── server_test.go
│       ├── settings_test.go
│       ├── stack_test.go
│       ├── tag_test.go
│       ├── team_test.go
│       └── user_test.go
└── token.sh
```

# Files

--------------------------------------------------------------------------------
/internal/mcp/utils_test.go:
--------------------------------------------------------------------------------

```go
package mcp

import (
	"reflect"
	"testing"
)

func TestParseAccessMap(t *testing.T) {
	tests := []struct {
		name    string
		entries []any
		want    map[int]string
		wantErr bool
	}{
		{
			name: "Valid single entry",
			entries: []any{
				map[string]any{
					"id":     float64(1),
					"access": AccessLevelEnvironmentAdmin,
				},
			},
			want: map[int]string{
				1: AccessLevelEnvironmentAdmin,
			},
			wantErr: false,
		},
		{
			name: "Valid multiple entries",
			entries: []any{
				map[string]any{
					"id":     float64(1),
					"access": AccessLevelEnvironmentAdmin,
				},
				map[string]any{
					"id":     float64(2),
					"access": AccessLevelReadonlyUser,
				},
			},
			want: map[int]string{
				1: AccessLevelEnvironmentAdmin,
				2: AccessLevelReadonlyUser,
			},
			wantErr: false,
		},
		{
			name: "Invalid entry type",
			entries: []any{
				"not a map",
			},
			want:    nil,
			wantErr: true,
		},
		{
			name: "Invalid ID type",
			entries: []any{
				map[string]any{
					"id":     "string-id",
					"access": AccessLevelEnvironmentAdmin,
				},
			},
			want:    nil,
			wantErr: true,
		},
		{
			name: "Invalid access type",
			entries: []any{
				map[string]any{
					"id":     float64(1),
					"access": 123,
				},
			},
			want:    nil,
			wantErr: true,
		},
		{
			name: "Invalid access level",
			entries: []any{
				map[string]any{
					"id":     float64(1),
					"access": "invalid_access_level",
				},
			},
			want:    nil,
			wantErr: true,
		},
		{
			name:    "Empty entries",
			entries: []any{},
			want:    map[int]string{},
			wantErr: false,
		},
		{
			name: "Missing ID field",
			entries: []any{
				map[string]any{
					"access": AccessLevelEnvironmentAdmin,
				},
			},
			want:    nil,
			wantErr: true,
		},
		{
			name: "Missing access field",
			entries: []any{
				map[string]any{
					"id": float64(1),
				},
			},
			want:    nil,
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := parseAccessMap(tt.entries)
			if (err != nil) != tt.wantErr {
				t.Errorf("parseAccessMap() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("parseAccessMap() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestIsValidHTTPMethod(t *testing.T) {
	tests := []struct {
		name   string
		method string
		expect bool
	}{
		{"Valid GET", "GET", true},
		{"Valid POST", "POST", true},
		{"Valid PUT", "PUT", true},
		{"Valid DELETE", "DELETE", true},
		{"Valid HEAD", "HEAD", true},
		{"Invalid lowercase get", "get", false},
		{"Invalid PATCH", "PATCH", false},
		{"Invalid OPTIONS", "OPTIONS", false},
		{"Invalid Empty", "", false},
		{"Invalid Random", "RANDOM", false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := isValidHTTPMethod(tt.method)
			if got != tt.expect {
				t.Errorf("isValidHTTPMethod(%q) = %v, want %v", tt.method, got, tt.expect)
			}
		})
	}
}

func TestParseKeyValueMap(t *testing.T) {
	tests := []struct {
		name    string
		items   []any
		want    map[string]string
		wantErr bool
	}{
		{
			name: "Valid single entry",
			items: []any{
				map[string]any{"key": "k1", "value": "v1"},
			},
			want: map[string]string{
				"k1": "v1",
			},
			wantErr: false,
		},
		{
			name: "Valid multiple entries",
			items: []any{
				map[string]any{"key": "k1", "value": "v1"},
				map[string]any{"key": "k2", "value": "v2"},
			},
			want: map[string]string{
				"k1": "v1",
				"k2": "v2",
			},
			wantErr: false,
		},
		{
			name:    "Empty items",
			items:   []any{},
			want:    map[string]string{},
			wantErr: false,
		},
		{
			name: "Invalid item type",
			items: []any{
				"not a map",
			},
			want:    nil,
			wantErr: true,
		},
		{
			name: "Invalid key type",
			items: []any{
				map[string]any{"key": 123, "value": "v1"},
			},
			want:    nil,
			wantErr: true,
		},
		{
			name: "Invalid value type",
			items: []any{
				map[string]any{"key": "k1", "value": 123},
			},
			want:    nil,
			wantErr: true,
		},
		{
			name: "Missing key field",
			items: []any{
				map[string]any{"value": "v1"},
			},
			want:    nil,
			wantErr: true,
		},
		{
			name: "Missing value field",
			items: []any{
				map[string]any{"key": "k1"},
			},
			want:    nil,
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := parseKeyValueMap(tt.items)
			if (err != nil) != tt.wantErr {
				t.Errorf("parseKeyValueMap() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("parseKeyValueMap() = %v, want %v", got, tt.want)
			}
		})
	}
}

```

--------------------------------------------------------------------------------
/pkg/toolgen/yaml.go:
--------------------------------------------------------------------------------

```go
package toolgen

import (
	"fmt"
	"log"
	"os"

	"github.com/mark3labs/mcp-go/mcp"
	"golang.org/x/mod/semver"
	"gopkg.in/yaml.v3"
)

// ToolsConfig represents the entire YAML configuration
type ToolsConfig struct {
	Version string           `yaml:"version"`
	Tools   []ToolDefinition `yaml:"tools"`
}

// ToolDefinition represents a single tool in the YAML config
type ToolDefinition struct {
	Name        string                `yaml:"name"`
	Description string                `yaml:"description"`
	Parameters  []ParameterDefinition `yaml:"parameters"`
	Annotations Annotations           `yaml:"annotations"`
}

// ParameterDefinition represents a tool parameter in the YAML config
type ParameterDefinition struct {
	Name        string         `yaml:"name"`
	Type        string         `yaml:"type"`
	Required    bool           `yaml:"required"`
	Enum        []string       `yaml:"enum,omitempty"`
	Description string         `yaml:"description"`
	Items       map[string]any `yaml:"items,omitempty"`
}

// Annotations represents a tool annotations in the YAML config
type Annotations struct {
	Title           string `yaml:"title"`
	ReadOnlyHint    bool   `yaml:"readOnlyHint"`
	DestructiveHint bool   `yaml:"destructiveHint"`
	IdempotentHint  bool   `yaml:"idempotentHint"`
	OpenWorldHint   bool   `yaml:"openWorldHint"`
}

// LoadToolsFromYAML loads tool definitions from a YAML file
// It returns the tools and the version of the tools.yaml file
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
}

// convertToolDefinitions converts YAML tool definitions to mcp.Tool objects
func convertToolDefinitions(defs []ToolDefinition) map[string]mcp.Tool {
	tools := make(map[string]mcp.Tool, len(defs))

	for _, def := range defs {
		tool, err := convertToolDefinition(def)
		if err != nil {
			log.Printf("skipping invalid tool definition %s: %s", def.Name, err)
			continue
		}

		tools[def.Name] = tool
	}

	return tools
}

// convertToolDefinition converts a single YAML tool definition to an mcp.Tool
func convertToolDefinition(def ToolDefinition) (mcp.Tool, error) {
	if def.Name == "" {
		return mcp.Tool{}, fmt.Errorf("tool name is required")
	}

	if def.Description == "" {
		return mcp.Tool{}, fmt.Errorf("tool description is required for tool '%s'", def.Name)
	}

	var zeroAnnotations Annotations
	if def.Annotations == zeroAnnotations {
		return mcp.Tool{}, fmt.Errorf("annotations block is required for tool '%s'", def.Name)
	}

	options := []mcp.ToolOption{
		mcp.WithDescription(def.Description),
	}

	for _, param := range def.Parameters {
		options = append(options, convertParameter(param))
	}

	options = append(options, convertAnnotation(def.Annotations))

	return mcp.NewTool(def.Name, options...), nil
}

// convertAnnotation converts a YAML annotation definition to an mcp option
func convertAnnotation(annotation Annotations) mcp.ToolOption {
	return mcp.WithToolAnnotation(mcp.ToolAnnotation{
		Title:           annotation.Title,
		ReadOnlyHint:    &annotation.ReadOnlyHint,
		DestructiveHint: &annotation.DestructiveHint,
		IdempotentHint:  &annotation.IdempotentHint,
		OpenWorldHint:   &annotation.OpenWorldHint,
	})
}

// convertParameter converts a YAML parameter definition to an mcp option
func convertParameter(param ParameterDefinition) mcp.ToolOption {
	var options []mcp.PropertyOption

	options = append(options, mcp.Description(param.Description))

	if param.Required {
		options = append(options, mcp.Required())
	}

	if param.Enum != nil {
		options = append(options, mcp.Enum(param.Enum...))
	}

	if len(param.Items) > 0 {
		options = append(options, mcp.Items(param.Items))
	}

	switch param.Type {
	case "string":
		return mcp.WithString(param.Name, options...)
	case "number":
		return mcp.WithNumber(param.Name, options...)
	case "boolean":
		return mcp.WithBoolean(param.Name, options...)
	case "array":
		return mcp.WithArray(param.Name, options...)
	case "object":
		return mcp.WithObject(param.Name, options...)
	default:
		// Default to string if type is unknown
		return mcp.WithString(param.Name, options...)
	}
}

```

--------------------------------------------------------------------------------
/tests/integration/server_test.go:
--------------------------------------------------------------------------------

```go
package integration

import (
	"context"
	"fmt"
	"testing"

	mcpmodels "github.com/mark3labs/mcp-go/mcp"
	"github.com/portainer/portainer-mcp/internal/mcp"
	"github.com/portainer/portainer-mcp/tests/integration/containers"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

const (
	toolsPath        = "../../internal/tooldef/tools.yaml"
	unsupportedImage = "portainer/portainer-ee:2.29.1" // Older version than SupportedPortainerVersion
)

// TestServerInitialization verifies that the Portainer MCP server
// can be successfully initialized with a real Portainer instance.
func TestServerInitialization(t *testing.T) {
	// Start a Portainer container
	ctx := context.Background()

	portainer, err := containers.NewPortainerContainer(ctx)
	require.NoError(t, err, "Failed to start Portainer container")

	// Ensure container is terminated at the end of the test
	defer func() {
		if err := portainer.Terminate(ctx); err != nil {
			t.Logf("Failed to terminate container: %v", err)
		}
	}()

	// Get the host and port for the Portainer API
	host, port := portainer.GetHostAndPort()
	serverURL := fmt.Sprintf("%s:%s", host, port)
	apiToken := portainer.GetAPIToken()

	// Create the MCP server - this is the main test objective
	mcpServer, err := mcp.NewPortainerMCPServer(serverURL, apiToken, toolsPath)

	// Assert the server was created successfully
	require.NoError(t, err, "Failed to create MCP server")
	require.NotNil(t, mcpServer, "MCP server should not be nil")
}

// TestServerInitializationUnsupportedVersion verifies that the Portainer MCP server
// correctly rejects initialization with an unsupported Portainer version.
func TestServerInitializationUnsupportedVersion(t *testing.T) {
	// Start a Portainer container with unsupported version
	ctx := context.Background()

	portainer, err := containers.NewPortainerContainer(ctx, containers.WithImage(unsupportedImage))
	require.NoError(t, err, "Failed to start unsupported Portainer container")

	// Ensure container is terminated at the end of the test
	defer func() {
		if err := portainer.Terminate(ctx); err != nil {
			t.Logf("Failed to terminate container: %v", err)
		}
	}()

	// Get the host and port for the Portainer API
	host, port := portainer.GetHostAndPort()
	serverURL := fmt.Sprintf("%s:%s", host, port)
	apiToken := portainer.GetAPIToken()

	// Try to create the MCP server - should fail with version error
	mcpServer, err := mcp.NewPortainerMCPServer(serverURL, apiToken, toolsPath)

	// Assert the server creation failed with correct error
	assert.Error(t, err, "Server creation should fail with unsupported version")
	assert.Contains(t, err.Error(), "unsupported Portainer server version", "Error should indicate version mismatch")
	assert.Nil(t, mcpServer, "Server should be nil when version check fails")
}

// TestServerInitializationDisabledVersionCheck verifies that the Portainer MCP server
// can successfully connect to unsupported Portainer versions when version check is disabled.
func TestServerInitializationDisabledVersionCheck(t *testing.T) {
	// Start a Portainer container with unsupported version
	ctx := context.Background()

	portainer, err := containers.NewPortainerContainer(ctx, containers.WithImage(unsupportedImage))
	require.NoError(t, err, "Failed to start unsupported Portainer container")

	// Ensure container is terminated at the end of the test
	defer func() {
		if err := portainer.Terminate(ctx); err != nil {
			t.Logf("Failed to terminate container: %v", err)
		}
	}()

	// Get the host and port for the Portainer API
	host, port := portainer.GetHostAndPort()
	serverURL := fmt.Sprintf("%s:%s", host, port)
	apiToken := portainer.GetAPIToken()

	// Create the MCP server with disabled version check - should succeed despite unsupported version
	mcpServer, err := mcp.NewPortainerMCPServer(serverURL, apiToken, toolsPath, mcp.WithDisableVersionCheck(true))

	// Assert the server was created successfully
	require.NoError(t, err, "Failed to create MCP server with disabled version check")
	require.NotNil(t, mcpServer, "MCP server should not be nil when version check is disabled")

	// Verify basic functionality by testing settings retrieval
	handler := mcpServer.HandleGetSettings()
	request := mcp.CreateMCPRequest(nil) // GetSettings doesn't require parameters

	result, err := handler(ctx, request)
	require.NoError(t, err, "Failed to get settings via MCP handler with disabled version check")
	require.NotNil(t, result, "Settings result should not be nil")
	require.Len(t, result.Content, 1, "Expected exactly one content block in settings result")

	// Verify the response contains valid content
	textContent, ok := result.Content[0].(mcpmodels.TextContent)
	require.True(t, ok, "Expected text content in settings MCP response")
	assert.NotEmpty(t, textContent.Text, "Settings response should not be empty")
}

```

--------------------------------------------------------------------------------
/internal/mcp/kubernetes.go:
--------------------------------------------------------------------------------

```go
package mcp

import (
	"context"
	"fmt"
	"io"
	"strings"

	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
	"github.com/portainer/portainer-mcp/internal/k8sutil"
	"github.com/portainer/portainer-mcp/pkg/portainer/models"
	"github.com/portainer/portainer-mcp/pkg/toolgen"
)

func (s *PortainerMCPServer) AddKubernetesProxyFeatures() {
	s.addToolIfExists(ToolKubernetesProxyStripped, s.HandleKubernetesProxyStripped())

	if !s.readOnly {
		s.addToolIfExists(ToolKubernetesProxy, s.HandleKubernetesProxy())
	}
}

func (s *PortainerMCPServer) HandleKubernetesProxyStripped() server.ToolHandlerFunc {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		parser := toolgen.NewParameterParser(request)

		environmentId, err := parser.GetInt("environmentId", true)
		if err != nil {
			return mcp.NewToolResultErrorFromErr("invalid environmentId parameter", err), nil
		}

		kubernetesAPIPath, err := parser.GetString("kubernetesAPIPath", true)
		if err != nil {
			return mcp.NewToolResultErrorFromErr("invalid kubernetesAPIPath parameter", err), nil
		}
		if !strings.HasPrefix(kubernetesAPIPath, "/") {
			return mcp.NewToolResultError("kubernetesAPIPath must start with a leading slash"), nil
		}

		queryParams, err := parser.GetArrayOfObjects("queryParams", false)
		if err != nil {
			return mcp.NewToolResultErrorFromErr("invalid queryParams parameter", err), nil
		}
		queryParamsMap, err := parseKeyValueMap(queryParams)
		if err != nil {
			return mcp.NewToolResultErrorFromErr("invalid query params", err), nil
		}

		headers, err := parser.GetArrayOfObjects("headers", false)
		if err != nil {
			return mcp.NewToolResultErrorFromErr("invalid headers parameter", err), nil
		}
		headersMap, err := parseKeyValueMap(headers)
		if err != nil {
			return mcp.NewToolResultErrorFromErr("invalid headers", err), nil
		}

		opts := models.KubernetesProxyRequestOptions{
			EnvironmentID: environmentId,
			Path:          kubernetesAPIPath,
			Method:        "GET",
			QueryParams:   queryParamsMap,
			Headers:       headersMap,
		}

		response, err := s.cli.ProxyKubernetesRequest(opts)
		if err != nil {
			return mcp.NewToolResultErrorFromErr("failed to send Kubernetes API request", err), nil
		}

		responseBody, err := k8sutil.ProcessRawKubernetesAPIResponse(response)
		if err != nil {
			return mcp.NewToolResultErrorFromErr("failed to process Kubernetes API response", err), nil
		}

		return mcp.NewToolResultText(string(responseBody)), nil
	}
}

func (s *PortainerMCPServer) HandleKubernetesProxy() server.ToolHandlerFunc {
	return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		parser := toolgen.NewParameterParser(request)

		environmentId, err := parser.GetInt("environmentId", true)
		if err != nil {
			return mcp.NewToolResultErrorFromErr("invalid environmentId parameter", err), nil
		}

		method, err := parser.GetString("method", true)
		if err != nil {
			return mcp.NewToolResultErrorFromErr("invalid method parameter", err), nil
		}
		if !isValidHTTPMethod(method) {
			return mcp.NewToolResultError(fmt.Sprintf("invalid method: %s", method)), nil
		}

		kubernetesAPIPath, err := parser.GetString("kubernetesAPIPath", true)
		if err != nil {
			return mcp.NewToolResultErrorFromErr("invalid kubernetesAPIPath parameter", err), nil
		}
		if !strings.HasPrefix(kubernetesAPIPath, "/") {
			return mcp.NewToolResultError("kubernetesAPIPath must start with a leading slash"), nil
		}

		queryParams, err := parser.GetArrayOfObjects("queryParams", false)
		if err != nil {
			return mcp.NewToolResultErrorFromErr("invalid queryParams parameter", err), nil
		}
		queryParamsMap, err := parseKeyValueMap(queryParams)
		if err != nil {
			return mcp.NewToolResultErrorFromErr("invalid query params", err), nil
		}

		headers, err := parser.GetArrayOfObjects("headers", false)
		if err != nil {
			return mcp.NewToolResultErrorFromErr("invalid headers parameter", err), nil
		}
		headersMap, err := parseKeyValueMap(headers)
		if err != nil {
			return mcp.NewToolResultErrorFromErr("invalid headers", err), nil
		}

		body, err := parser.GetString("body", false)
		if err != nil {
			return mcp.NewToolResultErrorFromErr("invalid body parameter", err), nil
		}

		opts := models.KubernetesProxyRequestOptions{
			EnvironmentID: environmentId,
			Path:          kubernetesAPIPath,
			Method:        method,
			QueryParams:   queryParamsMap,
			Headers:       headersMap,
		}

		if body != "" {
			opts.Body = strings.NewReader(body)
		}

		response, err := s.cli.ProxyKubernetesRequest(opts)
		if err != nil {
			return mcp.NewToolResultErrorFromErr("failed to send Kubernetes API request", err), nil
		}

		responseBody, err := io.ReadAll(response.Body)
		if err != nil {
			return mcp.NewToolResultErrorFromErr("failed to read Kubernetes API response", err), nil
		}

		return mcp.NewToolResultText(string(responseBody)), nil
	}
}

```

--------------------------------------------------------------------------------
/internal/mcp/server_test.go:
--------------------------------------------------------------------------------

```go
package mcp

import (
	"context"
	"errors"
	"testing"

	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestNewPortainerMCPServer(t *testing.T) {
	// Define paths to test data files
	validToolsPath := "testdata/valid_tools.yaml"
	invalidToolsPath := "testdata/invalid_tools.yaml"

	tests := []struct {
		name          string
		serverURL     string
		token         string
		toolsPath     string
		mockSetup     func(*MockPortainerClient)
		expectError   bool
		errorContains string
	}{
		{
			name:      "successful initialization with supported version",
			serverURL: "https://portainer.example.com",
			token:     "valid-token",
			toolsPath: validToolsPath,
			mockSetup: func(m *MockPortainerClient) {
				m.On("GetVersion").Return(SupportedPortainerVersion, nil)
			},
			expectError: false,
		},
		{
			name:          "invalid tools path",
			serverURL:     "https://portainer.example.com",
			token:         "valid-token",
			toolsPath:     "testdata/nonexistent.yaml",
			mockSetup:     func(m *MockPortainerClient) {},
			expectError:   true,
			errorContains: "failed to load tools",
		},
		{
			name:          "invalid tools version",
			serverURL:     "https://portainer.example.com",
			token:         "valid-token",
			toolsPath:     invalidToolsPath,
			mockSetup:     func(m *MockPortainerClient) {},
			expectError:   true,
			errorContains: "invalid version in tools.yaml",
		},
		{
			name:      "API communication error",
			serverURL: "https://portainer.example.com",
			token:     "valid-token",
			toolsPath: validToolsPath,
			mockSetup: func(m *MockPortainerClient) {
				m.On("GetVersion").Return("", errors.New("connection error"))
			},
			expectError:   true,
			errorContains: "failed to get Portainer server version",
		},
		{
			name:      "unsupported Portainer version",
			serverURL: "https://portainer.example.com",
			token:     "valid-token",
			toolsPath: validToolsPath,
			mockSetup: func(m *MockPortainerClient) {
				m.On("GetVersion").Return("2.0.0", nil)
			},
			expectError:   true,
			errorContains: "unsupported Portainer server version",
		},
		{
			name:      "unsupported version with disabled version check",
			serverURL: "https://portainer.example.com",
			token:     "valid-token",
			toolsPath: validToolsPath,
			mockSetup: func(m *MockPortainerClient) {
				// No GetVersion call expected when version check is disabled
			},
			expectError: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Create and configure the mock client
			mockClient := new(MockPortainerClient)
			tt.mockSetup(mockClient)

			// Create server with mock client using the WithClient option
			var options []ServerOption
			options = append(options, WithClient(mockClient))

			// Add WithDisableVersionCheck for the specific test case
			if tt.name == "unsupported version with disabled version check" {
				options = append(options, WithDisableVersionCheck(true))
			}

			server, err := NewPortainerMCPServer(
				tt.serverURL,
				tt.token,
				tt.toolsPath,
				options...,
			)

			if tt.expectError {
				assert.Error(t, err)
				if tt.errorContains != "" {
					assert.Contains(t, err.Error(), tt.errorContains)
				}
				assert.Nil(t, server)
			} else {
				require.NoError(t, err)
				assert.NotNil(t, server)
				assert.NotNil(t, server.srv)
				assert.NotNil(t, server.cli)
				assert.NotNil(t, server.tools)
			}

			// Verify that all expected methods were called
			mockClient.AssertExpectations(t)
		})
	}
}

func TestAddToolIfExists(t *testing.T) {
	tests := []struct {
		name     string
		tools    map[string]mcp.Tool
		toolName string
		exists   bool
	}{
		{
			name: "existing tool",
			tools: map[string]mcp.Tool{
				"test_tool": {
					Name:        "test_tool",
					Description: "Test tool description",
					InputSchema: mcp.ToolInputSchema{
						Properties: map[string]any{},
					},
				},
			},
			toolName: "test_tool",
			exists:   true,
		},
		{
			name: "non-existing tool",
			tools: map[string]mcp.Tool{
				"test_tool": {
					Name:        "test_tool",
					Description: "Test tool description",
					InputSchema: mcp.ToolInputSchema{
						Properties: map[string]any{},
					},
				},
			},
			toolName: "nonexistent_tool",
			exists:   false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Create server with test tools
			mcpServer := server.NewMCPServer(
				"Test Server",
				"1.0.0",
				server.WithResourceCapabilities(true, true),
				server.WithLogging(),
			)
			server := &PortainerMCPServer{
				tools: tt.tools,
				srv:   mcpServer,
			}

			// Create a handler function
			handler := func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
				return &mcp.CallToolResult{}, nil
			}

			// Call addToolIfExists
			server.addToolIfExists(tt.toolName, handler)

			// Verify if the tool exists in the tools map
			_, toolExists := server.tools[tt.toolName]
			assert.Equal(t, tt.exists, toolExists)
		})
	}
}

```

--------------------------------------------------------------------------------
/internal/mcp/user_test.go:
--------------------------------------------------------------------------------

```go
package mcp

import (
	"context"
	"encoding/json"
	"fmt"
	"testing"

	"github.com/mark3labs/mcp-go/mcp"
	"github.com/portainer/portainer-mcp/pkg/portainer/models"
	"github.com/stretchr/testify/assert"
)

func TestHandleGetUsers(t *testing.T) {
	tests := []struct {
		name        string
		mockUsers   []models.User
		mockError   error
		expectError bool
	}{
		{
			name: "successful users retrieval",
			mockUsers: []models.User{
				{ID: 1, Username: "user1", Role: "admin"},
				{ID: 2, Username: "user2", Role: "user"},
			},
			mockError:   nil,
			expectError: false,
		},
		{
			name:        "api error",
			mockUsers:   nil,
			mockError:   fmt.Errorf("api error"),
			expectError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Create mock client
			mockClient := &MockPortainerClient{}
			mockClient.On("GetUsers").Return(tt.mockUsers, tt.mockError)

			// Create server with mock client
			server := &PortainerMCPServer{
				cli: mockClient,
			}

			// Call handler
			handler := server.HandleGetUsers()
			result, err := handler(context.Background(), mcp.CallToolRequest{})

			// Verify results
			if tt.expectError {
				assert.NoError(t, err)
				assert.NotNil(t, result)
				assert.True(t, result.IsError, "result.IsError should be true for API errors")
				assert.Len(t, result.Content, 1)
				textContent, ok := result.Content[0].(mcp.TextContent)
				assert.True(t, ok, "Result content should be mcp.TextContent")
				if tt.mockError != nil {
					assert.Contains(t, textContent.Text, tt.mockError.Error())
				}
			} else {
				assert.NoError(t, err)
				assert.Len(t, result.Content, 1)
				textContent, ok := result.Content[0].(mcp.TextContent)
				assert.True(t, ok)

				var users []models.User
				err = json.Unmarshal([]byte(textContent.Text), &users)
				assert.NoError(t, err)
				assert.Equal(t, tt.mockUsers, users)
			}

			// Verify mock expectations
			mockClient.AssertExpectations(t)
		})
	}
}

func TestHandleUpdateUserRole(t *testing.T) {
	tests := []struct {
		name        string
		inputID     int
		inputRole   string
		mockError   error
		expectError bool
		setupParams func(request *mcp.CallToolRequest)
	}{
		{
			name:        "successful role update",
			inputID:     1,
			inputRole:   "admin",
			mockError:   nil,
			expectError: false,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id":   float64(1),
					"role": "admin",
				}
			},
		},
		{
			name:        "api error",
			inputID:     1,
			inputRole:   "admin",
			mockError:   fmt.Errorf("api error"),
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id":   float64(1),
					"role": "admin",
				}
			},
		},
		{
			name:        "missing id parameter",
			inputID:     0,
			inputRole:   "admin",
			mockError:   nil,
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"role": "admin",
				}
			},
		},
		{
			name:        "missing role parameter",
			inputID:     1,
			inputRole:   "",
			mockError:   nil,
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id": float64(1),
				}
			},
		},
		{
			name:        "invalid role",
			inputID:     1,
			inputRole:   "invalid_role",
			mockError:   nil,
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id":   float64(1),
					"role": "invalid_role",
				}
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Create mock client
			mockClient := &MockPortainerClient{}
			if !tt.expectError || tt.mockError != nil {
				mockClient.On("UpdateUserRole", tt.inputID, tt.inputRole).Return(tt.mockError)
			}

			// Create server with mock client
			server := &PortainerMCPServer{
				cli: mockClient,
			}

			// Create request with parameters
			request := CreateMCPRequest(map[string]any{})
			tt.setupParams(&request)

			// Call handler
			handler := server.HandleUpdateUserRole()
			result, err := handler(context.Background(), request)

			// Verify results
			if tt.expectError {
				assert.NoError(t, err)
				assert.NotNil(t, result)
				assert.True(t, result.IsError, "result.IsError should be true for expected errors")
				assert.Len(t, result.Content, 1)
				textContent, ok := result.Content[0].(mcp.TextContent)
				assert.True(t, ok, "Result content should be mcp.TextContent for errors")
				if tt.mockError != nil {
					assert.Contains(t, textContent.Text, tt.mockError.Error())
				} else {
					assert.NotEmpty(t, textContent.Text, "Error message should not be empty for parameter/validation errors")
					if tt.inputRole == "invalid_role" {
						assert.Contains(t, textContent.Text, "invalid role")
					}
				}
			} else {
				assert.NoError(t, err)
				assert.Len(t, result.Content, 1)
				textContent, ok := result.Content[0].(mcp.TextContent)
				assert.True(t, ok)
				assert.Contains(t, textContent.Text, "successfully")
			}

			// Verify mock expectations
			mockClient.AssertExpectations(t)
		})
	}
}

```

--------------------------------------------------------------------------------
/cloc.sh:
--------------------------------------------------------------------------------

```bash
#!/bin/bash

# This scripts counts the lines of code (LOC) and comments in Go source files
# within this project directory. It uses the commandline tool "cloc".
# Requires `cloc` to be installed (e.g., `sudo apt install cloc` or `brew install cloc`).
# Modified from: https://schneegans.github.io/tutorials/2022/04/18/badges
#
# Usage:
#   Run from the repository root:
#     ./cloc.sh
#
# Default Output:
#   Displays a summary of code statistics:
#     Total lines of code:                   <value>k
#     Lines of source code:                  <value>k
#     Lines of comments (source code):       <value>k
#     Lines of test code:                    <value>k
#     Comment Percentage:                    <value>%
#     Test Percentage:                       <value>%
#
# Flags for Specific Metrics:
#   You can request individual metrics using the following flags:
#     --loc             : Lines of source code (Go files, excluding tests).
#     --comments        : Lines of comments in source code.
#     --percentage      : Comment percentage in source code.
#     --test-loc        : Lines of test code (_test.go files + tests/integration/ dir).
#     --test-percentage : Percentage of test code compared to total code.
#     --total-loc       : Total lines of code (source + test).
#
# Example:
#   ./cloc.sh --test-percentage
#   # Output: 19.0 (example value)

# Get the location of this script.
SCRIPT_DIR="$( cd "$( dirname "$0" )" && pwd )"

# Run cloc for source code - this counts code lines, blank lines and comment lines
# for the specified languages, excluding test files.
# We are only interested in the summary, therefore the tail -1
SUMMARY_SRC="$(cloc "${SCRIPT_DIR}" --include-lang="Go" --not-match-f="_test\.go$" --not-match-d="tests/integration" --md | tail -1)"

# Run cloc for test files ending in _test.go
SUMMARY_TEST_FILES="$(cloc "${SCRIPT_DIR}" --include-lang="Go" --match-f='_test\.go$' --md | tail -1)"

# Run cloc for the tests/integration directory if it exists
SUMMARY_TEST_DIR=""
if [[ -d "${SCRIPT_DIR}/tests/integration" ]]; then
  SUMMARY_TEST_DIR="$(cloc "${SCRIPT_DIR}/tests/integration" --include-lang="Go" --md | tail -1)"
fi


# The SUMMARY strings are lines of a markdown table and look like this:
# SUM:|files|blank|comment|code
# We use the following command to split it into an array.
IFS='|' read -r -a TOKENS_SRC <<< "$SUMMARY_SRC"
IFS='|' read -r -a TOKENS_TEST_FILES <<< "$SUMMARY_TEST_FILES"
IFS='|' read -r -a TOKENS_TEST_DIR <<< "$SUMMARY_TEST_DIR"

# Store the individual tokens for better readability.
# Source Code
NUMBER_OF_FILES_SRC=${TOKENS_SRC[1]:-0} # Default to 0 if empty
COMMENT_LINES_SRC=${TOKENS_SRC[3]:-0}
LINES_OF_CODE_SRC=${TOKENS_SRC[4]:-0}

# Test Code (_test.go files)
LINES_OF_CODE_TEST_FILES=${TOKENS_TEST_FILES[4]:-0}

# Test Code (tests/integration dir)
LINES_OF_CODE_TEST_DIR=${TOKENS_TEST_DIR[4]:-0}

# Total Test Code
LINES_OF_TEST_CODE=$((LINES_OF_CODE_TEST_FILES + LINES_OF_CODE_TEST_DIR))

# Total Code (Source + Test)
TOTAL_LINES_OF_CODE=$((LINES_OF_CODE_SRC + LINES_OF_TEST_CODE))


# Print all results if no arguments are given.
if [[ $# -eq 0 ]] ; then
  awk -v loc_src=$LINES_OF_CODE_SRC \
      -v comments_src=$COMMENT_LINES_SRC \
      -v loc_test=$LINES_OF_TEST_CODE \
      -v loc_total=$TOTAL_LINES_OF_CODE \
      'BEGIN {
          label_width = 35 # Define a width for the labels
          printf "%-*s %6.1fk\n", label_width, "Total lines of code:", loc_total/1000;
          printf "%-*s %6.1fk\n", label_width, "Lines of source code:", loc_src/1000;
          printf "%-*s %6.1fk\n", label_width, "Lines of comments (source code):", comments_src/1000;
          printf "%-*s %6.1fk\n", label_width, "Lines of test code:", loc_test/1000;
          if (loc_src + comments_src > 0) {
            printf "%-*s %6.1f%%\n", label_width, "Comment Percentage:", 100*comments_src/(loc_src + comments_src);
          } else {
            printf "%-*s %6s\n", label_width, "Comment Percentage:", "N/A"; # Adjusted N/A alignment
          }
          if (loc_src + loc_test > 0) { 
            printf "%-*s %6.1f%%\n", label_width, "Test Percentage:", 100*loc_test/(loc_src + loc_test);
          } else {
            printf "%-*s %6s\n", label_width, "Test Percentage:", "N/A"; # Adjusted N/A alignment
          }
      }'
  exit 0
fi

# --- Argument Parsing ---

# Show lines of source code if --loc is given.
if [[ $* == *--loc* ]]
then
  awk -v a=$LINES_OF_CODE_SRC \
      'BEGIN {printf "%.1fk\n", a/1000}'
fi

# Show lines of comments if --comments is given.
if [[ $* == *--comments* ]]
then
  awk -v a=$COMMENT_LINES_SRC \
      'BEGIN {printf "%.1fk\n", a/1000}'
fi

# Show percentage of comments if --percentage is given.
if [[ $* == *--percentage* ]]
then
  awk -v a=$COMMENT_LINES_SRC -v b=$LINES_OF_CODE_SRC \
      'BEGIN {if (a+b > 0) printf "%.1f\n", 100*a/(a+b); else print "N/A"}'
fi

# Show lines of test code if --test-loc is given.
if [[ $* == *--test-loc* ]]
then
  awk -v a=$LINES_OF_TEST_CODE \
      'BEGIN {printf "%.1fk\n", a/1000}'
fi

# Show test percentage if --test-percentage is given.
if [[ $* == *--test-percentage* ]]
then
  awk -v a=$LINES_OF_TEST_CODE -v b=$LINES_OF_CODE_SRC \
      'BEGIN {if (a+b > 0) printf "%.1f\n", 100*a/(a+b); else print "N/A"}'
fi

# Show total lines of code if --total-loc is given.
if [[ $* == *--total-loc* ]]
then
  awk -v a=$TOTAL_LINES_OF_CODE \
      'BEGIN {printf "%.1fk\n", a/1000}'
fi
```

--------------------------------------------------------------------------------
/pkg/portainer/client/stack_test.go:
--------------------------------------------------------------------------------

```go
package client

import (
	"errors"
	"testing"
	"time"

	apimodels "github.com/portainer/client-api-go/v2/pkg/models"
	"github.com/portainer/portainer-mcp/pkg/portainer/models"
	"github.com/portainer/portainer-mcp/pkg/portainer/utils"
	"github.com/stretchr/testify/assert"
)

func TestGetStacks(t *testing.T) {
	now := time.Now().Unix()
	tests := []struct {
		name          string
		mockStacks    []*apimodels.PortainereeEdgeStack
		mockError     error
		expected      []models.Stack
		expectedError bool
	}{
		{
			name: "successful retrieval",
			mockStacks: []*apimodels.PortainereeEdgeStack{
				{
					ID:           1,
					Name:         "stack1",
					CreationDate: now,
					EdgeGroups:   []int64{1, 2},
				},
				{
					ID:           2,
					Name:         "stack2",
					CreationDate: now,
					EdgeGroups:   []int64{3},
				},
			},
			expected: []models.Stack{
				{
					ID:                  1,
					Name:                "stack1",
					CreatedAt:           time.Unix(now, 0).Format(time.RFC3339),
					EnvironmentGroupIds: []int{1, 2},
				},
				{
					ID:                  2,
					Name:                "stack2",
					CreatedAt:           time.Unix(now, 0).Format(time.RFC3339),
					EnvironmentGroupIds: []int{3},
				},
			},
		},
		{
			name:       "empty stacks",
			mockStacks: []*apimodels.PortainereeEdgeStack{},
			expected:   []models.Stack{},
		},
		{
			name:          "list error",
			mockError:     errors.New("failed to list stacks"),
			expectedError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockAPI := new(MockPortainerAPI)
			mockAPI.On("ListEdgeStacks").Return(tt.mockStacks, tt.mockError)

			client := &PortainerClient{cli: mockAPI}

			stacks, err := client.GetStacks()

			if tt.expectedError {
				assert.Error(t, err)
				return
			}
			assert.NoError(t, err)
			assert.Equal(t, tt.expected, stacks)
			mockAPI.AssertExpectations(t)
		})
	}
}

func TestGetStackFile(t *testing.T) {
	tests := []struct {
		name          string
		stackID       int
		mockFile      string
		mockError     error
		expected      string
		expectedError bool
	}{
		{
			name:     "successful retrieval",
			stackID:  1,
			mockFile: "version: '3'\nservices:\n  web:\n    image: nginx",
			expected: "version: '3'\nservices:\n  web:\n    image: nginx",
		},
		{
			name:          "get file error",
			stackID:       2,
			mockError:     errors.New("failed to get stack file"),
			expectedError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockAPI := new(MockPortainerAPI)
			mockAPI.On("GetEdgeStackFile", int64(tt.stackID)).Return(tt.mockFile, tt.mockError)

			client := &PortainerClient{cli: mockAPI}

			file, err := client.GetStackFile(tt.stackID)

			if tt.expectedError {
				assert.Error(t, err)
				return
			}
			assert.NoError(t, err)
			assert.Equal(t, tt.expected, file)
			mockAPI.AssertExpectations(t)
		})
	}
}

func TestCreateStack(t *testing.T) {
	tests := []struct {
		name                string
		stackName           string
		stackFile           string
		environmentGroupIds []int
		mockID              int64
		mockError           error
		expected            int
		expectedError       bool
	}{
		{
			name:                "successful creation",
			stackName:           "test-stack",
			stackFile:           "version: '3'\nservices:\n  web:\n    image: nginx",
			environmentGroupIds: []int{1, 2},
			mockID:              1,
			expected:            1,
		},
		{
			name:                "create error",
			stackName:           "test-stack",
			stackFile:           "version: '3'\nservices:\n  web:\n    image: nginx",
			environmentGroupIds: []int{1},
			mockError:           errors.New("failed to create stack"),
			expectedError:       true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockAPI := new(MockPortainerAPI)
			mockAPI.On("CreateEdgeStack", tt.stackName, tt.stackFile, utils.IntToInt64Slice(tt.environmentGroupIds)).Return(tt.mockID, tt.mockError)

			client := &PortainerClient{cli: mockAPI}

			id, err := client.CreateStack(tt.stackName, tt.stackFile, tt.environmentGroupIds)

			if tt.expectedError {
				assert.Error(t, err)
				return
			}
			assert.NoError(t, err)
			assert.Equal(t, tt.expected, id)
			mockAPI.AssertExpectations(t)
		})
	}
}

func TestUpdateStack(t *testing.T) {
	tests := []struct {
		name                string
		stackID             int
		stackFile           string
		environmentGroupIds []int
		mockError           error
		expectedError       bool
	}{
		{
			name:                "successful update",
			stackID:             1,
			stackFile:           "version: '3'\nservices:\n  web:\n    image: nginx:latest",
			environmentGroupIds: []int{1, 2},
		},
		{
			name:                "update error",
			stackID:             2,
			stackFile:           "version: '3'\nservices:\n  web:\n    image: nginx:latest",
			environmentGroupIds: []int{1},
			mockError:           errors.New("failed to update stack"),
			expectedError:       true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockAPI := new(MockPortainerAPI)
			mockAPI.On("UpdateEdgeStack", int64(tt.stackID), tt.stackFile, utils.IntToInt64Slice(tt.environmentGroupIds)).Return(tt.mockError)

			client := &PortainerClient{cli: mockAPI}

			err := client.UpdateStack(tt.stackID, tt.stackFile, tt.environmentGroupIds)

			if tt.expectedError {
				assert.Error(t, err)
				return
			}
			assert.NoError(t, err)
			mockAPI.AssertExpectations(t)
		})
	}
}

```

--------------------------------------------------------------------------------
/pkg/portainer/models/environment_test.go:
--------------------------------------------------------------------------------

```go
package models

import (
	"reflect"
	"testing"

	"github.com/portainer/client-api-go/v2/pkg/models"
)

func TestConvertEndpointToEnvironment(t *testing.T) {
	tests := []struct {
		name     string
		endpoint *models.PortainereeEndpoint
		want     Environment
	}{
		{
			name: "active docker-local environment with accesses",
			endpoint: &models.PortainereeEndpoint{
				ID:     1,
				Name:   "local-docker",
				Status: 1, // active
				Type:   1, // docker-local
				TagIds: []int64{1, 2},
				UserAccessPolicies: models.PortainerUserAccessPolicies{
					"1": models.PortainerAccessPolicy{RoleID: 1},
					"2": models.PortainerAccessPolicy{RoleID: 3},
				},
				TeamAccessPolicies: models.PortainerTeamAccessPolicies{
					"10": models.PortainerAccessPolicy{RoleID: 2},
					"20": models.PortainerAccessPolicy{RoleID: 4},
				},
			},
			want: Environment{
				ID:     1,
				Name:   "local-docker",
				Status: EnvironmentStatusActive,
				Type:   EnvironmentTypeDockerLocal,
				TagIds: []int{1, 2},
				UserAccesses: map[int]string{
					1: "environment_administrator",
					2: "standard_user",
				},
				TeamAccesses: map[int]string{
					10: "helpdesk_user",
					20: "readonly_user",
				},
			},
		},
		{
			name: "inactive kubernetes-agent environment with empty accesses",
			endpoint: &models.PortainereeEndpoint{
				ID:                 2,
				Name:               "k8s-agent",
				Status:             2, // inactive
				Type:               7, // kubernetes-edge-agent
				TagIds:             []int64{1},
				UserAccessPolicies: models.PortainerUserAccessPolicies{},
				TeamAccessPolicies: models.PortainerTeamAccessPolicies{},
			},
			want: Environment{
				ID:           2,
				Name:         "k8s-agent",
				Status:       EnvironmentStatusInactive,
				Type:         EnvironmentTypeKubernetesEdgeAgent,
				TagIds:       []int{1},
				UserAccesses: map[int]string{},
				TeamAccesses: map[int]string{},
			},
		},
		{
			name: "environment with invalid access IDs",
			endpoint: &models.PortainereeEndpoint{
				ID:     3,
				Name:   "invalid-access",
				Status: 1,
				Type:   1,
				TagIds: []int64{},
				UserAccessPolicies: models.PortainerUserAccessPolicies{
					"invalid": models.PortainerAccessPolicy{RoleID: 1},
					"2":       models.PortainerAccessPolicy{RoleID: 3},
				},
				TeamAccessPolicies: models.PortainerTeamAccessPolicies{
					"bad": models.PortainerAccessPolicy{RoleID: 2},
					"20":  models.PortainerAccessPolicy{RoleID: 4},
				},
			},
			want: Environment{
				ID:     3,
				Name:   "invalid-access",
				Status: EnvironmentStatusActive,
				Type:   EnvironmentTypeDockerLocal,
				TagIds: []int{},
				UserAccesses: map[int]string{
					2: "standard_user",
				},
				TeamAccesses: map[int]string{
					20: "readonly_user",
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := ConvertEndpointToEnvironment(tt.endpoint)
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("ConvertEndpointToEnvironment() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestConvertEnvironmentStatus(t *testing.T) {
	tests := []struct {
		name     string
		endpoint *models.PortainereeEndpoint
		want     string
	}{
		{
			name: "standard environment - active status",
			endpoint: &models.PortainereeEndpoint{
				Status: 1,
				Type:   1, // docker-local
			},
			want: EnvironmentStatusActive,
		},
		{
			name: "standard environment - inactive status",
			endpoint: &models.PortainereeEndpoint{
				Status: 2,
				Type:   2, // docker-agent
			},
			want: EnvironmentStatusInactive,
		},
		{
			name: "standard environment - unknown status",
			endpoint: &models.PortainereeEndpoint{
				Status: 0,
				Type:   3, // azure-aci
			},
			want: EnvironmentStatusUnknown,
		},
		{
			name: "edge environment - active with heartbeat",
			endpoint: &models.PortainereeEndpoint{
				Type:      4, // docker-edge-agent
				Heartbeat: true,
			},
			want: EnvironmentStatusActive,
		},
		{
			name: "edge environment - inactive without heartbeat",
			endpoint: &models.PortainereeEndpoint{
				Type:      7, // kubernetes-edge-agent
				Heartbeat: false,
			},
			want: EnvironmentStatusInactive,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := convertEnvironmentStatus(tt.endpoint)
			if got != tt.want {
				t.Errorf("convertEnvironmentStatus() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestConvertEnvironmentType(t *testing.T) {
	tests := []struct {
		name      string
		typeValue int
		want      string
	}{
		{
			name:      "docker-local type",
			typeValue: 1,
			want:      EnvironmentTypeDockerLocal,
		},
		{
			name:      "docker-agent type",
			typeValue: 2,
			want:      EnvironmentTypeDockerAgent,
		},
		{
			name:      "azure-aci type",
			typeValue: 3,
			want:      EnvironmentTypeAzureACI,
		},
		{
			name:      "docker-edge-agent type",
			typeValue: 4,
			want:      EnvironmentTypeDockerEdgeAgent,
		},
		{
			name:      "kubernetes-local type",
			typeValue: 5,
			want:      EnvironmentTypeKubernetesLocal,
		},
		{
			name:      "kubernetes-agent type",
			typeValue: 6,
			want:      EnvironmentTypeKubernetesAgent,
		},
		{
			name:      "kubernetes-edge-agent type",
			typeValue: 7,
			want:      EnvironmentTypeKubernetesEdgeAgent,
		},
		{
			name:      "unknown type",
			typeValue: 0,
			want:      EnvironmentTypeUnknown,
		},
		{
			name:      "invalid type",
			typeValue: 99,
			want:      EnvironmentTypeUnknown,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			endpoint := &models.PortainereeEndpoint{Type: int64(tt.typeValue)}
			got := convertEnvironmentType(endpoint)
			if got != tt.want {
				t.Errorf("convertEnvironmentType() = %v, want %v", got, tt.want)
			}
		})
	}
}

```

--------------------------------------------------------------------------------
/pkg/portainer/client/group_test.go:
--------------------------------------------------------------------------------

```go
package client

import (
	"errors"
	"testing"

	apimodels "github.com/portainer/client-api-go/v2/pkg/models"
	"github.com/portainer/portainer-mcp/pkg/portainer/models"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
)

func TestGetEnvironmentGroups(t *testing.T) {
	tests := []struct {
		name          string
		mockGroups    []*apimodels.EdgegroupsDecoratedEdgeGroup
		mockError     error
		expected      []models.Group
		expectedError bool
	}{
		{
			name: "successful retrieval",
			mockGroups: []*apimodels.EdgegroupsDecoratedEdgeGroup{
				{
					ID:        1,
					Name:      "group1",
					Endpoints: []int64{1, 2},
					TagIds:    []int64{1, 2},
				},
				{
					ID:        2,
					Name:      "group2",
					Endpoints: []int64{3},
					TagIds:    []int64{3},
				},
			},
			expected: []models.Group{
				{
					ID:             1,
					Name:           "group1",
					EnvironmentIds: []int{1, 2},
					TagIds:         []int{1, 2},
				},
				{
					ID:             2,
					Name:           "group2",
					EnvironmentIds: []int{3},
					TagIds:         []int{3},
				},
			},
		},
		{
			name:       "empty groups",
			mockGroups: []*apimodels.EdgegroupsDecoratedEdgeGroup{},
			expected:   []models.Group{},
		},
		{
			name:          "list error",
			mockError:     errors.New("failed to list edge groups"),
			expectedError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockAPI := new(MockPortainerAPI)
			mockAPI.On("ListEdgeGroups").Return(tt.mockGroups, tt.mockError)

			client := &PortainerClient{cli: mockAPI}

			groups, err := client.GetEnvironmentGroups()

			if tt.expectedError {
				assert.Error(t, err)
				return
			}
			assert.NoError(t, err)
			assert.Equal(t, tt.expected, groups)
			mockAPI.AssertExpectations(t)
		})
	}
}

func TestCreateEnvironmentGroup(t *testing.T) {
	tests := []struct {
		name           string
		groupName      string
		environmentIds []int
		mockID         int64
		mockError      error
		expectedID     int
		expectedError  bool
	}{
		{
			name:           "successful creation",
			groupName:      "new-group",
			environmentIds: []int{1, 2, 3},
			mockID:         1,
			expectedID:     1,
		},
		{
			name:           "creation error",
			groupName:      "error-group",
			environmentIds: []int{1},
			mockError:      errors.New("failed to create group"),
			expectedError:  true,
		},
		{
			name:           "empty environments",
			groupName:      "empty-group",
			environmentIds: []int{},
			mockID:         2,
			expectedID:     2,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockAPI := new(MockPortainerAPI)
			mockAPI.On("CreateEdgeGroup", tt.groupName, mock.Anything).Return(tt.mockID, tt.mockError)

			client := &PortainerClient{cli: mockAPI}

			id, err := client.CreateEnvironmentGroup(tt.groupName, tt.environmentIds)

			if tt.expectedError {
				assert.Error(t, err)
				return
			}
			assert.NoError(t, err)
			assert.Equal(t, tt.expectedID, id)
			mockAPI.AssertExpectations(t)
		})
	}
}

func TestUpdateEnvironmentGroupName(t *testing.T) {
	tests := []struct {
		name          string
		groupID       int
		newName       string
		mockError     error
		expectedError bool
	}{
		{
			name:    "successful update",
			groupID: 1,
			newName: "updated-group",
		},
		{
			name:          "update error",
			groupID:       1,
			newName:       "error-group",
			mockError:     errors.New("failed to update group name"),
			expectedError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockAPI := new(MockPortainerAPI)
			mockAPI.On("UpdateEdgeGroup", int64(tt.groupID), &tt.newName, mock.Anything, mock.Anything).Return(tt.mockError)

			client := &PortainerClient{cli: mockAPI}

			err := client.UpdateEnvironmentGroupName(tt.groupID, tt.newName)

			if tt.expectedError {
				assert.Error(t, err)
				return
			}
			assert.NoError(t, err)
			mockAPI.AssertExpectations(t)
		})
	}
}

func TestUpdateEnvironmentGroupEnvironments(t *testing.T) {
	tests := []struct {
		name           string
		groupID        int
		environmentIds []int
		mockError      error
		expectedError  bool
	}{
		{
			name:           "successful update",
			groupID:        1,
			environmentIds: []int{1, 2, 3},
		},
		{
			name:           "update error",
			groupID:        1,
			environmentIds: []int{1},
			mockError:      errors.New("failed to update group environments"),
			expectedError:  true,
		},
		{
			name:           "empty environments",
			groupID:        1,
			environmentIds: []int{},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockAPI := new(MockPortainerAPI)
			mockAPI.On("UpdateEdgeGroup", int64(tt.groupID), mock.Anything, mock.Anything, mock.Anything).Return(tt.mockError)

			client := &PortainerClient{cli: mockAPI}

			err := client.UpdateEnvironmentGroupEnvironments(tt.groupID, tt.environmentIds)

			if tt.expectedError {
				assert.Error(t, err)
				return
			}
			assert.NoError(t, err)
			mockAPI.AssertExpectations(t)
		})
	}
}

func TestUpdateEnvironmentGroupTags(t *testing.T) {
	tests := []struct {
		name          string
		groupID       int
		tagIds        []int
		mockError     error
		expectedError bool
	}{
		{
			name:    "successful update",
			groupID: 1,
			tagIds:  []int{1, 2, 3},
		},
		{
			name:          "update error",
			groupID:       1,
			tagIds:        []int{1},
			mockError:     errors.New("failed to update group tags"),
			expectedError: true,
		},
		{
			name:    "empty tags",
			groupID: 1,
			tagIds:  []int{},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockAPI := new(MockPortainerAPI)
			mockAPI.On("UpdateEdgeGroup", int64(tt.groupID), mock.Anything, mock.Anything, mock.Anything).Return(tt.mockError)

			client := &PortainerClient{cli: mockAPI}

			err := client.UpdateEnvironmentGroupTags(tt.groupID, tt.tagIds)

			if tt.expectedError {
				assert.Error(t, err)
				return
			}
			assert.NoError(t, err)
			mockAPI.AssertExpectations(t)
		})
	}
}

```

--------------------------------------------------------------------------------
/tests/integration/containers/portainer.go:
--------------------------------------------------------------------------------

```go
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
}

```

--------------------------------------------------------------------------------
/tests/integration/stack_test.go:
--------------------------------------------------------------------------------

```go
package integration

import (
	"encoding/json"
	"fmt"
	"testing"

	mcpmodels "github.com/mark3labs/mcp-go/mcp"
	"github.com/portainer/portainer-mcp/internal/mcp"
	"github.com/portainer/portainer-mcp/pkg/portainer/models"
	"github.com/portainer/portainer-mcp/tests/integration/helpers"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

const (
	testStackName        = "test-mcp-stack"
	testStackFile        = "version: '3'\nservices:\n  web:\n    image: nginx:latest"
	testStackFileUpdated = "version: '3'\nservices:\n  web:\n    image: nginx:alpine"
	testEdgeGroupName    = "test-stack-group"
)

// prepareStackManagementTestEnvironment creates a test environment group needed for stack tests
func prepareStackManagementTestEnvironment(t *testing.T, env *helpers.TestEnv) int {
	// First, enable Edge features in Portainer
	host, port := env.Portainer.GetHostAndPort()
	serverAddr := fmt.Sprintf("%s:%s", host, port)
	tunnelAddr := fmt.Sprintf("%s:8000", host)

	err := env.RawClient.UpdateSettings(true, serverAddr, tunnelAddr)
	require.NoError(t, err, "Failed to update settings to enable Edge features")

	// Create a test environment group for the stack to be associated with
	testGroupID, err := env.RawClient.CreateEdgeGroup(testEdgeGroupName, []int64{})
	require.NoError(t, err, "Failed to create test environment group via raw client")

	return int(testGroupID)
}

// TestStackManagement is an integration test suite that verifies the complete
// lifecycle of stack management in Portainer MCP. It tests stack creation,
// retrieval, file content retrieval, and updates.
func TestStackManagement(t *testing.T) {
	env := helpers.NewTestEnv(t)
	defer env.Cleanup(t)

	// Prepare the test environment
	testGroupID := prepareStackManagementTestEnvironment(t, env)

	var testStackID int

	// Subtest: Stack Creation
	// Verifies that:
	// - A new stack can be created via the MCP handler
	// - The handler response indicates success with an ID
	// - The created stack exists in Portainer when checked directly via Raw Client
	t.Run("Stack Creation", func(t *testing.T) {
		handler := env.MCPServer.HandleCreateStack()
		request := mcp.CreateMCPRequest(map[string]any{
			"name":                testStackName,
			"file":                testStackFile,
			"environmentGroupIds": []any{float64(testGroupID)},
		})

		result, err := handler(env.Ctx, request)
		require.NoError(t, err, "Failed to create stack via MCP handler")

		textContent, ok := result.Content[0].(mcpmodels.TextContent)
		require.True(t, ok, "Expected text content in MCP response")

		// Check for success message and extract ID for later tests
		assert.Contains(t, textContent.Text, "Stack created successfully with ID:", "Success message prefix mismatch")

		// Verify by fetching stacks directly via client and finding the created stack by name
		stack, err := env.RawClient.GetEdgeStackByName(testStackName)
		require.NoError(t, err, "Failed to get stack directly via client after creation")
		assert.Equal(t, testStackName, stack.Name, "Stack name mismatch")

		// Extract stack ID for subsequent tests
		testStackID = int(stack.ID)
	})

	// Subtest: Stack Listing
	// Verifies that:
	// - The stack list can be retrieved via the MCP handler
	// - The list contains the expected stack
	// - The stack data matches the expected properties
	t.Run("Stack Listing", func(t *testing.T) {
		handler := env.MCPServer.HandleGetStacks()
		result, err := handler(env.Ctx, mcp.CreateMCPRequest(nil))
		require.NoError(t, err, "Failed to get stacks via MCP handler")

		assert.Len(t, result.Content, 1, "Expected exactly one content block in the result")
		textContent, ok := result.Content[0].(mcpmodels.TextContent)
		assert.True(t, ok, "Expected text content in MCP response")

		var retrievedStacks []models.Stack
		err = json.Unmarshal([]byte(textContent.Text), &retrievedStacks)
		require.NoError(t, err, "Failed to unmarshal retrieved stacks")
		require.Len(t, retrievedStacks, 1, "Expected exactly one stack after unmarshalling")

		stack := retrievedStacks[0]
		assert.Equal(t, testStackName, stack.Name, "Stack name mismatch")

		// Fetch the same stack directly via the client
		rawStack, err := env.RawClient.GetEdgeStack(int64(testStackID))
		require.NoError(t, err, "Failed to get stack directly via client")

		// Convert the raw stack to the expected Stack model
		expectedStack := models.ConvertEdgeStackToStack(rawStack)
		assert.Equal(t, expectedStack, stack, "Stack mismatch between MCP handler and direct client call")
	})

	// Subtest: Get Stack File
	// Verifies that:
	// - The stack file can be retrieved via the MCP handler
	// - The file content matches the content used during creation
	t.Run("Get Stack File", func(t *testing.T) {
		handler := env.MCPServer.HandleGetStackFile()
		request := mcp.CreateMCPRequest(map[string]any{
			"id": float64(testStackID),
		})

		result, err := handler(env.Ctx, request)
		require.NoError(t, err, "Failed to get stack file via MCP handler")

		textContent, ok := result.Content[0].(mcpmodels.TextContent)
		require.True(t, ok, "Expected text content in MCP response")

		// Compare with the original content
		assert.Equal(t, testStackFile, textContent.Text, "Stack file content mismatch")
	})

	// Subtest: Stack Update
	// Verifies that:
	// - A stack can be updated via the MCP handler
	// - The handler response indicates success
	// - The stack file is updated when checked directly via Raw Client
	t.Run("Stack Update", func(t *testing.T) {
		handler := env.MCPServer.HandleUpdateStack()
		request := mcp.CreateMCPRequest(map[string]any{
			"id":                  float64(testStackID),
			"file":                testStackFileUpdated,
			"environmentGroupIds": []any{float64(testGroupID)},
		})

		result, err := handler(env.Ctx, request)
		require.NoError(t, err, "Failed to update stack via MCP handler")

		textContent, ok := result.Content[0].(mcpmodels.TextContent)
		require.True(t, ok, "Expected text content in MCP response")
		assert.Contains(t, textContent.Text, "Stack updated successfully", "Success message mismatch")

		// Verify by fetching stack file directly via raw client
		updatedFile, err := env.RawClient.GetEdgeStackFile(int64(testStackID))
		require.NoError(t, err, "Failed to get stack file via raw client after update")
		assert.Equal(t, testStackFileUpdated, updatedFile, "Stack file was not updated correctly")
	})
}

```

--------------------------------------------------------------------------------
/internal/mcp/access_group.go:
--------------------------------------------------------------------------------

```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) AddAccessGroupFeatures() {
	s.addToolIfExists(ToolListAccessGroups, s.HandleGetAccessGroups())

	if !s.readOnly {
		s.addToolIfExists(ToolCreateAccessGroup, s.HandleCreateAccessGroup())
		s.addToolIfExists(ToolUpdateAccessGroupName, s.HandleUpdateAccessGroupName())
		s.addToolIfExists(ToolUpdateAccessGroupUserAccesses, s.HandleUpdateAccessGroupUserAccesses())
		s.addToolIfExists(ToolUpdateAccessGroupTeamAccesses, s.HandleUpdateAccessGroupTeamAccesses())
		s.addToolIfExists(ToolAddEnvironmentToAccessGroup, s.HandleAddEnvironmentToAccessGroup())
		s.addToolIfExists(ToolRemoveEnvironmentFromAccessGroup, s.HandleRemoveEnvironmentFromAccessGroup())
	}
}

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 mcp.NewToolResultErrorFromErr("failed to get access groups", err), nil
		}

		data, err := json.Marshal(accessGroups)
		if err != nil {
			return mcp.NewToolResultErrorFromErr("failed to marshal access groups", err), nil
		}

		return mcp.NewToolResultText(string(data)), nil
	}
}

func (s *PortainerMCPServer) HandleCreateAccessGroup() 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 mcp.NewToolResultErrorFromErr("invalid name parameter", err), nil
		}

		environmentIds, err := parser.GetArrayOfIntegers("environmentIds", false)
		if err != nil {
			return mcp.NewToolResultErrorFromErr("invalid environmentIds parameter", err), nil
		}

		groupID, err := s.cli.CreateAccessGroup(name, environmentIds)
		if err != nil {
			return mcp.NewToolResultErrorFromErr("failed to create access group", err), nil
		}

		return mcp.NewToolResultText(fmt.Sprintf("Access group created successfully with ID: %d", groupID)), nil
	}
}

func (s *PortainerMCPServer) HandleUpdateAccessGroupName() 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 mcp.NewToolResultErrorFromErr("invalid id parameter", err), nil
		}

		name, err := parser.GetString("name", true)
		if err != nil {
			return mcp.NewToolResultErrorFromErr("invalid name parameter", err), nil
		}

		err = s.cli.UpdateAccessGroupName(id, name)
		if err != nil {
			return mcp.NewToolResultErrorFromErr("failed to update access group name", err), nil
		}

		return mcp.NewToolResultText("Access group name updated successfully"), nil
	}
}

func (s *PortainerMCPServer) HandleUpdateAccessGroupUserAccesses() 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 mcp.NewToolResultErrorFromErr("invalid id parameter", err), nil
		}

		userAccesses, err := parser.GetArrayOfObjects("userAccesses", true)
		if err != nil {
			return mcp.NewToolResultErrorFromErr("invalid userAccesses parameter", err), nil
		}

		userAccessesMap, err := parseAccessMap(userAccesses)
		if err != nil {
			return mcp.NewToolResultErrorFromErr("invalid user accesses", err), nil
		}

		err = s.cli.UpdateAccessGroupUserAccesses(id, userAccessesMap)
		if err != nil {
			return mcp.NewToolResultErrorFromErr("failed to update access group user accesses", err), nil
		}

		return mcp.NewToolResultText("Access group user accesses updated successfully"), nil
	}
}

func (s *PortainerMCPServer) HandleUpdateAccessGroupTeamAccesses() 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 mcp.NewToolResultErrorFromErr("invalid id parameter", err), nil
		}

		teamAccesses, err := parser.GetArrayOfObjects("teamAccesses", true)
		if err != nil {
			return mcp.NewToolResultErrorFromErr("invalid teamAccesses parameter", err), nil
		}

		teamAccessesMap, err := parseAccessMap(teamAccesses)
		if err != nil {
			return mcp.NewToolResultErrorFromErr("invalid team accesses", err), nil
		}

		err = s.cli.UpdateAccessGroupTeamAccesses(id, teamAccessesMap)
		if err != nil {
			return mcp.NewToolResultErrorFromErr("failed to update access group team accesses", err), nil
		}

		return mcp.NewToolResultText("Access group team accesses updated successfully"), nil
	}
}

func (s *PortainerMCPServer) HandleAddEnvironmentToAccessGroup() 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 mcp.NewToolResultErrorFromErr("invalid id parameter", err), nil
		}

		environmentId, err := parser.GetInt("environmentId", true)
		if err != nil {
			return mcp.NewToolResultErrorFromErr("invalid environmentId parameter", err), nil
		}

		err = s.cli.AddEnvironmentToAccessGroup(id, environmentId)
		if err != nil {
			return mcp.NewToolResultErrorFromErr("failed to add environment to access group", err), nil
		}

		return mcp.NewToolResultText("Environment added to access group successfully"), nil
	}
}

func (s *PortainerMCPServer) HandleRemoveEnvironmentFromAccessGroup() 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 mcp.NewToolResultErrorFromErr("invalid id parameter", err), nil
		}

		environmentId, err := parser.GetInt("environmentId", true)
		if err != nil {
			return mcp.NewToolResultErrorFromErr("invalid environmentId parameter", err), nil
		}

		err = s.cli.RemoveEnvironmentFromAccessGroup(id, environmentId)
		if err != nil {
			return mcp.NewToolResultErrorFromErr("failed to remove environment from access group", err), nil
		}

		return mcp.NewToolResultText("Environment removed from access group successfully"), nil
	}
}

```

--------------------------------------------------------------------------------
/internal/mcp/server.go:
--------------------------------------------------------------------------------

```go
package mcp

import (
	"fmt"
	"log"
	"net/http"

	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
	"github.com/portainer/portainer-mcp/pkg/portainer/client"
	"github.com/portainer/portainer-mcp/pkg/portainer/models"
	"github.com/portainer/portainer-mcp/pkg/toolgen"
)

const (
	// MinimumToolsVersion is the minimum supported version of the tools.yaml file
	MinimumToolsVersion = "1.0"
	// SupportedPortainerVersion is the version of Portainer that is supported by this tool
	SupportedPortainerVersion = "2.31.2"
)

// PortainerClient defines the interface for the wrapper client used by the MCP server
type PortainerClient interface {
	// Tag methods
	GetEnvironmentTags() ([]models.EnvironmentTag, error)
	CreateEnvironmentTag(name string) (int, error)

	// Environment methods
	GetEnvironments() ([]models.Environment, error)
	UpdateEnvironmentTags(id int, tagIds []int) error
	UpdateEnvironmentUserAccesses(id int, userAccesses map[int]string) error
	UpdateEnvironmentTeamAccesses(id int, teamAccesses map[int]string) error

	// Environment Group methods
	GetEnvironmentGroups() ([]models.Group, error)
	CreateEnvironmentGroup(name string, environmentIds []int) (int, error)
	UpdateEnvironmentGroupName(id int, name string) error
	UpdateEnvironmentGroupEnvironments(id int, environmentIds []int) error
	UpdateEnvironmentGroupTags(id int, tagIds []int) error

	// Access Group methods
	GetAccessGroups() ([]models.AccessGroup, error)
	CreateAccessGroup(name string, environmentIds []int) (int, error)
	UpdateAccessGroupName(id int, name string) error
	UpdateAccessGroupUserAccesses(id int, userAccesses map[int]string) error
	UpdateAccessGroupTeamAccesses(id int, teamAccesses map[int]string) error
	AddEnvironmentToAccessGroup(id int, environmentId int) error
	RemoveEnvironmentFromAccessGroup(id int, environmentId int) error

	// Stack methods
	GetStacks() ([]models.Stack, error)
	GetStackFile(id int) (string, error)
	CreateStack(name string, file string, environmentGroupIds []int) (int, error)
	UpdateStack(id int, file string, environmentGroupIds []int) error

	// Team methods
	CreateTeam(name string) (int, error)
	GetTeams() ([]models.Team, error)
	UpdateTeamName(id int, name string) error
	UpdateTeamMembers(id int, userIds []int) error

	// User methods
	GetUsers() ([]models.User, error)
	UpdateUserRole(id int, role string) error

	// Settings methods
	GetSettings() (models.PortainerSettings, error)

	// Version methods
	GetVersion() (string, error)

	// Docker Proxy methods
	ProxyDockerRequest(opts models.DockerProxyRequestOptions) (*http.Response, error)

	// Kubernetes Proxy methods
	ProxyKubernetesRequest(opts models.KubernetesProxyRequestOptions) (*http.Response, error)
}

// PortainerMCPServer is the main server that handles MCP protocol communication
// with AI assistants and translates them into Portainer API calls.
type PortainerMCPServer struct {
	srv      *server.MCPServer
	cli      PortainerClient
	tools    map[string]mcp.Tool
	readOnly bool
}

// 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
	disableVersionCheck 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
	}
}

// WithDisableVersionCheck disables the Portainer server version check.
// This allows connecting to unsupported Portainer versions.
func WithDisableVersionCheck(disable bool) ServerOption {
	return func(opts *serverOptions) {
		opts.disableVersionCheck = disable
	}
}

// NewPortainerMCPServer creates a new Portainer MCP server.
//
// This server provides an implementation of the MCP protocol for Portainer,
// allowing AI assistants to interact with Portainer through a structured API.
//
// Parameters:
//   - serverURL: The base URL of the Portainer server (e.g., "https://portainer.example.com")
//   - token: The API token for authenticating with the Portainer server
//   - toolsPath: Path to the tools.yaml file that defines the available MCP tools
//   - options: Optional functional options for customizing server behavior (e.g., WithClient)
//
// Returns:
//   - A configured PortainerMCPServer instance ready to be started
//   - An error if initialization fails
//
// Possible errors:
//   - Failed to load tools from the specified path
//   - Failed to communicate with the Portainer server
//   - Incompatible Portainer server version
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))
	}

	if !opts.disableVersionCheck {
		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.5.1",
			server.WithToolCapabilities(true),
			server.WithLogging(),
		),
		cli:      portainerClient,
		tools:    tools,
		readOnly: opts.readOnly,
	}, nil
}

// Start begins listening for MCP protocol messages on standard input/output.
// This is a blocking call that will run until the connection is closed.
func (s *PortainerMCPServer) Start() error {
	return server.ServeStdio(s.srv)
}

// addToolIfExists adds a tool to the server if it exists in the tools map
func (s *PortainerMCPServer) addToolIfExists(toolName string, handler server.ToolHandlerFunc) {
	if tool, exists := s.tools[toolName]; exists {
		s.srv.AddTool(tool, handler)
	} else {
		log.Printf("Tool %s not found, will not be registered for MCP usage", toolName)
	}
}

```

--------------------------------------------------------------------------------
/docs/clients_and_models.md:
--------------------------------------------------------------------------------

```markdown
# Portainer MCP Client and Model Usage Guide

This document clarifies the different client implementations and model structures used within the `portainer-mcp` project to prevent confusion and aid development.

## Overview

The project interacts with the Portainer API using two main client layers and involves two primary sets of data models:

1.  **Raw Client & Models:** Provided by the `portainer/client-api-go` library.
2.  **Wrapper Client & Local Models:** Defined within `portainer-mcp/pkg/portainer/`.

Understanding the distinction and interaction between these layers is crucial.

## Clients

### 1. Raw Client (`portainer/client-api-go/v2`)

*   **Package:** `github.com/portainer/client-api-go/v2`
*   **Role:** This is the underlying library that directly communicates with the Portainer API.
*   **Usage:** It's instantiated within the Wrapper Client. It's also often used directly within **integration tests** (`tests/integration/`) to fetch the ground-truth state from Portainer for comparison against the MCP handler's output.
*   **Models Used:** Interacts primarily with the Raw Models defined in `github.com/portainer/client-api-go/v2/pkg/models`.

### 2. Wrapper Client (`portainer-mcp/pkg/portainer/client`)

*   **Package:** `github.com/portainer/portainer-mcp/pkg/portainer/client`
*   **Role:** This client acts as an **abstraction layer** on top of the Raw Client. Its primary purposes are:
    *   To simplify the interface exposed to the rest of the `portainer-mcp` application (specifically the MCP server handlers in `internal/mcp/`).
    *   To perform necessary **data transformations**, converting Raw Models from the API into the simpler, tailored Local Models.
    *   To encapsulate common logic or error handling related to Portainer API interactions.
*   **Usage:** This is the client used by the **MCP server handlers** (`internal/mcp/server.go` instantiates it and passes it to handlers).
*   **Models Used:** Takes Raw Models as input from the Raw Client but typically **returns Local Models** (`portainer-mcp/pkg/portainer/models`) after performing conversions.

## Models

### 1. Raw Models (`portainer/client-api-go/v2/pkg/models`)

*   **Package:** `github.com/portainer/client-api-go/v2/pkg/models`
*   **Role:** These structs directly map to the data structures returned by the Portainer API.
*   **Characteristics:** Can be complex, may contain fields not relevant to MCP, and might use types (like numeric enums) that are less convenient for MCP's purposes.
*   **Examples:** `models.PortainereeSettings`, `models.PortainereeEndpoint`.
*   **Usage:** Returned by the Raw Client, used as input to the conversion functions within the Wrapper Client / Local Models package.
*   **Naming Convention:** To improve clarity, variables holding instances of these Raw Models are typically prefixed with `raw` (e.g., `rawSettings`, `rawEndpoint`).

### 2. Local Models (`portainer-mcp/pkg/portainer/models`)

*   **Package:** `github.com/portainer/portainer-mcp/pkg/portainer/models`
*   **Role:** These are simplified, tailored structs designed specifically for use within the `portainer-mcp` application and for exposure via the MCP tools.
*   **Characteristics:** Simpler structure, contain only relevant fields, often use more convenient types (like string enums).
*   **Examples:** `models.PortainerSettings`, `models.Environment`, `models.EnvironmentTag`.
*   **Usage:** Returned by the Wrapper Client, used within MCP server handlers, and ultimately determine the structure of data returned by MCP tools.

### 3. Conversion Functions

*   **Location:** Typically reside within `portainer-mcp/pkg/portainer/models`.
*   **Role:** Bridge the gap between Raw Models and Local Models.
*   **Examples:** `ConvertSettingsToPortainerSettings`, `ConvertEndpointToEnvironment`.
*   **Usage:** Called by the Wrapper Client methods to transform data before returning it. The function parameters accepting Raw Models typically follow the `raw` prefix naming convention (e.g., `func ConvertSettingsToPortainerSettings(rawSettings *apimodels.PortainereeSettings)`).

## Typical Workflow Example (`GetSettings`)

1.  **MCP Handler (`internal/mcp/settings.go`)**: Receives a tool call.
2.  Calls `s.cli.GetSettings()`. Here, `s.cli` is an instance of the **Wrapper Client** (`PortainerClient`).
3.  **Wrapper Client (`pkg/portainer/client/settings.go`)**: Its `GetSettings` method is executed.
4.  Calls the **Raw Client**'s `GetSettings` method (e.g., `c.cli.GetSettings()`).
5.  Raw Client interacts with the Portainer API and returns a **Raw Model** (`*portainermodels.PortainereeSettings`).
6.  Wrapper Client calls the **Conversion Function** (`models.ConvertSettingsToPortainerSettings`) with the Raw Model.
7.  Conversion Function returns a **Local Model** (`models.PortainerSettings`).
8.  Wrapper Client returns the Local Model to the MCP Handler.
9.  MCP Handler marshals the **Local Model** (`models.PortainerSettings`) into JSON and returns it as the tool result.

## Import Conventions

To improve clarity, especially in files where both model types might appear (like tests), consider using consistent import aliases. Leaving the local `portainer-mcp/pkg/portainer/models` package as the default `models` and aliasing the external library is recommended:

```go
import (
    "github.com/portainer/portainer-mcp/pkg/portainer/models" // Default: models (Local MCP Models)
    apimodels "github.com/portainer/client-api-go/v2/pkg/models"      // Alias: apimodels (Raw Client-API-Go Models)
)
```

This approach keeps code cleaner for the more frequently used local models while clearly indicating when the raw API models are involved.

## Testing Implications

*   **Unit Tests** (like `pkg/portainer/client/settings_test.go`): Should mock the Raw Client interface and verify that the Wrapper Client correctly calls the Raw Client and performs the necessary conversions, returning the expected Local Model.
*   **Integration Tests** (like `tests/integration/settings_test.go`): 
    *   Call the MCP handler, which uses the Wrapper Client internally and returns JSON representing a Local Model.
    *   Often need to *also* call the Raw Client directly to get the ground-truth state from the live Portainer instance (variables holding this state should follow the `raw` prefix convention, e.g., `rawEndpoint`).
    *   May need to manually apply the same Conversion Function to the Raw Model obtained from the Raw Client to create an expected Local Model for comparison against the handler's result.

By understanding these distinct layers and their interactions, development and testing within `portainer-mcp` should be clearer. 
```

--------------------------------------------------------------------------------
/pkg/portainer/client/environment_test.go:
--------------------------------------------------------------------------------

```go
package client

import (
	"errors"
	"testing"

	apimodels "github.com/portainer/client-api-go/v2/pkg/models"
	"github.com/portainer/portainer-mcp/pkg/portainer/models"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
)

func TestGetEnvironments(t *testing.T) {
	tests := []struct {
		name          string
		mockEndpoints []*apimodels.PortainereeEndpoint
		mockError     error
		expected      []models.Environment
		expectedError bool
	}{
		{
			name: "successful retrieval",
			mockEndpoints: []*apimodels.PortainereeEndpoint{
				{
					ID:      1,
					Name:    "env1",
					GroupID: 1,
					Status:  1, // active
					Type:    1, // docker-local
					TagIds:  []int64{1, 2},
					UserAccessPolicies: apimodels.PortainerUserAccessPolicies{
						"1": apimodels.PortainerAccessPolicy{RoleID: 1}, // environment_administrator
						"2": apimodels.PortainerAccessPolicy{RoleID: 2}, // helpdesk_user
						"3": apimodels.PortainerAccessPolicy{RoleID: 3}, // standard_user
						"4": apimodels.PortainerAccessPolicy{RoleID: 4}, // readonly_user
						"5": apimodels.PortainerAccessPolicy{RoleID: 5}, // operator_user
					},
					TeamAccessPolicies: apimodels.PortainerTeamAccessPolicies{
						"6":  apimodels.PortainerAccessPolicy{RoleID: 1}, // environment_administrator
						"7":  apimodels.PortainerAccessPolicy{RoleID: 2}, // helpdesk_user
						"8":  apimodels.PortainerAccessPolicy{RoleID: 3}, // standard_user
						"9":  apimodels.PortainerAccessPolicy{RoleID: 4}, // readonly_user
						"10": apimodels.PortainerAccessPolicy{RoleID: 5}, // operator_user
					},
				},
				{
					ID:      2,
					Name:    "env2",
					GroupID: 1,
					Status:  2, // inactive
					Type:    2, // docker-agent
					TagIds:  []int64{3},
				},
				{
					ID:     3,
					Name:   "env3",
					Status: 0, // unknown
					Type:   0, // unknown
				},
			},
			expected: []models.Environment{
				{
					ID:     1,
					Name:   "env1",
					Status: "active",
					Type:   "docker-local",
					TagIds: []int{1, 2},
					UserAccesses: map[int]string{
						1: "environment_administrator",
						2: "helpdesk_user",
						3: "standard_user",
						4: "readonly_user",
						5: "operator_user",
					},
					TeamAccesses: map[int]string{
						6:  "environment_administrator",
						7:  "helpdesk_user",
						8:  "standard_user",
						9:  "readonly_user",
						10: "operator_user",
					},
				},
				{
					ID:           2,
					Name:         "env2",
					Status:       "inactive",
					Type:         "docker-agent",
					TagIds:       []int{3},
					UserAccesses: map[int]string{},
					TeamAccesses: map[int]string{},
				},
				{
					ID:           3,
					Name:         "env3",
					Status:       "unknown",
					Type:         "unknown",
					TagIds:       []int{},
					UserAccesses: map[int]string{},
					TeamAccesses: map[int]string{},
				},
			},
		},
		{
			name:          "empty environments",
			mockEndpoints: []*apimodels.PortainereeEndpoint{},
			expected:      []models.Environment{},
		},
		{
			name:          "list error",
			mockError:     errors.New("failed to list endpoints"),
			expectedError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockAPI := new(MockPortainerAPI)
			mockAPI.On("ListEndpoints").Return(tt.mockEndpoints, tt.mockError)

			client := &PortainerClient{cli: mockAPI}

			environments, err := client.GetEnvironments()

			if tt.expectedError {
				assert.Error(t, err)
				return
			}
			assert.NoError(t, err)
			assert.Equal(t, tt.expected, environments)
			mockAPI.AssertExpectations(t)
		})
	}
}

func TestUpdateEnvironmentTags(t *testing.T) {
	tests := []struct {
		name          string
		envID         int
		tagIds        []int
		mockError     error
		expectedError bool
	}{
		{
			name:   "successful update",
			envID:  1,
			tagIds: []int{1, 2, 3},
		},
		{
			name:          "update error",
			envID:         1,
			tagIds:        []int{1},
			mockError:     errors.New("failed to update tags"),
			expectedError: true,
		},
		{
			name:   "empty tags",
			envID:  1,
			tagIds: []int{},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockAPI := new(MockPortainerAPI)
			mockAPI.On("UpdateEndpoint", int64(tt.envID), mock.Anything, mock.Anything, mock.Anything).Return(tt.mockError)

			client := &PortainerClient{cli: mockAPI}

			err := client.UpdateEnvironmentTags(tt.envID, tt.tagIds)

			if tt.expectedError {
				assert.Error(t, err)
				return
			}
			assert.NoError(t, err)
			mockAPI.AssertExpectations(t)
		})
	}
}

func TestUpdateEnvironmentUserAccesses(t *testing.T) {
	tests := []struct {
		name          string
		envID         int
		userAccesses  map[int]string
		mockError     error
		expectedError bool
	}{
		{
			name:  "successful update",
			envID: 1,
			userAccesses: map[int]string{
				1: "environment_administrator",
				2: "helpdesk_user",
				3: "standard_user",
				4: "readonly_user",
				5: "operator_user",
			},
		},
		{
			name:  "update error",
			envID: 1,
			userAccesses: map[int]string{
				1: "environment_administrator",
			},
			mockError:     errors.New("failed to update user accesses"),
			expectedError: true,
		},
		{
			name:         "empty accesses",
			envID:        1,
			userAccesses: map[int]string{},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockAPI := new(MockPortainerAPI)
			mockAPI.On("UpdateEndpoint", int64(tt.envID), mock.Anything, mock.Anything, mock.Anything).Return(tt.mockError)

			client := &PortainerClient{cli: mockAPI}

			err := client.UpdateEnvironmentUserAccesses(tt.envID, tt.userAccesses)

			if tt.expectedError {
				assert.Error(t, err)
				return
			}
			assert.NoError(t, err)
			mockAPI.AssertExpectations(t)
		})
	}
}

func TestUpdateEnvironmentTeamAccesses(t *testing.T) {
	tests := []struct {
		name          string
		envID         int
		teamAccesses  map[int]string
		mockError     error
		expectedError bool
	}{
		{
			name:  "successful update",
			envID: 1,
			teamAccesses: map[int]string{
				1: "environment_administrator",
				2: "helpdesk_user",
				3: "standard_user",
				4: "readonly_user",
				5: "operator_user",
			},
		},
		{
			name:  "update error",
			envID: 1,
			teamAccesses: map[int]string{
				1: "environment_administrator",
			},
			mockError:     errors.New("failed to update team accesses"),
			expectedError: true,
		},
		{
			name:         "empty accesses",
			envID:        1,
			teamAccesses: map[int]string{},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockAPI := new(MockPortainerAPI)
			mockAPI.On("UpdateEndpoint", int64(tt.envID), mock.Anything, mock.Anything, mock.Anything).Return(tt.mockError)

			client := &PortainerClient{cli: mockAPI}

			err := client.UpdateEnvironmentTeamAccesses(tt.envID, tt.teamAccesses)

			if tt.expectedError {
				assert.Error(t, err)
				return
			}
			assert.NoError(t, err)
			mockAPI.AssertExpectations(t)
		})
	}
}

```

--------------------------------------------------------------------------------
/tests/integration/team_test.go:
--------------------------------------------------------------------------------

```go
package integration

import (
	"encoding/json"
	"testing"

	mcpmodels "github.com/mark3labs/mcp-go/mcp"
	"github.com/portainer/portainer-mcp/internal/mcp"
	"github.com/portainer/portainer-mcp/pkg/portainer/models"
	"github.com/portainer/portainer-mcp/tests/integration/helpers"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

const (
	testTeamName         = "test-mcp-team"
	testTeamNewName      = "test-mcp-team-updated"
	testUser1Name        = "test-team-user1"
	testUser2Name        = "test-team-user2"
	testTeamUserPassword = "testpassword"
	teamUserRoleStandard = 2 // Portainer API role ID for Standard User
)

// prepareTeamManagementTestEnvironment creates test users that can be assigned to teams
func prepareTeamManagementTestEnvironment(t *testing.T, env *helpers.TestEnv) (int, int) {
	testUser1ID, err := env.RawClient.CreateUser(testUser1Name, testTeamUserPassword, teamUserRoleStandard)
	require.NoError(t, err, "Failed to create first test user via raw client")

	testUser2ID, err := env.RawClient.CreateUser(testUser2Name, testTeamUserPassword, teamUserRoleStandard)
	require.NoError(t, err, "Failed to create second test user via raw client")

	return int(testUser1ID), int(testUser2ID)
}

// TestTeamManagement is an integration test suite that verifies the complete
// lifecycle of team management in Portainer MCP. It tests team creation,
// listing, name updates, and member management.
func TestTeamManagement(t *testing.T) {
	env := helpers.NewTestEnv(t)
	defer env.Cleanup(t)

	// Prepare the test environment
	testUser1ID, testUser2ID := prepareTeamManagementTestEnvironment(t, env)

	var testTeamID int

	// Subtest: Team Creation
	// Verifies that:
	// - A new team can be created via the MCP handler.
	// - The handler response indicates success with an ID.
	// - The created team exists in Portainer when checked directly via the Raw Client.
	t.Run("Team Creation", func(t *testing.T) {
		handler := env.MCPServer.HandleCreateTeam()
		request := mcp.CreateMCPRequest(map[string]any{
			"name": testTeamName,
		})

		result, err := handler(env.Ctx, request)
		require.NoError(t, err, "Failed to create team via MCP handler")

		textContent, ok := result.Content[0].(mcpmodels.TextContent)
		require.True(t, ok, "Expected text content in MCP response")

		// Check for success message and extract ID for later tests
		assert.Contains(t, textContent.Text, "Team created successfully with ID:", "Success message prefix mismatch")

		// Verify by fetching teams directly via client and finding the created team by name
		team, err := env.RawClient.GetTeamByName(testTeamName)
		require.NoError(t, err, "Failed to get team directly via client after creation")
		assert.Equal(t, testTeamName, team.Name, "Team name mismatch")

		// Extract team ID for subsequent tests
		testTeamID = int(team.ID)
	})

	// Subtest: Team Listing
	// Verifies that:
	// - The team list can be retrieved via the MCP handler
	// - The list contains the expected number of teams (one, the test team)
	// - The team has the correct name property
	// - The team data matches the team obtained directly via Raw Client when converted to the same model
	t.Run("Team Listing", func(t *testing.T) {
		handler := env.MCPServer.HandleGetTeams()
		result, err := handler(env.Ctx, mcp.CreateMCPRequest(nil))
		require.NoError(t, err, "Failed to get teams via MCP handler")

		assert.Len(t, result.Content, 1, "Expected exactly one content block in the result")
		textContent, ok := result.Content[0].(mcpmodels.TextContent)
		assert.True(t, ok, "Expected text content in MCP response")

		var retrievedTeams []models.Team
		err = json.Unmarshal([]byte(textContent.Text), &retrievedTeams)
		require.NoError(t, err, "Failed to unmarshal retrieved teams")
		require.Len(t, retrievedTeams, 1, "Expected exactly one team after unmarshalling")

		team := retrievedTeams[0]
		assert.Equal(t, testTeamName, team.Name, "Team name mismatch")

		// Fetch the same team directly via the client
		rawTeam, err := env.RawClient.GetTeam(int64(testTeamID))
		require.NoError(t, err, "Failed to get team directly via client")

		// Convert the raw team to the expected Team model
		rawMemberships, err := env.RawClient.ListTeamMemberships()
		require.NoError(t, err, "Failed to get team memberships directly via client")
		expectedTeam := models.ConvertToTeam(rawTeam, rawMemberships)
		assert.Equal(t, expectedTeam, team, "Team mismatch between MCP handler and direct client call")
	})

	// Subtest: Team Name Update
	// Verifies that:
	// - A team's name can be updated via the MCP handler
	// - The handler response indicates success
	// - The team name is actually updated when checked directly via Raw Client
	t.Run("Team Name Update", func(t *testing.T) {
		handler := env.MCPServer.HandleUpdateTeamName()
		request := mcp.CreateMCPRequest(map[string]any{
			"id":   float64(testTeamID),
			"name": testTeamNewName,
		})

		result, err := handler(env.Ctx, request)
		require.NoError(t, err, "Failed to update team name via MCP handler")

		textContent, ok := result.Content[0].(mcpmodels.TextContent)
		require.True(t, ok, "Expected text content in MCP response for team name update")
		assert.Contains(t, textContent.Text, "Team name updated successfully", "Success message mismatch for team name update")

		// Verify by fetching team directly via raw client
		rawTeam, err := env.RawClient.GetTeam(int64(testTeamID))
		require.NoError(t, err, "Failed to get team directly via client after name update")
		assert.Equal(t, testTeamNewName, rawTeam.Name, "Team name was not updated")
	})

	// Subtest: Team Members Update
	// Verifies that:
	// - Team members can be updated via the MCP handler
	// - The handler response indicates success
	// - The team memberships are correctly updated when checked directly via Raw Client
	// - Both test users are properly assigned to the team
	t.Run("Team Members Update", func(t *testing.T) {
		handler := env.MCPServer.HandleUpdateTeamMembers()
		request := mcp.CreateMCPRequest(map[string]any{
			"id":      float64(testTeamID),
			"userIds": []any{float64(testUser1ID), float64(testUser2ID)},
		})

		result, err := handler(env.Ctx, request)
		require.NoError(t, err, "Failed to update team members via MCP handler")

		textContent, ok := result.Content[0].(mcpmodels.TextContent)
		require.True(t, ok, "Expected text content in MCP response for team members update")
		assert.Contains(t, textContent.Text, "Team members updated successfully", "Success message mismatch for team members update")

		// Verify by fetching team directly via raw client
		rawTeam, err := env.RawClient.GetTeam(int64(testTeamID))
		require.NoError(t, err, "Failed to get team directly via client after member update")
		rawMemberships, err := env.RawClient.ListTeamMemberships()
		require.NoError(t, err, "Failed to get team memberships directly via client")
		expectedTeam := models.ConvertToTeam(rawTeam, rawMemberships)
		assert.ElementsMatch(t, []int{testUser1ID, testUser2ID}, expectedTeam.MemberIDs, "Team memberships mismatch")
	})
}

```

--------------------------------------------------------------------------------
/internal/mcp/mocks_test.go:
--------------------------------------------------------------------------------

```go
package mcp

import (
	"net/http"

	"github.com/portainer/portainer-mcp/pkg/portainer/models"
	"github.com/stretchr/testify/mock"
)

// Mock Implementation Patterns:
//
// This file contains mock implementations of the PortainerClient interface.
// The following patterns are used throughout the mocks:
//
// 1. Methods returning (T, error):
//    - Uses m.Called() to record the method call and get mock behavior
//    - Includes nil check on first return value to avoid type assertion panics
//    - Example:
//      func (m *Mock) Method() (T, error) {
//          args := m.Called()
//          if args.Get(0) == nil {
//              return nil, args.Error(1)
//          }
//          return args.Get(0).(T), args.Error(1)
//      }
//
// 2. Methods returning only error:
//    - Uses m.Called() with any parameters
//    - Returns only the error value
//    - Example:
//      func (m *Mock) Method(param string) error {
//          args := m.Called(param)
//          return args.Error(0)
//      }
//
// Usage in Tests:
//   mock := new(MockPortainerClient)
//   mock.On("MethodName").Return(expectedValue, nil)
//   result, err := mock.MethodName()
//   mock.AssertExpectations(t)

// MockPortainerClient is a mock implementation of the PortainerClient interface
type MockPortainerClient struct {
	mock.Mock
}

// Tag methods

func (m *MockPortainerClient) GetEnvironmentTags() ([]models.EnvironmentTag, error) {
	args := m.Called()
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]models.EnvironmentTag), args.Error(1)
}

func (m *MockPortainerClient) CreateEnvironmentTag(name string) (int, error) {
	args := m.Called(name)
	return args.Int(0), args.Error(1)
}

// Environment methods

func (m *MockPortainerClient) GetEnvironments() ([]models.Environment, error) {
	args := m.Called()
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]models.Environment), args.Error(1)
}

func (m *MockPortainerClient) UpdateEnvironmentTags(id int, tagIds []int) error {
	args := m.Called(id, tagIds)
	return args.Error(0)
}

func (m *MockPortainerClient) UpdateEnvironmentUserAccesses(id int, userAccesses map[int]string) error {
	args := m.Called(id, userAccesses)
	return args.Error(0)
}

func (m *MockPortainerClient) UpdateEnvironmentTeamAccesses(id int, teamAccesses map[int]string) error {
	args := m.Called(id, teamAccesses)
	return args.Error(0)
}

// Environment Group methods

func (m *MockPortainerClient) GetEnvironmentGroups() ([]models.Group, error) {
	args := m.Called()
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]models.Group), args.Error(1)
}

func (m *MockPortainerClient) CreateEnvironmentGroup(name string, environmentIds []int) (int, error) {
	args := m.Called(name, environmentIds)
	return args.Int(0), args.Error(1)
}

func (m *MockPortainerClient) UpdateEnvironmentGroupName(id int, name string) error {
	args := m.Called(id, name)
	return args.Error(0)
}

func (m *MockPortainerClient) UpdateEnvironmentGroupEnvironments(id int, environmentIds []int) error {
	args := m.Called(id, environmentIds)
	return args.Error(0)
}

func (m *MockPortainerClient) UpdateEnvironmentGroupTags(id int, tagIds []int) error {
	args := m.Called(id, tagIds)
	return args.Error(0)
}

// Access Group methods

func (m *MockPortainerClient) GetAccessGroups() ([]models.AccessGroup, error) {
	args := m.Called()
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]models.AccessGroup), args.Error(1)
}

func (m *MockPortainerClient) CreateAccessGroup(name string, environmentIds []int) (int, error) {
	args := m.Called(name, environmentIds)
	return args.Int(0), args.Error(1)
}

func (m *MockPortainerClient) UpdateAccessGroupName(id int, name string) error {
	args := m.Called(id, name)
	return args.Error(0)
}

func (m *MockPortainerClient) UpdateAccessGroupUserAccesses(id int, userAccesses map[int]string) error {
	args := m.Called(id, userAccesses)
	return args.Error(0)
}

func (m *MockPortainerClient) UpdateAccessGroupTeamAccesses(id int, teamAccesses map[int]string) error {
	args := m.Called(id, teamAccesses)
	return args.Error(0)
}

func (m *MockPortainerClient) AddEnvironmentToAccessGroup(id int, environmentId int) error {
	args := m.Called(id, environmentId)
	return args.Error(0)
}

func (m *MockPortainerClient) RemoveEnvironmentFromAccessGroup(id int, environmentId int) error {
	args := m.Called(id, environmentId)
	return args.Error(0)
}

// Stack methods

func (m *MockPortainerClient) GetStacks() ([]models.Stack, error) {
	args := m.Called()
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]models.Stack), args.Error(1)
}

func (m *MockPortainerClient) GetStackFile(id int) (string, error) {
	args := m.Called(id)
	return args.String(0), args.Error(1)
}

func (m *MockPortainerClient) CreateStack(name string, file string, environmentGroupIds []int) (int, error) {
	args := m.Called(name, file, environmentGroupIds)
	return args.Int(0), args.Error(1)
}

func (m *MockPortainerClient) UpdateStack(id int, file string, environmentGroupIds []int) error {
	args := m.Called(id, file, environmentGroupIds)
	return args.Error(0)
}

// Team methods

func (m *MockPortainerClient) CreateTeam(name string) (int, error) {
	args := m.Called(name)
	return args.Int(0), args.Error(1)
}

func (m *MockPortainerClient) GetTeams() ([]models.Team, error) {
	args := m.Called()
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]models.Team), args.Error(1)
}

func (m *MockPortainerClient) UpdateTeamName(id int, name string) error {
	args := m.Called(id, name)
	return args.Error(0)
}

func (m *MockPortainerClient) UpdateTeamMembers(id int, userIds []int) error {
	args := m.Called(id, userIds)
	return args.Error(0)
}

// User methods

func (m *MockPortainerClient) GetUsers() ([]models.User, error) {
	args := m.Called()
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]models.User), args.Error(1)
}

func (m *MockPortainerClient) UpdateUserRole(id int, role string) error {
	args := m.Called(id, role)
	return args.Error(0)
}

// Settings methods

func (m *MockPortainerClient) GetSettings() (models.PortainerSettings, error) {
	args := m.Called()
	if args.Get(0) == nil {
		return models.PortainerSettings{}, args.Error(1)
	}
	return args.Get(0).(models.PortainerSettings), args.Error(1)
}

func (m *MockPortainerClient) GetVersion() (string, error) {
	args := m.Called()
	if args.Get(0) == nil {
		return "", args.Error(1)
	}
	return args.Get(0).(string), args.Error(1)
}

// Docker Proxy methods
func (m *MockPortainerClient) ProxyDockerRequest(opts models.DockerProxyRequestOptions) (*http.Response, error) {
	args := m.Called(opts)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*http.Response), args.Error(1)
}

// Kubernetes Proxy methods
func (m *MockPortainerClient) ProxyKubernetesRequest(opts models.KubernetesProxyRequestOptions) (*http.Response, error) {
	args := m.Called(opts)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*http.Response), args.Error(1)
}

```

--------------------------------------------------------------------------------
/pkg/portainer/client/team_test.go:
--------------------------------------------------------------------------------

```go
package client

import (
	"errors"
	"testing"

	apimodels "github.com/portainer/client-api-go/v2/pkg/models"
	"github.com/portainer/portainer-mcp/pkg/portainer/models"
	"github.com/stretchr/testify/assert"
)

func TestGetTeams(t *testing.T) {
	tests := []struct {
		name            string
		mockTeams       []*apimodels.PortainerTeam
		mockMemberships []*apimodels.PortainerTeamMembership
		mockTeamError   error
		mockMemberError error
		expected        []models.Team
		expectedError   bool
	}{
		{
			name: "successful retrieval",
			mockTeams: []*apimodels.PortainerTeam{
				{
					ID:   1,
					Name: "team1",
				},
				{
					ID:   2,
					Name: "team2",
				},
			},
			mockMemberships: []*apimodels.PortainerTeamMembership{
				{
					ID:     1,
					UserID: 100,
					TeamID: 1,
				},
				{
					ID:     2,
					UserID: 101,
					TeamID: 1,
				},
				{
					ID:     3,
					UserID: 102,
					TeamID: 2,
				},
			},
			expected: []models.Team{
				{
					ID:        1,
					Name:      "team1",
					MemberIDs: []int{100, 101},
				},
				{
					ID:        2,
					Name:      "team2",
					MemberIDs: []int{102},
				},
			},
		},
		{
			name: "teams with empty memberships",
			mockTeams: []*apimodels.PortainerTeam{
				{
					ID:   1,
					Name: "team1",
				},
				{
					ID:   2,
					Name: "team2",
				},
			},
			mockMemberships: []*apimodels.PortainerTeamMembership{},
			expected: []models.Team{
				{
					ID:        1,
					Name:      "team1",
					MemberIDs: []int{},
				},
				{
					ID:        2,
					Name:      "team2",
					MemberIDs: []int{},
				},
			},
		},
		{
			name:            "empty teams",
			mockTeams:       []*apimodels.PortainerTeam{},
			mockMemberships: []*apimodels.PortainerTeamMembership{},
			expected:        []models.Team{},
		},
		{
			name:          "list teams error",
			mockTeamError: errors.New("failed to list teams"),
			expectedError: true,
		},
		{
			name: "list memberships error",
			mockTeams: []*apimodels.PortainerTeam{
				{
					ID:   1,
					Name: "team1",
				},
			},
			mockMemberError: errors.New("failed to list memberships"),
			expectedError:   true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockAPI := new(MockPortainerAPI)
			mockAPI.On("ListTeams").Return(tt.mockTeams, tt.mockTeamError)
			mockAPI.On("ListTeamMemberships").Return(tt.mockMemberships, tt.mockMemberError)

			client := &PortainerClient{cli: mockAPI}

			teams, err := client.GetTeams()

			if tt.expectedError {
				assert.Error(t, err)
				return
			}
			assert.NoError(t, err)
			assert.Equal(t, tt.expected, teams)
			mockAPI.AssertExpectations(t)
		})
	}
}

func TestUpdateTeamName(t *testing.T) {
	tests := []struct {
		name          string
		teamID        int
		teamName      string
		mockError     error
		expectedError bool
	}{
		{
			name:     "successful update",
			teamID:   1,
			teamName: "new-team-name",
		},
		{
			name:          "update error",
			teamID:        2,
			teamName:      "new-team-name",
			mockError:     errors.New("failed to update team name"),
			expectedError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockAPI := new(MockPortainerAPI)
			mockAPI.On("UpdateTeamName", tt.teamID, tt.teamName).Return(tt.mockError)

			client := &PortainerClient{cli: mockAPI}

			err := client.UpdateTeamName(tt.teamID, tt.teamName)

			if tt.expectedError {
				assert.Error(t, err)
				return
			}
			assert.NoError(t, err)
			mockAPI.AssertExpectations(t)
		})
	}
}

func TestCreateTeam(t *testing.T) {
	tests := []struct {
		name          string
		teamName      string
		mockID        int64
		mockError     error
		expected      int
		expectedError bool
	}{
		{
			name:     "successful creation",
			teamName: "new-team",
			mockID:   1,
			expected: 1,
		},
		{
			name:          "create error",
			teamName:      "new-team",
			mockError:     errors.New("failed to create team"),
			expectedError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockAPI := new(MockPortainerAPI)
			mockAPI.On("CreateTeam", tt.teamName).Return(tt.mockID, tt.mockError)

			client := &PortainerClient{cli: mockAPI}

			id, err := client.CreateTeam(tt.teamName)

			if tt.expectedError {
				assert.Error(t, err)
				return
			}
			assert.NoError(t, err)
			assert.Equal(t, tt.expected, id)
			mockAPI.AssertExpectations(t)
		})
	}
}

func TestUpdateTeamMembers(t *testing.T) {
	tests := []struct {
		name            string
		teamID          int
		userIDs         []int
		mockMemberships []*apimodels.PortainerTeamMembership
		mockListError   error
		mockDeleteError error
		mockCreateError error
		expectedError   bool
	}{
		{
			name:    "successful update - add and remove members",
			teamID:  1,
			userIDs: []int{101, 102}, // Want to keep 101 and add 102
			mockMemberships: []*apimodels.PortainerTeamMembership{
				{
					ID:     1,
					UserID: 100, // Should be removed
					TeamID: 1,
				},
				{
					ID:     2,
					UserID: 101, // Should be kept
					TeamID: 1,
				},
			},
		},
		{
			name:    "successful update - no changes needed",
			teamID:  1,
			userIDs: []int{100, 101},
			mockMemberships: []*apimodels.PortainerTeamMembership{
				{
					ID:     1,
					UserID: 100,
					TeamID: 1,
				},
				{
					ID:     2,
					UserID: 101,
					TeamID: 1,
				},
			},
		},
		{
			name:          "list memberships error",
			teamID:        1,
			userIDs:       []int{100},
			mockListError: errors.New("failed to list memberships"),
			expectedError: true,
		},
		{
			name:    "delete membership error",
			teamID:  1,
			userIDs: []int{101}, // Want to remove 100
			mockMemberships: []*apimodels.PortainerTeamMembership{
				{
					ID:     1,
					UserID: 100,
					TeamID: 1,
				},
			},
			mockDeleteError: errors.New("failed to delete membership"),
			expectedError:   true,
		},
		{
			name:            "create membership error",
			teamID:          1,
			userIDs:         []int{100}, // Want to add 100
			mockMemberships: []*apimodels.PortainerTeamMembership{},
			mockCreateError: errors.New("failed to create membership"),
			expectedError:   true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockAPI := new(MockPortainerAPI)
			mockAPI.On("ListTeamMemberships").Return(tt.mockMemberships, tt.mockListError)

			// Set up delete expectations for memberships that should be removed
			for _, membership := range tt.mockMemberships {
				shouldDelete := true
				for _, keepID := range tt.userIDs {
					if int(membership.UserID) == keepID {
						shouldDelete = false
						break
					}
				}
				if shouldDelete {
					mockAPI.On("DeleteTeamMembership", int(membership.ID)).Return(tt.mockDeleteError)
				}
			}

			// Set up create expectations for new members
			for _, userID := range tt.userIDs {
				exists := false
				for _, membership := range tt.mockMemberships {
					if int(membership.UserID) == userID && int(membership.TeamID) == tt.teamID {
						exists = true
						break
					}
				}
				if !exists {
					mockAPI.On("CreateTeamMembership", tt.teamID, userID).Return(tt.mockCreateError)
				}
			}

			client := &PortainerClient{cli: mockAPI}

			err := client.UpdateTeamMembers(tt.teamID, tt.userIDs)

			if tt.expectedError {
				assert.Error(t, err)
				return
			}
			assert.NoError(t, err)
			mockAPI.AssertExpectations(t)
		})
	}
}

```

--------------------------------------------------------------------------------
/tests/integration/environment_test.go:
--------------------------------------------------------------------------------

```go
package integration

import (
	"encoding/json"
	"fmt"
	"testing"

	mcpmodels "github.com/mark3labs/mcp-go/mcp"
	"github.com/portainer/client-api-go/v2/client/utils"
	apimodels "github.com/portainer/client-api-go/v2/pkg/models"
	"github.com/portainer/portainer-mcp/internal/mcp"
	"github.com/portainer/portainer-mcp/pkg/portainer/models"
	"github.com/portainer/portainer-mcp/tests/integration/helpers"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

const (
	// Test data constants
	testEndpointName = "test-endpoint"
	testTag1Name     = "tag1"
	testTag2Name     = "tag2"
)

// prepareTestEnvironment prepares the test environment for the tests
// It enables Edge Compute settings and creates an Edge Docker endpoint
func prepareEnvironmentManagementTestEnvironment(t *testing.T, env *helpers.TestEnv) {
	host, port := env.Portainer.GetHostAndPort()
	serverAddr := fmt.Sprintf("%s:%s", host, port)
	tunnelAddr := fmt.Sprintf("%s:8000", host)

	err := env.RawClient.UpdateSettings(true, serverAddr, tunnelAddr)
	require.NoError(t, err, "Failed to update settings")

	_, err = env.RawClient.CreateEdgeDockerEndpoint(testEndpointName)
	require.NoError(t, err, "Failed to create Edge Docker endpoint")
}

// TestEnvironmentManagement is an integration test suite that verifies the complete
// lifecycle of environment management in Portainer MCP. It tests the retrieval and
// configuration of environments, including tag management, user access controls,
// and team access policies.
func TestEnvironmentManagement(t *testing.T) {
	env := helpers.NewTestEnv(t)
	defer env.Cleanup(t)

	// Prepare the test environment
	prepareEnvironmentManagementTestEnvironment(t, env)

	var environment models.Environment

	// Subtest: Environment Retrieval
	// Verifies that:
	// - The environment is correctly retrieved from the system
	// - The environment has the expected default properties (type, status)
	// - No tags, user accesses, or team accesses are initially assigned
	// - Compares MCP handler output with direct client API call result
	t.Run("Environment Retrieval", func(t *testing.T) {
		handler := env.MCPServer.HandleGetEnvironments()
		result, err := handler(env.Ctx, mcp.CreateMCPRequest(nil))
		require.NoError(t, err, "Failed to get environments via MCP handler")

		assert.Len(t, result.Content, 1, "Expected exactly one environment from MCP handler")
		textContent, ok := result.Content[0].(mcpmodels.TextContent)
		assert.True(t, ok, "Expected text content in MCP response")

		var environments []models.Environment
		err = json.Unmarshal([]byte(textContent.Text), &environments)
		require.NoError(t, err, "Failed to unmarshal environments from MCP response")
		require.Len(t, environments, 1, "Expected exactly one environment after unmarshalling")

		// Extract the environment for subsequent tests
		environment = environments[0]

		// Fetch the same endpoint directly via the client
		rawEndpoint, err := env.RawClient.GetEndpoint(int64(environment.ID))
		require.NoError(t, err, "Failed to get endpoint directly via client")

		// Convert the raw endpoint to the expected Environment model using the package's converter
		expectedEnvironment := models.ConvertEndpointToEnvironment(rawEndpoint)

		// Compare the Environment struct from MCP handler with the one converted from the direct client call
		assert.Equal(t, expectedEnvironment, environment, "Mismatch between MCP handler environment and converted client environment")
	})

	// Subtest: Tag Management
	// Verifies that:
	// - New tags can be created in the system
	// - Multiple tags can be assigned to an environment simultaneously
	// - The environment correctly reflects the assigned tag IDs
	// - The tags are properly persisted in the endpoint configuration
	t.Run("Tag Management", func(t *testing.T) {
		tagId1, err := env.RawClient.CreateTag(testTag1Name)
		require.NoError(t, err, "Failed to create first tag")
		tagId2, err := env.RawClient.CreateTag(testTag2Name)
		require.NoError(t, err, "Failed to create second tag")

		request := mcp.CreateMCPRequest(map[string]any{
			"id":     float64(environment.ID),
			"tagIds": []any{float64(tagId1), float64(tagId2)},
		})

		handler := env.MCPServer.HandleUpdateEnvironmentTags()
		_, err = handler(env.Ctx, request)
		require.NoError(t, err, "Failed to update environment tags via MCP handler")

		// Verify by fetching endpoint directly via client
		rawEndpoint, err := env.RawClient.GetEndpoint(int64(environment.ID))
		require.NoError(t, err, "Failed to get endpoint via client after tag update")
		assert.ElementsMatch(t, []int64{tagId1, tagId2}, rawEndpoint.TagIds, "Tag IDs mismatch (Client check)") // Use ElementsMatch for unordered comparison
	})

	// Subtest: User Access Management
	// Verifies that:
	// - User access policies can be assigned to an environment
	// - Multiple users with different access levels can be configured
	// - Access levels are correctly mapped to appropriate role IDs
	// - The environment's user access policies are properly updated and persisted
	t.Run("User Access Management", func(t *testing.T) {
		request := mcp.CreateMCPRequest(map[string]any{
			"id": float64(environment.ID),
			"userAccesses": []any{
				map[string]any{"id": float64(1), "access": "environment_administrator"},
				map[string]any{"id": float64(2), "access": "standard_user"},
			},
		})

		handler := env.MCPServer.HandleUpdateEnvironmentUserAccesses()
		_, err := handler(env.Ctx, request)
		require.NoError(t, err, "Failed to update environment user accesses via MCP handler")

		// Verify by fetching endpoint directly via client
		rawEndpoint, err := env.RawClient.GetEndpoint(int64(environment.ID))
		require.NoError(t, err, "Failed to get endpoint via client after user access update")

		expectedRawUserAccesses := utils.BuildAccessPolicies[apimodels.PortainerUserAccessPolicies](map[int64]string{
			1: "environment_administrator",
			2: "standard_user",
		})
		assert.Equal(t, expectedRawUserAccesses, rawEndpoint.UserAccessPolicies, "User access policies mismatch (Client check)")
	})

	// Subtest: Team Access Management
	// Verifies that:
	// - Team access policies can be assigned to an environment
	// - Multiple teams with different access levels can be configured
	// - Access levels are correctly mapped to appropriate role IDs
	// - The environment's team access policies are properly updated and persisted
	t.Run("Team Access Management", func(t *testing.T) {
		request := mcp.CreateMCPRequest(map[string]any{
			"id": float64(environment.ID),
			"teamAccesses": []any{
				map[string]any{"id": float64(1), "access": "environment_administrator"},
				map[string]any{"id": float64(2), "access": "standard_user"},
			},
		})

		handler := env.MCPServer.HandleUpdateEnvironmentTeamAccesses()
		_, err := handler(env.Ctx, request)
		require.NoError(t, err, "Failed to update environment team accesses via MCP handler")

		// Verify by fetching endpoint directly via client
		rawEndpoint, err := env.RawClient.GetEndpoint(int64(environment.ID))
		require.NoError(t, err, "Failed to get endpoint via client after team access update")

		expectedRawTeamAccesses := utils.BuildAccessPolicies[apimodels.PortainerTeamAccessPolicies](map[int64]string{
			1: "environment_administrator",
			2: "standard_user",
		})
		assert.Equal(t, expectedRawTeamAccesses, rawEndpoint.TeamAccessPolicies, "Team access policies mismatch (Client check)")
	})
}

```

--------------------------------------------------------------------------------
/tests/integration/docker_test.go:
--------------------------------------------------------------------------------

```go
package integration

import (
	"encoding/json"
	"fmt"
	"testing"

	mcpmodels "github.com/mark3labs/mcp-go/mcp"

	"github.com/portainer/portainer-mcp/internal/mcp"
	"github.com/portainer/portainer-mcp/tests/integration/containers"
	"github.com/portainer/portainer-mcp/tests/integration/helpers"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

const (
	// Test data constants
	testLocalEndpointName = "test-local-endpoint"
	testLocalEndpointID   = 1
	testVolumeName        = "test-proxy-volume"
)

// prepareDockerProxyTestEnvironment prepares the test environment for the tests
// It creates a local Docker endpoint
func prepareDockerProxyTestEnvironment(t *testing.T, env *helpers.TestEnv) {
	_, err := env.RawClient.CreateLocalDockerEndpoint(testLocalEndpointName)
	require.NoError(t, err, "Failed to create Local Docker endpoint")
}

// TestDockerProxy is an integration test suite that verifies the Docker proxy functionality
// provided by the Portainer MCP server. It tests the ability to proxy various Docker API requests
// to a specified environment, including:
// - Retrieving Docker version information (GET /version)
// - Creating a volume (POST /volumes/create)
// - Listing volumes with filters (GET /volumes?filters=...)
// - Removing a volume (DELETE /volumes/{name})
// It primarily tests against volumes, as testing container operations would require
// pulling images beforehand, potentially leading to rate limiting issues in CI/CD
// or rapid testing scenarios.
func TestDockerProxy(t *testing.T) {
	env := helpers.NewTestEnv(t, containers.WithDockerSocketBind(true))
	defer env.Cleanup(t)

	// Prepare the test environment
	prepareDockerProxyTestEnvironment(t, env)

	// Subtest: GET /version
	// Verifies that:
	// - A simple GET request to the Docker /version endpoint can be successfully proxied.
	// - The handler returns a non-empty response without errors.
	// - The response content contains expected fields like ApiVersion and Version.
	t.Run("GET /version", func(t *testing.T) {
		request := mcp.CreateMCPRequest(map[string]any{
			"environmentId": float64(testLocalEndpointID),
			"method":        "GET",
			"dockerAPIPath": "/version",
			"queryParams":   nil, // No query params for /version
			"headers":       nil, // No specific headers needed
			"body":          "",  // No body for GET request
		})

		handler := env.MCPServer.HandleDockerProxy()
		result, err := handler(env.Ctx, request)

		require.NoError(t, err, "Handler execution failed")
		require.NotNil(t, result, "Handler returned nil result")
		require.Len(t, result.Content, 1, "Expected exactly one content item in result")

		textContent, ok := result.Content[0].(mcpmodels.TextContent)
		require.True(t, ok, "Expected text content in result")
		require.NotEmpty(t, textContent.Text, "Result text content should not be empty")

		// Unmarshal and check specific fields
		var versionInfo map[string]any // Using map[string]any for flexibility
		err = json.Unmarshal([]byte(textContent.Text), &versionInfo)
		require.NoError(t, err, "Failed to unmarshal version JSON")
		assert.Contains(t, versionInfo, "ApiVersion", "Version info should contain ApiVersion")
		assert.NotEmpty(t, versionInfo["ApiVersion"], "ApiVersion should not be empty")
		assert.Contains(t, versionInfo, "Version", "Version info should contain Version")
		assert.NotEmpty(t, versionInfo["Version"], "Version should not be empty")
	})

	// Subtest: Create Volume
	// Verifies that:
	// - A POST request to /volumes/create proxies correctly.
	// - A volume with the specified name is created.
	// - The handler response reflects the created volume details.
	t.Run("Create Volume", func(t *testing.T) {
		createBody := fmt.Sprintf(`{"Name": "%s"}`, testVolumeName)
		request := mcp.CreateMCPRequest(map[string]any{
			"environmentId": float64(testLocalEndpointID),
			"method":        "POST",
			"dockerAPIPath": "/volumes/create",
			"headers": []any{
				map[string]any{"key": "Content-Type", "value": "application/json"},
			},
			"body": createBody,
		})

		handler := env.MCPServer.HandleDockerProxy()
		result, err := handler(env.Ctx, request)

		require.NoError(t, err, "Create Volume handler execution failed")
		require.NotNil(t, result, "Create Volume handler returned nil result")
		require.Len(t, result.Content, 1, "Expected one content item for Create Volume")

		textContent, ok := result.Content[0].(mcpmodels.TextContent)
		require.True(t, ok, "Expected text content for Create Volume")
		require.NotEmpty(t, textContent.Text, "Create Volume response text should not be empty")

		var volumeInfo map[string]any
		err = json.Unmarshal([]byte(textContent.Text), &volumeInfo)
		require.NoError(t, err, "Failed to unmarshal Create Volume response")
		assert.Equal(t, testVolumeName, volumeInfo["Name"], "Volume name in response mismatch")
	})

	// Subtest: List Volumes with Filter
	// Verifies that:
	// - A GET request to /volumes with a name filter proxies correctly.
	// - The response contains only the volume created earlier.
	t.Run("List Volumes with Filter", func(t *testing.T) {
		filterJSON := fmt.Sprintf(`{"name":["%s"]}`, testVolumeName)
		request := mcp.CreateMCPRequest(map[string]any{
			"environmentId": float64(testLocalEndpointID),
			"method":        "GET",
			"dockerAPIPath": "/volumes",
			"queryParams": []any{
				map[string]any{"key": "filters", "value": filterJSON},
			},
		})

		handler := env.MCPServer.HandleDockerProxy()
		result, err := handler(env.Ctx, request)

		require.NoError(t, err, "List Volumes handler execution failed")
		require.NotNil(t, result, "List Volumes handler returned nil result")
		require.Len(t, result.Content, 1, "Expected one content item for List Volumes")

		textContent, ok := result.Content[0].(mcpmodels.TextContent)
		require.True(t, ok, "Expected text content for List Volumes")
		require.NotEmpty(t, textContent.Text, "List Volumes response text should not be empty")

		var listResponse map[string][]map[string]any
		err = json.Unmarshal([]byte(textContent.Text), &listResponse)
		require.NoError(t, err, "Failed to unmarshal List Volumes response")
		require.Contains(t, listResponse, "Volumes", "List response missing 'Volumes' key")
		require.Len(t, listResponse["Volumes"], 1, "Expected exactly one volume in the filtered list")
		assert.Equal(t, testVolumeName, listResponse["Volumes"][0]["Name"], "Filtered volume name mismatch")
	})

	// Subtest: Remove Volume
	// Verifies that:
	// - A DELETE request to /volumes/{name} proxies correctly.
	// - The volume created earlier is successfully removed.
	// - The handler response is empty (reflecting Docker's 204 No Content).
	t.Run("Remove Volume", func(t *testing.T) {
		request := mcp.CreateMCPRequest(map[string]any{
			"environmentId": float64(testLocalEndpointID),
			"method":        "DELETE",
			"dockerAPIPath": "/volumes/" + testVolumeName,
		})

		handler := env.MCPServer.HandleDockerProxy()
		result, err := handler(env.Ctx, request)

		require.NoError(t, err, "Remove Volume handler execution failed")
		require.NotNil(t, result, "Remove Volume handler returned nil result")
		require.Len(t, result.Content, 1, "Expected one content item for Remove Volume")

		textContent, ok := result.Content[0].(mcpmodels.TextContent)
		require.True(t, ok, "Expected text content for Remove Volume")
		assert.Empty(t, textContent.Text, "Remove Volume response text should be empty for 204 No Content")
	})
}

```

--------------------------------------------------------------------------------
/internal/mcp/docker_test.go:
--------------------------------------------------------------------------------

```go
package mcp

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net/http"
	"strings"
	"testing"

	"github.com/mark3labs/mcp-go/mcp"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
)

func createMockHttpResponse(statusCode int, body string) *http.Response {
	return &http.Response{
		StatusCode: statusCode,
		Body:       io.NopCloser(strings.NewReader(body)),
	}
}

// errorReader simulates an error during io.ReadAll
type errorReader struct{}

func (r *errorReader) Read(p []byte) (n int, err error) {
	return 0, fmt.Errorf("simulated read error")
}

func (r *errorReader) Close() error {
	return nil
}

func TestHandleDockerProxy_ParameterValidation(t *testing.T) {
	tests := []struct {
		name             string
		inputParams      map[string]any
		expectedErrorMsg string
	}{
		{
			name: "invalid body type (not a string)",
			inputParams: map[string]any{
				"environmentId": float64(2),
				"dockerAPIPath": "/containers/create",
				"method":        "POST",
				"body":          123.45, // Invalid type for body
			},
			expectedErrorMsg: "body must be a string",
		},
		{
			name: "missing environmentId",
			inputParams: map[string]any{
				"dockerAPIPath": "/containers/json",
				"method":        "GET",
			},
			expectedErrorMsg: "environmentId is required",
		},
		{
			name: "missing dockerAPIPath",
			inputParams: map[string]any{
				"environmentId": float64(1),
				"method":        "GET",
			},
			expectedErrorMsg: "dockerAPIPath is required",
		},
		{
			name: "missing method",
			inputParams: map[string]any{
				"environmentId": float64(1),
				"dockerAPIPath": "/containers/json",
			},
			expectedErrorMsg: "method is required",
		},
		{
			name: "invalid dockerAPIPath (no leading slash)",
			inputParams: map[string]any{
				"environmentId": float64(1),
				"dockerAPIPath": "containers/json",
				"method":        "GET",
			},
			expectedErrorMsg: "dockerAPIPath must start with a leading slash",
		},
		{
			name: "invalid HTTP method",
			inputParams: map[string]any{
				"environmentId": float64(1),
				"dockerAPIPath": "/containers/json",
				"method":        "INVALID",
			},
			expectedErrorMsg: "invalid method: INVALID",
		},
		{
			name: "invalid queryParams type (not an array)",
			inputParams: map[string]any{
				"environmentId": float64(1),
				"dockerAPIPath": "/containers/json",
				"method":        "GET",
				"queryParams":   "not-an-array", // Invalid type
			},
			expectedErrorMsg: "queryParams must be an array",
		},
		{
			name: "invalid queryParams content (missing key)",
			inputParams: map[string]any{
				"environmentId": float64(1),
				"dockerAPIPath": "/containers/json",
				"method":        "GET",
				"queryParams":   []any{map[string]any{"value": "true"}}, // Missing 'key'
			},
			expectedErrorMsg: "invalid query params: invalid key: <nil>",
		},
		{
			name: "invalid headers type (not an array)",
			inputParams: map[string]any{
				"environmentId": float64(1),
				"dockerAPIPath": "/containers/json",
				"method":        "GET",
				"headers":       map[string]any{"key": "value"}, // Invalid type
			},
			expectedErrorMsg: "headers must be an array",
		},
		{
			name: "invalid headers content (value not string)",
			inputParams: map[string]any{
				"environmentId": float64(1),
				"dockerAPIPath": "/containers/json",
				"method":        "GET",
				"headers":       []any{map[string]any{"key": "X-Custom", "value": 123}}, // Value not string
			},
			expectedErrorMsg: "invalid headers: invalid value: 123",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			server := &PortainerMCPServer{}

			request := CreateMCPRequest(tt.inputParams)
			handler := server.HandleDockerProxy()
			result, err := handler(context.Background(), request)

			// All parameter/validation errors now return (result{IsError: true}, nil)
			assert.NoError(t, err)   // Handler now returns nil error
			assert.NotNil(t, result) // Handler returns a result object
			assert.True(t, result.IsError, "result.IsError should be true for parameter validation errors")
			assert.Len(t, result.Content, 1)                       // Expect one content item for the error message
			textContent, ok := result.Content[0].(mcp.TextContent) // Content should be TextContent
			assert.True(t, ok, "Result content should be mcp.TextContent for errors")
			assert.Contains(t, textContent.Text, tt.expectedErrorMsg, "Error message mismatch")
		})
	}
}

func TestHandleDockerProxy_ClientInteraction(t *testing.T) {
	type testCase struct {
		name  string
		input map[string]any // Parameters for the MCP request
		mock  struct {       // Details for mocking the client call
			response *http.Response
			err      error
		}
		expect struct { // Expected outcome
			errSubstring string // Check for error containing this text (if error expected)
			resultText   string // Expected text result (if success expected)
		}
	}

	tests := []testCase{
		{
			name: "successful GET request", // Query params are parsed by toolgen, but not yet passed by handler
			input: map[string]any{
				"environmentId": float64(1),
				"dockerAPIPath": "/containers/json",
				"method":        "GET",
				"queryParams": []any{ //
					map[string]any{"key": "all", "value": "true"},
					map[string]any{"key": "filter", "value": "dangling"},
				},
			},
			mock: struct {
				response *http.Response
				err      error
			}{
				response: createMockHttpResponse(http.StatusOK, `[{"Id":"123"}]`),
				err:      nil,
			},
			expect: struct {
				errSubstring string
				resultText   string
			}{
				resultText: `[{"Id":"123"}]`,
			},
		},
		{
			name: "successful POST request with body",
			input: map[string]any{
				"environmentId": float64(2),
				"dockerAPIPath": "/containers/create",
				"method":        "POST",
				"body":          `{"name":"test"}`,
				"headers": []any{
					map[string]any{"key": "X-Custom", "value": "test-value"},
					map[string]any{"key": "Authorization", "value": "Bearer abc"},
				},
			},
			mock: struct {
				response *http.Response
				err      error
			}{
				response: createMockHttpResponse(http.StatusCreated, `{"Id":"456"}`),
				err:      nil,
			},
			expect: struct {
				errSubstring string
				resultText   string
			}{
				resultText: `{"Id":"456"}`,
			},
		},
		{
			name: "client API error",
			input: map[string]any{
				"environmentId": float64(3),
				"dockerAPIPath": "/version",
				"method":        "GET",
			},
			mock: struct {
				response *http.Response
				err      error
			}{
				response: nil,
				err:      errors.New("portainer api error"),
			},
			expect: struct {
				errSubstring string
				resultText   string
			}{
				errSubstring: "failed to send Docker API request: portainer api error",
			},
		},
		{
			name: "error reading response body",
			input: map[string]any{
				"environmentId": float64(4),
				"dockerAPIPath": "/info",
				"method":        "GET",
			},
			mock: struct {
				response *http.Response
				err      error
			}{
				response: &http.Response{
					StatusCode: http.StatusOK,
					Body:       &errorReader{}, // Simulate read error
				},
				err: nil, // No client error, but response body read fails
			},
			expect: struct {
				errSubstring string
				resultText   string
			}{
				errSubstring: "failed to read Docker API response: simulated read error",
			},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			mockClient := new(MockPortainerClient)

			mockClient.On("ProxyDockerRequest", mock.AnythingOfType("models.DockerProxyRequestOptions")).
				Return(tc.mock.response, tc.mock.err)

			server := &PortainerMCPServer{
				cli: mockClient,
			}

			request := CreateMCPRequest(tc.input)
			handler := server.HandleDockerProxy()
			result, err := handler(context.Background(), request)

			if tc.expect.errSubstring != "" {
				assert.NoError(t, err)
				assert.NotNil(t, result)
				assert.True(t, result.IsError, "result.IsError should be true for errors")
				assert.Len(t, result.Content, 1)
				textContent, ok := result.Content[0].(mcp.TextContent)
				assert.True(t, ok, "Result content should be mcp.TextContent for errors")
				assert.Contains(t, textContent.Text, tc.expect.errSubstring)
			} else {
				assert.NoError(t, err)
				assert.NotNil(t, result)
				assert.Len(t, result.Content, 1)
				textContent, ok := result.Content[0].(mcp.TextContent)
				assert.True(t, ok)
				assert.Equal(t, tc.expect.resultText, textContent.Text)
			}

			mockClient.AssertExpectations(t)
		})
	}
}

```

--------------------------------------------------------------------------------
/tests/integration/group_test.go:
--------------------------------------------------------------------------------

```go
package integration

import (
	"encoding/json"
	"fmt"
	"testing"

	mcpmodels "github.com/mark3labs/mcp-go/mcp"
	"github.com/portainer/portainer-mcp/internal/mcp"
	"github.com/portainer/portainer-mcp/pkg/portainer/models"
	"github.com/portainer/portainer-mcp/tests/integration/helpers"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

const (
	testGroupName        = "test-mcp-group"
	testGroupUpdatedName = "test-mcp-group-updated"
	testGroupTagName1    = "test-group-tag1"
	testGroupTagName2    = "test-group-tag2"
	testEnvName          = "test-group-env"
)

// prepareEnvironmentGroupTestEnvironment prepares the test environment for environment group tests
func prepareEnvironmentGroupTestEnvironment(t *testing.T, env *helpers.TestEnv) (int, int) {
	// Enable Edge features in Portainer
	host, port := env.Portainer.GetHostAndPort()
	serverAddr := fmt.Sprintf("%s:%s", host, port)
	tunnelAddr := fmt.Sprintf("%s:8000", host)

	err := env.RawClient.UpdateSettings(true, serverAddr, tunnelAddr)
	require.NoError(t, err, "Failed to update settings to enable Edge features")

	// Create a test environment for association with groups
	envID, err := env.RawClient.CreateEdgeDockerEndpoint(testEnvName)
	require.NoError(t, err, "Failed to create test environment")

	// Create test tag
	tagID, err := env.RawClient.CreateTag(testGroupTagName1)
	require.NoError(t, err, "Failed to create test tag")

	return int(envID), int(tagID)
}

// TestEnvironmentGroupManagement is an integration test suite that verifies the complete
// lifecycle of environment group management in Portainer MCP. It tests group creation,
// listing, name updates, environment association and tag association.
func TestEnvironmentGroupManagement(t *testing.T) {
	env := helpers.NewTestEnv(t)
	defer env.Cleanup(t)

	// Prepare the test environment
	testEnvID, testTagID := prepareEnvironmentGroupTestEnvironment(t, env)

	var testGroupID int

	// Subtest: Environment Group Creation
	// Verifies that:
	// - A new environment group can be created via the MCP handler
	// - The handler response indicates success with an ID
	// - The created group exists in Portainer when checked directly via Raw Client
	t.Run("Environment Group Creation", func(t *testing.T) {
		handler := env.MCPServer.HandleCreateEnvironmentGroup()
		request := mcp.CreateMCPRequest(map[string]any{
			"name":           testGroupName,
			"environmentIds": []any{float64(testEnvID)},
		})

		result, err := handler(env.Ctx, request)
		require.NoError(t, err, "Failed to create environment group via MCP handler")

		textContent, ok := result.Content[0].(mcpmodels.TextContent)
		require.True(t, ok, "Expected text content in MCP response")

		// Check for success message
		assert.Contains(t, textContent.Text, "Environment group created successfully with ID:", "Success message prefix mismatch")

		// Verify by fetching group directly via client and finding the created group by name
		group, err := env.RawClient.GetEdgeGroupByName(testGroupName)
		require.NoError(t, err, "Failed to get environment group directly via client")
		assert.Equal(t, testGroupName, group.Name, "Group name mismatch")

		// Extract group ID for subsequent tests
		testGroupID = int(group.ID)
	})

	// Subtest: Environment Group Listing
	// Verifies that:
	// - The group list can be retrieved via the MCP handler
	// - The list contains the expected group
	// - The group data matches the expected properties
	t.Run("Environment Group Listing", func(t *testing.T) {
		handler := env.MCPServer.HandleGetEnvironmentGroups()
		result, err := handler(env.Ctx, mcp.CreateMCPRequest(nil))
		require.NoError(t, err, "Failed to get environment groups via MCP handler")

		assert.Len(t, result.Content, 1, "Expected exactly one content block in the result")
		textContent, ok := result.Content[0].(mcpmodels.TextContent)
		assert.True(t, ok, "Expected text content in MCP response")

		var retrievedGroups []models.Group
		err = json.Unmarshal([]byte(textContent.Text), &retrievedGroups)
		require.NoError(t, err, "Failed to unmarshal retrieved groups")
		require.Len(t, retrievedGroups, 1, "Expected exactly one group after unmarshalling")

		group := retrievedGroups[0]
		assert.Equal(t, testGroupName, group.Name, "Group name mismatch")

		// Fetch the same group directly via the client
		rawGroup, err := env.RawClient.GetEdgeGroup(int64(testGroupID))
		require.NoError(t, err, "Failed to get environment group directly via client")

		// Convert the raw group to the expected Group model
		expectedGroup := models.ConvertEdgeGroupToGroup(rawGroup)
		assert.Equal(t, expectedGroup, group, "Group mismatch between MCP handler and direct client call")
	})

	// Subtest: Environment Group Name Update
	// Verifies that:
	// - The group name can be updated via the MCP handler
	// - The handler response indicates success
	// - The name is correctly updated when checked directly via Raw Client
	t.Run("Environment Group Name Update", func(t *testing.T) {
		handler := env.MCPServer.HandleUpdateEnvironmentGroupName()
		request := mcp.CreateMCPRequest(map[string]any{
			"id":   float64(testGroupID),
			"name": testGroupUpdatedName,
		})

		result, err := handler(env.Ctx, request)
		require.NoError(t, err, "Failed to update environment group name via MCP handler")

		textContent, ok := result.Content[0].(mcpmodels.TextContent)
		require.True(t, ok, "Expected text content in MCP response")
		assert.Contains(t, textContent.Text, "Environment group name updated successfully", "Success message mismatch")

		// Verify by fetching group directly via raw client
		updatedGroup, err := env.RawClient.GetEdgeGroup(int64(testGroupID))
		require.NoError(t, err, "Failed to get environment group directly via client")
		assert.Equal(t, testGroupUpdatedName, updatedGroup.Name, "Group name was not updated")
	})

	// Subtest: Environment Group Tag Update
	// Verifies that:
	// - Tags can be associated with a group via the MCP handler
	// - The handler response indicates success
	// - The tags are correctly associated when checked directly via Raw Client
	t.Run("Environment Group Tag Update", func(t *testing.T) {
		// Create a second tag
		tagID2, err := env.RawClient.CreateTag(testGroupTagName2)
		require.NoError(t, err, "Failed to create second test tag")

		handler := env.MCPServer.HandleUpdateEnvironmentGroupTags()
		request := mcp.CreateMCPRequest(map[string]any{
			"id":     float64(testGroupID),
			"tagIds": []any{float64(testTagID), float64(tagID2)},
		})

		result, err := handler(env.Ctx, request)
		require.NoError(t, err, "Failed to update environment group tags via MCP handler")

		textContent, ok := result.Content[0].(mcpmodels.TextContent)
		require.True(t, ok, "Expected text content in MCP response")
		assert.Contains(t, textContent.Text, "Environment group tags updated successfully", "Success message mismatch")

		// Verify by fetching group directly via raw client
		updatedGroup, err := env.RawClient.GetEdgeGroup(int64(testGroupID))
		require.NoError(t, err, "Failed to get environment group directly via client")
		assert.ElementsMatch(t, []int64{int64(testTagID), int64(tagID2)}, updatedGroup.TagIds, "Tag IDs mismatch")
	})

	// Subtest: Environment Group Environments Update
	// Verifies that:
	// - Environment associations can be updated via the MCP handler
	// - The handler response indicates success
	// - The environment associations are correctly updated when checked directly via Raw Client
	t.Run("Environment Group Environments Update", func(t *testing.T) {
		// Create a second environment
		env2Name := "test-env-2"
		env2ID, err := env.RawClient.CreateEdgeDockerEndpoint(env2Name)
		require.NoError(t, err, "Failed to create second test environment")

		handler := env.MCPServer.HandleUpdateEnvironmentGroupEnvironments()
		request := mcp.CreateMCPRequest(map[string]any{
			"id":             float64(testGroupID),
			"environmentIds": []any{float64(testEnvID), float64(env2ID)},
		})

		result, err := handler(env.Ctx, request)
		require.NoError(t, err, "Failed to update environment group environments via MCP handler")

		textContent, ok := result.Content[0].(mcpmodels.TextContent)
		require.True(t, ok, "Expected text content in MCP response")
		assert.Contains(t, textContent.Text, "Environment group environments updated successfully", "Success message mismatch")

		// Verify by fetching group directly via raw client
		updatedGroup, err := env.RawClient.GetEdgeGroup(int64(testGroupID))
		require.NoError(t, err, "Failed to get environment group directly via client")
		assert.ElementsMatch(t, []int64{int64(testEnvID), int64(env2ID)}, updatedGroup.Endpoints, "Environment IDs mismatch")
	})
}

```

--------------------------------------------------------------------------------
/pkg/portainer/client/mocks_test.go:
--------------------------------------------------------------------------------

```go
package client

import (
	"net/http"

	"github.com/portainer/client-api-go/v2/client"
	apimodels "github.com/portainer/client-api-go/v2/pkg/models"
	"github.com/stretchr/testify/mock"
)

// Mock Implementation Patterns:
//
// This file contains mock implementations of the PortainerAPIClient interface.
// The following patterns are used throughout the mocks:
//
// 1. Methods returning (T, error):
//    - Uses m.Called() to record the method call and get mock behavior
//    - Includes nil check on first return value to avoid type assertion panics
//    - Example:
//      func (m *Mock) Method() (T, error) {
//          args := m.Called()
//          if args.Get(0) == nil {
//              return nil, args.Error(1)
//          }
//          return args.Get(0).(T), args.Error(1)
//      }
//
// 2. Methods returning only error:
//    - Uses m.Called() with any parameters
//    - Returns only the error value
//    - Example:
//      func (m *Mock) Method(param string) error {
//          args := m.Called(param)
//          return args.Error(0)
//      }
//
// 3. Methods with primitive return types:
//    - Uses type-specific getters (e.g., Int64, String)
//    - Example:
//      func (m *Mock) Method() (int64, error) {
//          args := m.Called()
//          return args.Get(0).(int64), args.Error(1)
//      }
//
// Usage in Tests:
//   mock := new(MockPortainerAPI)
//   mock.On("MethodName").Return(expectedValue, nil)
//   result, err := mock.MethodName()
//   mock.AssertExpectations(t)

// MockPortainerAPI is a mock of the PortainerAPIClient interface
type MockPortainerAPI struct {
	mock.Mock
}

// ListEdgeGroups mocks the ListEdgeGroups method
func (m *MockPortainerAPI) ListEdgeGroups() ([]*apimodels.EdgegroupsDecoratedEdgeGroup, error) {
	args := m.Called()
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]*apimodels.EdgegroupsDecoratedEdgeGroup), args.Error(1)
}

// CreateEdgeGroup mocks the CreateEdgeGroup method
func (m *MockPortainerAPI) CreateEdgeGroup(name string, environmentIds []int64) (int64, error) {
	args := m.Called(name, environmentIds)
	return args.Get(0).(int64), args.Error(1)
}

// UpdateEdgeGroup mocks the UpdateEdgeGroup method
func (m *MockPortainerAPI) UpdateEdgeGroup(id int64, name *string, environmentIds *[]int64, tagIds *[]int64) error {
	args := m.Called(id, name, environmentIds, tagIds)
	return args.Error(0)
}

// ListEdgeStacks mocks the ListEdgeStacks method
func (m *MockPortainerAPI) ListEdgeStacks() ([]*apimodels.PortainereeEdgeStack, error) {
	args := m.Called()
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]*apimodels.PortainereeEdgeStack), args.Error(1)
}

// CreateEdgeStack mocks the CreateEdgeStack method
func (m *MockPortainerAPI) CreateEdgeStack(name string, file string, environmentGroupIds []int64) (int64, error) {
	args := m.Called(name, file, environmentGroupIds)
	return args.Get(0).(int64), args.Error(1)
}

// UpdateEdgeStack mocks the UpdateEdgeStack method
func (m *MockPortainerAPI) UpdateEdgeStack(id int64, file string, environmentGroupIds []int64) error {
	args := m.Called(id, file, environmentGroupIds)
	return args.Error(0)
}

// GetEdgeStackFile mocks the GetEdgeStackFile method
func (m *MockPortainerAPI) GetEdgeStackFile(id int64) (string, error) {
	args := m.Called(id)
	return args.String(0), args.Error(1)
}

// ListEndpointGroups mocks the ListEndpointGroups method
func (m *MockPortainerAPI) ListEndpointGroups() ([]*apimodels.PortainerEndpointGroup, error) {
	args := m.Called()
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]*apimodels.PortainerEndpointGroup), args.Error(1)
}

// CreateEndpointGroup mocks the CreateEndpointGroup method
func (m *MockPortainerAPI) CreateEndpointGroup(name string, associatedEndpoints []int64) (int64, error) {
	args := m.Called(name, associatedEndpoints)
	return args.Get(0).(int64), args.Error(1)
}

// UpdateEndpointGroup mocks the UpdateEndpointGroup method
func (m *MockPortainerAPI) UpdateEndpointGroup(id int64, name *string, userAccesses *map[int64]string, teamAccesses *map[int64]string) error {
	args := m.Called(id, name, userAccesses, teamAccesses)
	return args.Error(0)
}

// AddEnvironmentToEndpointGroup mocks the AddEnvironmentToEndpointGroup method
func (m *MockPortainerAPI) AddEnvironmentToEndpointGroup(groupId int64, environmentId int64) error {
	args := m.Called(groupId, environmentId)
	return args.Error(0)
}

// RemoveEnvironmentFromEndpointGroup mocks the RemoveEnvironmentFromEndpointGroup method
func (m *MockPortainerAPI) RemoveEnvironmentFromEndpointGroup(groupId int64, environmentId int64) error {
	args := m.Called(groupId, environmentId)
	return args.Error(0)
}

// ListEndpoints mocks the ListEndpoints method
func (m *MockPortainerAPI) ListEndpoints() ([]*apimodels.PortainereeEndpoint, error) {
	args := m.Called()
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]*apimodels.PortainereeEndpoint), args.Error(1)
}

// GetEndpoint mocks the GetEndpoint method
func (m *MockPortainerAPI) GetEndpoint(id int64) (*apimodels.PortainereeEndpoint, error) {
	args := m.Called(id)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*apimodels.PortainereeEndpoint), args.Error(1)
}

// UpdateEndpoint mocks the UpdateEndpoint method
func (m *MockPortainerAPI) UpdateEndpoint(id int64, tagIds *[]int64, userAccesses *map[int64]string, teamAccesses *map[int64]string) error {
	args := m.Called(id, tagIds, userAccesses, teamAccesses)
	return args.Error(0)
}

// GetSettings mocks the GetSettings method
func (m *MockPortainerAPI) GetSettings() (*apimodels.PortainereeSettings, error) {
	args := m.Called()
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*apimodels.PortainereeSettings), args.Error(1)
}

// ListTags mocks the ListTags method
func (m *MockPortainerAPI) ListTags() ([]*apimodels.PortainerTag, error) {
	args := m.Called()
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]*apimodels.PortainerTag), args.Error(1)
}

// CreateTag mocks the CreateTag method
func (m *MockPortainerAPI) CreateTag(name string) (int64, error) {
	args := m.Called(name)
	return args.Get(0).(int64), args.Error(1)
}

// ListTeams mocks the ListTeams method
func (m *MockPortainerAPI) ListTeams() ([]*apimodels.PortainerTeam, error) {
	args := m.Called()
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]*apimodels.PortainerTeam), args.Error(1)
}

// ListTeamMemberships mocks the ListTeamMemberships method
func (m *MockPortainerAPI) ListTeamMemberships() ([]*apimodels.PortainerTeamMembership, error) {
	args := m.Called()
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]*apimodels.PortainerTeamMembership), args.Error(1)
}

// CreateTeam mocks the CreateTeam method
func (m *MockPortainerAPI) CreateTeam(name string) (int64, error) {
	args := m.Called(name)
	return args.Get(0).(int64), args.Error(1)
}

// UpdateTeamName mocks the UpdateTeamName method
func (m *MockPortainerAPI) UpdateTeamName(id int, name string) error {
	args := m.Called(id, name)
	return args.Error(0)
}

// DeleteTeamMembership mocks the DeleteTeamMembership method
func (m *MockPortainerAPI) DeleteTeamMembership(id int) error {
	args := m.Called(id)
	return args.Error(0)
}

// CreateTeamMembership mocks the CreateTeamMembership method
func (m *MockPortainerAPI) CreateTeamMembership(teamId int, userId int) error {
	args := m.Called(teamId, userId)
	return args.Error(0)
}

// ListUsers mocks the ListUsers method
func (m *MockPortainerAPI) ListUsers() ([]*apimodels.PortainereeUser, error) {
	args := m.Called()
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]*apimodels.PortainereeUser), args.Error(1)
}

// UpdateUserRole mocks the UpdateUserRole method
func (m *MockPortainerAPI) UpdateUserRole(id int, role int64) error {
	args := m.Called(id, role)
	return args.Error(0)
}

// GetVersion mocks the GetVersion method
func (m *MockPortainerAPI) GetVersion() (string, error) {
	args := m.Called()
	return args.String(0), args.Error(1)
}

// ProxyDockerRequest mocks the ProxyDockerRequest method
func (m *MockPortainerAPI) ProxyDockerRequest(environmentId int, opts client.ProxyRequestOptions) (*http.Response, error) {
	args := m.Called(environmentId, opts)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*http.Response), args.Error(1)
}

// ProxyKubernetesRequest mocks the ProxyKubernetesRequest method
func (m *MockPortainerAPI) ProxyKubernetesRequest(environmentId int, opts client.ProxyRequestOptions) (*http.Response, error) {
	args := m.Called(environmentId, opts)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*http.Response), args.Error(1)
}

```

--------------------------------------------------------------------------------
/internal/mcp/team_test.go:
--------------------------------------------------------------------------------

```go
package mcp

import (
	"context"
	"encoding/json"
	"fmt"
	"testing"

	"github.com/mark3labs/mcp-go/mcp"
	"github.com/portainer/portainer-mcp/pkg/portainer/models"
	"github.com/stretchr/testify/assert"
)

func TestHandleCreateTeam(t *testing.T) {
	tests := []struct {
		name        string
		teamName    string
		mockID      int
		mockError   error
		expectError bool
		setupParams func(request *mcp.CallToolRequest)
	}{
		{
			name:        "successful team creation",
			teamName:    "test-team",
			mockID:      1,
			mockError:   nil,
			expectError: false,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"name": "test-team",
				}
			},
		},
		{
			name:        "api error",
			teamName:    "test-team",
			mockID:      0,
			mockError:   fmt.Errorf("api error"),
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"name": "test-team",
				}
			},
		},
		{
			name:        "missing name parameter",
			teamName:    "",
			mockID:      0,
			mockError:   nil,
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				// No need to set any parameters as the request will be invalid
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockClient := &MockPortainerClient{}
			if !tt.expectError || tt.mockError != nil {
				mockClient.On("CreateTeam", tt.teamName).Return(tt.mockID, tt.mockError)
			}

			server := &PortainerMCPServer{
				cli: mockClient,
			}

			request := CreateMCPRequest(map[string]any{})
			tt.setupParams(&request)

			handler := server.HandleCreateTeam()
			result, err := handler(context.Background(), request)

			if tt.expectError {
				assert.NoError(t, err)
				assert.NotNil(t, result)
				assert.True(t, result.IsError, "result.IsError should be true for expected errors")
				assert.Len(t, result.Content, 1)
				textContent, ok := result.Content[0].(mcp.TextContent)
				assert.True(t, ok, "Result content should be mcp.TextContent for errors")
				if tt.mockError != nil {
					assert.Contains(t, textContent.Text, tt.mockError.Error())
				} else {
					assert.NotEmpty(t, textContent.Text, "Error message should not be empty for parameter errors")
				}
			} else {
				assert.NoError(t, err)
				assert.Len(t, result.Content, 1)
				textContent, ok := result.Content[0].(mcp.TextContent)
				assert.True(t, ok)
				assert.Contains(t, textContent.Text, fmt.Sprintf("ID: %d", tt.mockID))
			}

			mockClient.AssertExpectations(t)
		})
	}
}

func TestHandleGetTeams(t *testing.T) {
	tests := []struct {
		name        string
		mockTeams   []models.Team
		mockError   error
		expectError bool
	}{
		{
			name: "successful teams retrieval",
			mockTeams: []models.Team{
				{ID: 1, Name: "team1"},
				{ID: 2, Name: "team2"},
			},
			mockError:   nil,
			expectError: false,
		},
		{
			name:        "api error",
			mockTeams:   nil,
			mockError:   fmt.Errorf("api error"),
			expectError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockClient := &MockPortainerClient{}
			mockClient.On("GetTeams").Return(tt.mockTeams, tt.mockError)

			server := &PortainerMCPServer{
				cli: mockClient,
			}

			handler := server.HandleGetTeams()
			result, err := handler(context.Background(), mcp.CallToolRequest{})

			if tt.expectError {
				assert.NoError(t, err)
				assert.NotNil(t, result)
				assert.True(t, result.IsError, "result.IsError should be true for expected errors")
				assert.Len(t, result.Content, 1)
				textContent, ok := result.Content[0].(mcp.TextContent)
				assert.True(t, ok, "Result content should be mcp.TextContent for errors")
				if tt.mockError != nil {
					assert.Contains(t, textContent.Text, tt.mockError.Error())
				} else {
					assert.NotEmpty(t, textContent.Text, "Error message should not be empty for parameter errors")
				}
			} else {
				assert.NoError(t, err)
				assert.Len(t, result.Content, 1)
				textContent, ok := result.Content[0].(mcp.TextContent)
				assert.True(t, ok)

				var teams []models.Team
				err = json.Unmarshal([]byte(textContent.Text), &teams)
				assert.NoError(t, err)
				assert.Equal(t, tt.mockTeams, teams)
			}

			mockClient.AssertExpectations(t)
		})
	}
}

func TestHandleUpdateTeamName(t *testing.T) {
	tests := []struct {
		name        string
		inputID     int
		inputName   string
		mockError   error
		expectError bool
		setupParams func(request *mcp.CallToolRequest)
	}{
		{
			name:        "successful name update",
			inputID:     1,
			inputName:   "new-name",
			mockError:   nil,
			expectError: false,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id":   float64(1),
					"name": "new-name",
				}
			},
		},
		{
			name:        "api error",
			inputID:     1,
			inputName:   "new-name",
			mockError:   fmt.Errorf("api error"),
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id":   float64(1),
					"name": "new-name",
				}
			},
		},
		{
			name:        "missing id parameter",
			inputID:     0,
			inputName:   "new-name",
			mockError:   nil,
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"name": "new-name",
				}
			},
		},
		{
			name:        "missing name parameter",
			inputID:     1,
			inputName:   "",
			mockError:   nil,
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id": float64(1),
				}
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockClient := &MockPortainerClient{}
			if !tt.expectError || tt.mockError != nil {
				mockClient.On("UpdateTeamName", tt.inputID, tt.inputName).Return(tt.mockError)
			}

			server := &PortainerMCPServer{
				cli: mockClient,
			}

			request := CreateMCPRequest(map[string]any{})
			tt.setupParams(&request)

			handler := server.HandleUpdateTeamName()
			result, err := handler(context.Background(), request)

			if tt.expectError {
				assert.NoError(t, err)
				assert.NotNil(t, result)
				assert.True(t, result.IsError, "result.IsError should be true for expected errors")
				assert.Len(t, result.Content, 1)
				textContent, ok := result.Content[0].(mcp.TextContent)
				assert.True(t, ok, "Result content should be mcp.TextContent for errors")
				if tt.mockError != nil {
					assert.Contains(t, textContent.Text, tt.mockError.Error())
				} else {
					assert.NotEmpty(t, textContent.Text, "Error message should not be empty for parameter errors")
				}
			} else {
				assert.NoError(t, err)
				assert.Len(t, result.Content, 1)
				textContent, ok := result.Content[0].(mcp.TextContent)
				assert.True(t, ok)
				assert.Contains(t, textContent.Text, "successfully")
			}

			mockClient.AssertExpectations(t)
		})
	}
}

func TestHandleUpdateTeamMembers(t *testing.T) {
	tests := []struct {
		name        string
		inputID     int
		inputUsers  []int
		mockError   error
		expectError bool
		setupParams func(request *mcp.CallToolRequest)
	}{
		{
			name:        "successful members update",
			inputID:     1,
			inputUsers:  []int{1, 2, 3},
			mockError:   nil,
			expectError: false,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id":      float64(1),
					"userIds": []any{float64(1), float64(2), float64(3)},
				}
			},
		},
		{
			name:        "api error",
			inputID:     1,
			inputUsers:  []int{1, 2, 3},
			mockError:   fmt.Errorf("api error"),
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id":      float64(1),
					"userIds": []any{float64(1), float64(2), float64(3)},
				}
			},
		},
		{
			name:        "missing id parameter",
			inputID:     0,
			inputUsers:  []int{1, 2, 3},
			mockError:   nil,
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"userIds": []any{float64(1), float64(2), float64(3)},
				}
			},
		},
		{
			name:        "missing userIds parameter",
			inputID:     1,
			inputUsers:  nil,
			mockError:   nil,
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id": float64(1),
				}
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockClient := &MockPortainerClient{}
			if !tt.expectError || tt.mockError != nil {
				mockClient.On("UpdateTeamMembers", tt.inputID, tt.inputUsers).Return(tt.mockError)
			}

			server := &PortainerMCPServer{
				cli: mockClient,
			}

			request := CreateMCPRequest(map[string]any{})
			tt.setupParams(&request)

			handler := server.HandleUpdateTeamMembers()
			result, err := handler(context.Background(), request)

			if tt.expectError {
				assert.NoError(t, err)
				assert.NotNil(t, result)
				assert.True(t, result.IsError, "result.IsError should be true for expected errors")
				assert.Len(t, result.Content, 1)
				textContent, ok := result.Content[0].(mcp.TextContent)
				assert.True(t, ok, "Result content should be mcp.TextContent for errors")
				if tt.mockError != nil {
					assert.Contains(t, textContent.Text, tt.mockError.Error())
				} else {
					assert.NotEmpty(t, textContent.Text, "Error message should not be empty for parameter errors")
				}
			} else {
				assert.NoError(t, err)
				assert.Len(t, result.Content, 1)
				textContent, ok := result.Content[0].(mcp.TextContent)
				assert.True(t, ok)
				assert.Contains(t, textContent.Text, "successfully")
			}

			mockClient.AssertExpectations(t)
		})
	}
}

```

--------------------------------------------------------------------------------
/pkg/portainer/client/access_group_test.go:
--------------------------------------------------------------------------------

```go
package client

import (
	"errors"
	"testing"

	apimodels "github.com/portainer/client-api-go/v2/pkg/models"
	"github.com/portainer/portainer-mcp/pkg/portainer/models"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
)

func TestGetAccessGroups(t *testing.T) {
	tests := []struct {
		name                  string
		mockEndpointGroups    []*apimodels.PortainerEndpointGroup
		mockEndpoints         []*apimodels.PortainereeEndpoint
		mockEndpointGroupsErr error
		mockEndpointsErr      error
		expected              []models.AccessGroup
		expectedError         bool
	}{
		{
			name: "successful retrieval",
			mockEndpointGroups: []*apimodels.PortainerEndpointGroup{
				{
					ID:   1,
					Name: "group1",
					UserAccessPolicies: apimodels.PortainerUserAccessPolicies{
						"1": apimodels.PortainerAccessPolicy{RoleID: 1}, // environment_administrator
						"2": apimodels.PortainerAccessPolicy{RoleID: 2}, // helpdesk_user
						"3": apimodels.PortainerAccessPolicy{RoleID: 3}, // standard_user
						"4": apimodels.PortainerAccessPolicy{RoleID: 4}, // readonly_user
						"5": apimodels.PortainerAccessPolicy{RoleID: 5}, // operator_user
					},
					TeamAccessPolicies: apimodels.PortainerTeamAccessPolicies{
						"6":  apimodels.PortainerAccessPolicy{RoleID: 1}, // environment_administrator
						"7":  apimodels.PortainerAccessPolicy{RoleID: 2}, // helpdesk_user
						"8":  apimodels.PortainerAccessPolicy{RoleID: 3}, // standard_user
						"9":  apimodels.PortainerAccessPolicy{RoleID: 4}, // readonly_user
						"10": apimodels.PortainerAccessPolicy{RoleID: 5}, // operator_user
					},
				},
			},
			mockEndpoints: []*apimodels.PortainereeEndpoint{
				{ID: 1, Name: "endpoint1", GroupID: 1},
				{ID: 2, Name: "endpoint2", GroupID: 1},
				{ID: 3, Name: "endpoint3", GroupID: 2},
			},
			expected: []models.AccessGroup{
				{
					ID:             1,
					Name:           "group1",
					EnvironmentIds: []int{1, 2},
					UserAccesses: map[int]string{
						1: "environment_administrator",
						2: "helpdesk_user",
						3: "standard_user",
						4: "readonly_user",
						5: "operator_user",
					},
					TeamAccesses: map[int]string{
						6:  "environment_administrator",
						7:  "helpdesk_user",
						8:  "standard_user",
						9:  "readonly_user",
						10: "operator_user",
					},
				},
			},
		},
		{
			name:                  "endpoint group list error",
			mockEndpointGroupsErr: errors.New("failed to list groups"),
			expectedError:         true,
		},
		{
			name: "endpoint list error",
			mockEndpointGroups: []*apimodels.PortainerEndpointGroup{
				{ID: 1, Name: "group1"},
			},
			mockEndpointsErr: errors.New("failed to list endpoints"),
			expectedError:    true,
		},
		{
			name:               "empty groups with endpoints",
			mockEndpointGroups: []*apimodels.PortainerEndpointGroup{},
			mockEndpoints: []*apimodels.PortainereeEndpoint{
				{ID: 1, Name: "endpoint1", GroupID: 1},
				{ID: 2, Name: "endpoint2", GroupID: 2},
			},
			expected: []models.AccessGroup{},
		},
		{
			name: "groups with empty endpoints",
			mockEndpointGroups: []*apimodels.PortainerEndpointGroup{
				{
					ID:   1,
					Name: "group1",
					UserAccessPolicies: apimodels.PortainerUserAccessPolicies{
						"1": apimodels.PortainerAccessPolicy{RoleID: 1},
					},
				},
			},
			mockEndpoints: []*apimodels.PortainereeEndpoint{},
			expected: []models.AccessGroup{
				{
					ID:             1,
					Name:           "group1",
					EnvironmentIds: []int{},
					UserAccesses: map[int]string{
						1: "environment_administrator",
					},
					TeamAccesses: map[int]string{},
				},
			},
		},
		{
			name:               "both empty",
			mockEndpointGroups: []*apimodels.PortainerEndpointGroup{},
			mockEndpoints:      []*apimodels.PortainereeEndpoint{},
			expected:           []models.AccessGroup{},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockAPI := new(MockPortainerAPI)
			mockAPI.On("ListEndpointGroups").Return(tt.mockEndpointGroups, tt.mockEndpointGroupsErr)
			mockAPI.On("ListEndpoints").Return(tt.mockEndpoints, tt.mockEndpointsErr)

			client := &PortainerClient{cli: mockAPI}

			groups, err := client.GetAccessGroups()

			if tt.expectedError {
				assert.Error(t, err)
				return
			}
			assert.NoError(t, err)
			assert.Equal(t, tt.expected, groups)
			mockAPI.AssertExpectations(t)
		})
	}
}

func TestCreateAccessGroup(t *testing.T) {
	tests := []struct {
		name          string
		groupName     string
		envIDs        []int
		mockReturnID  int64
		mockError     error
		expected      int
		expectedError bool
	}{
		{
			name:         "successful creation",
			groupName:    "newgroup",
			envIDs:       []int{1, 2, 3},
			mockReturnID: 1,
			expected:     1,
		},
		{
			name:          "creation error",
			groupName:     "newgroup",
			envIDs:        []int{1},
			mockError:     errors.New("failed to create group"),
			expectedError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockAPI := new(MockPortainerAPI)
			mockAPI.On("CreateEndpointGroup", tt.groupName, mock.Anything).Return(tt.mockReturnID, tt.mockError)

			client := &PortainerClient{cli: mockAPI}

			id, err := client.CreateAccessGroup(tt.groupName, tt.envIDs)

			if tt.expectedError {
				assert.Error(t, err)
				return
			}
			assert.NoError(t, err)
			assert.Equal(t, tt.expected, id)
			mockAPI.AssertExpectations(t)
		})
	}
}

func TestUpdateAccessGroupName(t *testing.T) {
	tests := []struct {
		name          string
		groupID       int
		newName       string
		mockError     error
		expectedError bool
	}{
		{
			name:    "successful update",
			groupID: 1,
			newName: "updated-group",
		},
		{
			name:          "update error",
			groupID:       1,
			newName:       "updated-group",
			mockError:     errors.New("failed to update group"),
			expectedError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockAPI := new(MockPortainerAPI)
			mockAPI.On("UpdateEndpointGroup", int64(tt.groupID), &tt.newName, mock.Anything, mock.Anything).Return(tt.mockError)

			client := &PortainerClient{cli: mockAPI}

			err := client.UpdateAccessGroupName(tt.groupID, tt.newName)

			if tt.expectedError {
				assert.Error(t, err)
				return
			}
			assert.NoError(t, err)
			mockAPI.AssertExpectations(t)
		})
	}
}

func TestUpdateAccessGroupUserAccesses(t *testing.T) {
	tests := []struct {
		name          string
		groupID       int
		userAccesses  map[int]string
		mockError     error
		expectedError bool
	}{
		{
			name:    "successful update",
			groupID: 1,
			userAccesses: map[int]string{
				1: "environment_administrator",
				2: "readonly_user",
			},
		},
		{
			name:    "update error",
			groupID: 1,
			userAccesses: map[int]string{
				1: "environment_administrator",
			},
			mockError:     errors.New("failed to update user accesses"),
			expectedError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockAPI := new(MockPortainerAPI)
			mockAPI.On("UpdateEndpointGroup", int64(tt.groupID), mock.Anything, mock.Anything, mock.Anything).Return(tt.mockError)

			client := &PortainerClient{cli: mockAPI}

			err := client.UpdateAccessGroupUserAccesses(tt.groupID, tt.userAccesses)

			if tt.expectedError {
				assert.Error(t, err)
				return
			}
			assert.NoError(t, err)
			mockAPI.AssertExpectations(t)
		})
	}
}

func TestUpdateAccessGroupTeamAccesses(t *testing.T) {
	tests := []struct {
		name          string
		groupID       int
		teamAccesses  map[int]string
		mockError     error
		expectedError bool
	}{
		{
			name:    "successful update",
			groupID: 1,
			teamAccesses: map[int]string{
				1: "environment_administrator",
				2: "readonly_user",
			},
		},
		{
			name:    "update error",
			groupID: 1,
			teamAccesses: map[int]string{
				1: "environment_administrator",
			},
			mockError:     errors.New("failed to update team accesses"),
			expectedError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockAPI := new(MockPortainerAPI)
			mockAPI.On("UpdateEndpointGroup", int64(tt.groupID), mock.Anything, mock.Anything, mock.Anything).Return(tt.mockError)

			client := &PortainerClient{cli: mockAPI}

			err := client.UpdateAccessGroupTeamAccesses(tt.groupID, tt.teamAccesses)

			if tt.expectedError {
				assert.Error(t, err)
				return
			}
			assert.NoError(t, err)
			mockAPI.AssertExpectations(t)
		})
	}
}

func TestAddEnvironmentToAccessGroup(t *testing.T) {
	tests := []struct {
		name          string
		groupID       int
		envID         int
		mockError     error
		expectedError bool
	}{
		{
			name:    "successful addition",
			groupID: 1,
			envID:   2,
		},
		{
			name:          "addition error",
			groupID:       1,
			envID:         2,
			mockError:     errors.New("failed to add environment"),
			expectedError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockAPI := new(MockPortainerAPI)
			mockAPI.On("AddEnvironmentToEndpointGroup", int64(tt.groupID), int64(tt.envID)).Return(tt.mockError)

			client := &PortainerClient{cli: mockAPI}

			err := client.AddEnvironmentToAccessGroup(tt.groupID, tt.envID)

			if tt.expectedError {
				assert.Error(t, err)
				return
			}
			assert.NoError(t, err)
			mockAPI.AssertExpectations(t)
		})
	}
}

func TestRemoveEnvironmentFromAccessGroup(t *testing.T) {
	tests := []struct {
		name          string
		groupID       int
		envID         int
		mockError     error
		expectedError bool
	}{
		{
			name:    "successful removal",
			groupID: 1,
			envID:   2,
		},
		{
			name:          "removal error",
			groupID:       1,
			envID:         2,
			mockError:     errors.New("failed to remove environment"),
			expectedError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockAPI := new(MockPortainerAPI)
			mockAPI.On("RemoveEnvironmentFromEndpointGroup", int64(tt.groupID), int64(tt.envID)).Return(tt.mockError)

			client := &PortainerClient{cli: mockAPI}

			err := client.RemoveEnvironmentFromAccessGroup(tt.groupID, tt.envID)

			if tt.expectedError {
				assert.Error(t, err)
				return
			}
			assert.NoError(t, err)
			mockAPI.AssertExpectations(t)
		})
	}
}

```
Page 2/4FirstPrevNextLast