#
tokens: 40272/50000 9/115 files (page 3/4)
lines: off (toggle) GitHub
raw markdown copy
This is page 3 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

--------------------------------------------------------------------------------
/pkg/toolgen/param_test.go:
--------------------------------------------------------------------------------

```go
package toolgen

import (
	"reflect"
	"testing"

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

// Helper function to create a ParameterParser with given arguments
func newTestParser(args map[string]any) *ParameterParser {
	return NewParameterParser(mcp.CallToolRequest{
		Params: mcp.CallToolParams{
			Arguments: args,
		},
	})
}

func TestGetString(t *testing.T) {
	tests := []struct {
		name     string
		args     map[string]any
		param    string
		required bool
		want     string
		wantErr  bool
	}{
		{
			name:     "valid string",
			args:     map[string]any{"name": "test"},
			param:    "name",
			required: true,
			want:     "test",
			wantErr:  false,
		},
		{
			name:     "missing required param",
			args:     map[string]any{},
			param:    "name",
			required: true,
			want:     "",
			wantErr:  true,
		},
		{
			name:     "missing optional param",
			args:     map[string]any{},
			param:    "name",
			required: false,
			want:     "",
			wantErr:  false,
		},
		{
			name:     "wrong type",
			args:     map[string]any{"name": 123},
			param:    "name",
			required: true,
			want:     "",
			wantErr:  true,
		},
		{
			name:     "nil value",
			args:     map[string]any{"name": nil},
			param:    "name",
			required: true,
			want:     "",
			wantErr:  true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			p := newTestParser(tt.args)
			got, err := p.GetString(tt.param, tt.required)
			if (err != nil) != tt.wantErr {
				t.Errorf("GetString() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if got != tt.want {
				t.Errorf("GetString() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestGetNumber(t *testing.T) {
	tests := []struct {
		name     string
		args     map[string]any
		param    string
		required bool
		want     float64
		wantErr  bool
	}{
		{
			name:     "valid number",
			args:     map[string]any{"num": float64(42)},
			param:    "num",
			required: true,
			want:     42,
			wantErr:  false,
		},
		{
			name:     "missing required param",
			args:     map[string]any{},
			param:    "num",
			required: true,
			want:     0,
			wantErr:  true,
		},
		{
			name:     "missing optional param",
			args:     map[string]any{},
			param:    "num",
			required: false,
			want:     0,
			wantErr:  false,
		},
		{
			name:     "wrong type",
			args:     map[string]any{"num": "123"},
			param:    "num",
			required: true,
			want:     0,
			wantErr:  true,
		},
		{
			name:     "nil value",
			args:     map[string]any{"num": nil},
			param:    "num",
			required: true,
			want:     0,
			wantErr:  true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			p := newTestParser(tt.args)
			got, err := p.GetNumber(tt.param, tt.required)
			if (err != nil) != tt.wantErr {
				t.Errorf("GetNumber() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if got != tt.want {
				t.Errorf("GetNumber() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestGetBoolean(t *testing.T) {
	tests := []struct {
		name     string
		args     map[string]any
		param    string
		required bool
		want     bool
		wantErr  bool
	}{
		{
			name:     "valid true",
			args:     map[string]any{"flag": true},
			param:    "flag",
			required: true,
			want:     true,
			wantErr:  false,
		},
		{
			name:     "valid false",
			args:     map[string]any{"flag": false},
			param:    "flag",
			required: true,
			want:     false,
			wantErr:  false,
		},
		{
			name:     "missing required param",
			args:     map[string]any{},
			param:    "flag",
			required: true,
			want:     false,
			wantErr:  true,
		},
		{
			name:     "missing optional param",
			args:     map[string]any{},
			param:    "flag",
			required: false,
			want:     false,
			wantErr:  false,
		},
		{
			name:     "wrong type",
			args:     map[string]any{"flag": "true"},
			param:    "flag",
			required: true,
			want:     false,
			wantErr:  true,
		},
		{
			name:     "nil value",
			args:     map[string]any{"flag": nil},
			param:    "flag",
			required: true,
			want:     false,
			wantErr:  true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			p := newTestParser(tt.args)
			got, err := p.GetBoolean(tt.param, tt.required)
			if (err != nil) != tt.wantErr {
				t.Errorf("GetBoolean() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if got != tt.want {
				t.Errorf("GetBoolean() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestGetArrayOfObjects(t *testing.T) {
	tests := []struct {
		name     string
		args     map[string]any
		param    string
		required bool
		want     []any
		wantErr  bool
	}{
		{
			name: "valid array of objects",
			args: map[string]any{"objects": []any{
				map[string]any{"id": 1},
				map[string]any{"id": 2},
			}},
			param:    "objects",
			required: true,
			want: []any{
				map[string]any{"id": 1},
				map[string]any{"id": 2},
			},
			wantErr: false,
		},
		{
			name:     "missing required param",
			args:     map[string]any{},
			param:    "objects",
			required: true,
			want:     nil,
			wantErr:  true,
		},
		{
			name:     "missing optional param",
			args:     map[string]any{},
			param:    "objects",
			required: false,
			want:     []any{},
			wantErr:  false,
		},
		{
			name:     "wrong type",
			args:     map[string]any{"objects": "not an array"},
			param:    "objects",
			required: true,
			want:     nil,
			wantErr:  true,
		},
		{
			name:     "nil value",
			args:     map[string]any{"objects": nil},
			param:    "objects",
			required: true,
			want:     nil,
			wantErr:  true,
		},
	}

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

func TestParseArrayOfIntegers(t *testing.T) {
	tests := []struct {
		name    string
		input   []any
		want    []int
		wantErr bool
	}{
		{
			name:    "empty array",
			input:   []any{},
			want:    []int{},
			wantErr: false,
		},
		{
			name:    "single value",
			input:   []any{float64(42)},
			want:    []int{42},
			wantErr: false,
		},
		{
			name:    "multiple values",
			input:   []any{float64(1), float64(2), float64(3), float64(4), float64(5)},
			want:    []int{1, 2, 3, 4, 5},
			wantErr: false,
		},
		{
			name:    "negative values",
			input:   []any{float64(-1), float64(-2), float64(-3)},
			want:    []int{-1, -2, -3},
			wantErr: false,
		},
		{
			name:    "mixed positive and negative values",
			input:   []any{float64(0), float64(1), float64(-2), float64(3), float64(-4)},
			want:    []int{0, 1, -2, 3, -4},
			wantErr: false,
		},
		{
			name:    "invalid string value",
			input:   []any{float64(1), "abc", float64(3)},
			want:    nil,
			wantErr: true,
		},
		{
			name:    "invalid boolean value",
			input:   []any{float64(1), true, float64(3)},
			want:    nil,
			wantErr: true,
		},
		{
			name:    "invalid nil value",
			input:   []any{float64(1), nil, float64(3)},
			want:    nil,
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := parseArrayOfIntegers(tt.input)

			// Check error status
			if (err != nil) != tt.wantErr {
				t.Errorf("ParseNumericArray() error = %v, wantErr %v", err, tt.wantErr)
				return
			}

			// If we expect an error, no need to check the result
			if tt.wantErr {
				return
			}

			// Check result values
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("ParseNumericArray() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestGetInt(t *testing.T) {
	tests := []struct {
		name     string
		args     map[string]any
		param    string
		required bool
		want     int
		wantErr  bool
	}{
		{
			name:     "valid integer",
			args:     map[string]any{"num": float64(42)},
			param:    "num",
			required: true,
			want:     42,
			wantErr:  false,
		},
		{
			name:     "valid zero",
			args:     map[string]any{"num": float64(0)},
			param:    "num",
			required: true,
			want:     0,
			wantErr:  false,
		},
		{
			name:     "valid negative",
			args:     map[string]any{"num": float64(-42)},
			param:    "num",
			required: true,
			want:     -42,
			wantErr:  false,
		},
		{
			name:     "missing required param",
			args:     map[string]any{},
			param:    "num",
			required: true,
			want:     0,
			wantErr:  true,
		},
		{
			name:     "missing optional param",
			args:     map[string]any{},
			param:    "num",
			required: false,
			want:     0,
			wantErr:  false,
		},
		{
			name:     "wrong type string",
			args:     map[string]any{"num": "123"},
			param:    "num",
			required: true,
			want:     0,
			wantErr:  true,
		},
		{
			name:     "wrong type boolean",
			args:     map[string]any{"num": true},
			param:    "num",
			required: true,
			want:     0,
			wantErr:  true,
		},
		{
			name:     "nil value",
			args:     map[string]any{"num": nil},
			param:    "num",
			required: true,
			want:     0,
			wantErr:  true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			p := newTestParser(tt.args)
			got, err := p.GetInt(tt.param, tt.required)
			if (err != nil) != tt.wantErr {
				t.Errorf("GetInt() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if got != tt.want {
				t.Errorf("GetInt() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestGetArrayOfIntegers(t *testing.T) {
	tests := []struct {
		name     string
		args     map[string]any
		param    string
		required bool
		want     []int
		wantErr  bool
	}{
		{
			name: "valid array of integers",
			args: map[string]any{"nums": []any{
				float64(1), float64(2), float64(3),
			}},
			param:    "nums",
			required: true,
			want:     []int{1, 2, 3},
			wantErr:  false,
		},
		{
			name: "valid array with negative numbers",
			args: map[string]any{"nums": []any{
				float64(-1), float64(0), float64(1),
			}},
			param:    "nums",
			required: true,
			want:     []int{-1, 0, 1},
			wantErr:  false,
		},
		{
			name:     "empty array",
			args:     map[string]any{"nums": []any{}},
			param:    "nums",
			required: true,
			want:     []int{},
			wantErr:  false,
		},
		{
			name:     "missing required param",
			args:     map[string]any{},
			param:    "nums",
			required: true,
			want:     nil,
			wantErr:  true,
		},
		{
			name:     "missing optional param",
			args:     map[string]any{},
			param:    "nums",
			required: false,
			want:     []int{},
			wantErr:  false,
		},
		{
			name: "invalid array with string",
			args: map[string]any{"nums": []any{
				float64(1), "2", float64(3),
			}},
			param:    "nums",
			required: true,
			want:     nil,
			wantErr:  true,
		},
		{
			name: "invalid array with boolean",
			args: map[string]any{"nums": []any{
				float64(1), true, float64(3),
			}},
			param:    "nums",
			required: true,
			want:     nil,
			wantErr:  true,
		},
		{
			name: "invalid array with nil",
			args: map[string]any{"nums": []any{
				float64(1), nil, float64(3),
			}},
			param:    "nums",
			required: true,
			want:     nil,
			wantErr:  true,
		},
		{
			name:     "wrong type (string instead of array)",
			args:     map[string]any{"nums": "not an array"},
			param:    "nums",
			required: true,
			want:     nil,
			wantErr:  true,
		},
		{
			name:     "nil value",
			args:     map[string]any{"nums": nil},
			param:    "nums",
			required: true,
			want:     nil,
			wantErr:  true,
		},
	}

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

```

--------------------------------------------------------------------------------
/internal/mcp/environment_test.go:
--------------------------------------------------------------------------------

```go
package mcp

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

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

func TestHandleGetEnvironments(t *testing.T) {
	tests := []struct {
		name             string
		mockEnvironments []models.Environment
		mockError        error
		expectError      bool
	}{
		{
			name: "successful environments retrieval",
			mockEnvironments: []models.Environment{
				{ID: 1, Name: "env1"},
				{ID: 2, Name: "env2"},
			},
			mockError:   nil,
			expectError: false,
		},
		{
			name:             "api error",
			mockEnvironments: nil,
			mockError:        fmt.Errorf("api error"),
			expectError:      true,
		},
	}

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

			server := &PortainerMCPServer{
				cli: mockClient,
			}

			handler := server.HandleGetEnvironments()
			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 environments []models.Environment
				err = json.Unmarshal([]byte(textContent.Text), &environments)
				assert.NoError(t, err)
				assert.Equal(t, tt.mockEnvironments, environments)
			}

			mockClient.AssertExpectations(t)
		})
	}
}

func TestHandleUpdateEnvironmentTags(t *testing.T) {
	tests := []struct {
		name        string
		inputID     int
		inputTagIDs []int
		mockError   error
		expectError bool
		setupParams func(request *mcp.CallToolRequest)
	}{
		{
			name:        "successful tags update",
			inputID:     1,
			inputTagIDs: []int{1, 2, 3},
			mockError:   nil,
			expectError: false,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id":     float64(1),
					"tagIds": []any{float64(1), float64(2), float64(3)},
				}
			},
		},
		{
			name:        "api error",
			inputID:     1,
			inputTagIDs: []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),
					"tagIds": []any{float64(1), float64(2), float64(3)},
				}
			},
		},
		{
			name:        "missing id parameter",
			inputID:     0,
			inputTagIDs: []int{1, 2, 3},
			mockError:   nil,
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"tagIds": []any{float64(1), float64(2), float64(3)},
				}
			},
		},
		{
			name:        "missing tagIds parameter",
			inputID:     1,
			inputTagIDs: 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("UpdateEnvironmentTags", tt.inputID, tt.inputTagIDs).Return(tt.mockError)
			}

			server := &PortainerMCPServer{
				cli: mockClient,
			}

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

			handler := server.HandleUpdateEnvironmentTags()
			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 TestHandleUpdateEnvironmentUserAccesses(t *testing.T) {
	tests := []struct {
		name          string
		inputID       int
		inputAccesses map[int]string
		mockError     error
		expectError   bool
		setupParams   func(request *mcp.CallToolRequest)
	}{
		{
			name:    "successful user accesses update",
			inputID: 1,
			inputAccesses: map[int]string{
				1: "environment_administrator",
				2: "standard_user",
			},
			mockError:   nil,
			expectError: false,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id": float64(1),
					"userAccesses": []any{
						map[string]any{"id": float64(1), "access": "environment_administrator"},
						map[string]any{"id": float64(2), "access": "standard_user"},
					},
				}
			},
		},
		{
			name:    "api error",
			inputID: 1,
			inputAccesses: map[int]string{
				1: "environment_administrator",
			},
			mockError:   fmt.Errorf("api error"),
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id": float64(1),
					"userAccesses": []any{
						map[string]any{"id": float64(1), "access": "environment_administrator"},
					},
				}
			},
		},
		{
			name:        "missing id parameter",
			inputID:     0,
			mockError:   nil,
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"userAccesses": []any{
						map[string]any{"id": float64(1), "access": "environment_administrator"},
					},
				}
			},
		},
		{
			name:        "missing userAccesses parameter",
			inputID:     1,
			mockError:   nil,
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id": float64(1),
				}
			},
		},
		{
			name:    "invalid access level",
			inputID: 1,
			inputAccesses: map[int]string{
				1: "invalid_access",
			},
			mockError:   nil,
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id": float64(1),
					"userAccesses": []any{
						map[string]any{"id": float64(1), "access": "invalid_access"},
					},
				}
			},
		},
	}

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

			server := &PortainerMCPServer{
				cli: mockClient,
			}

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

			handler := server.HandleUpdateEnvironmentUserAccesses()
			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/validation errors")
					if strings.Contains(tt.name, "invalid access level") {
						assert.Contains(t, textContent.Text, "invalid user accesses")
					}
				}
			} 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 TestHandleUpdateEnvironmentTeamAccesses(t *testing.T) {
	tests := []struct {
		name          string
		inputID       int
		inputAccesses map[int]string
		mockError     error
		expectError   bool
		setupParams   func(request *mcp.CallToolRequest)
	}{
		{
			name:    "successful team accesses update",
			inputID: 1,
			inputAccesses: map[int]string{
				1: "environment_administrator",
				2: "standard_user",
			},
			mockError:   nil,
			expectError: false,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id": float64(1),
					"teamAccesses": []any{
						map[string]any{"id": float64(1), "access": "environment_administrator"},
						map[string]any{"id": float64(2), "access": "standard_user"},
					},
				}
			},
		},
		{
			name:    "api error",
			inputID: 1,
			inputAccesses: map[int]string{
				1: "environment_administrator",
			},
			mockError:   fmt.Errorf("api error"),
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id": float64(1),
					"teamAccesses": []any{
						map[string]any{"id": float64(1), "access": "environment_administrator"},
					},
				}
			},
		},
		{
			name:        "missing id parameter",
			inputID:     0,
			mockError:   nil,
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"teamAccesses": []any{
						map[string]any{"id": float64(1), "access": "environment_administrator"},
					},
				}
			},
		},
		{
			name:        "missing teamAccesses parameter",
			inputID:     1,
			mockError:   nil,
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id": float64(1),
				}
			},
		},
		{
			name:    "invalid access level",
			inputID: 1,
			inputAccesses: map[int]string{
				1: "invalid_access",
			},
			mockError:   nil,
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id": float64(1),
					"teamAccesses": []any{
						map[string]any{"id": float64(1), "access": "invalid_access"},
					},
				}
			},
		},
	}

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

			server := &PortainerMCPServer{
				cli: mockClient,
			}

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

			handler := server.HandleUpdateEnvironmentTeamAccesses()
			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/validation errors")
					if strings.Contains(tt.name, "invalid access level") {
						assert.Contains(t, textContent.Text, "invalid team accesses")
					}
				}
			} 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)
		})
	}
}

```

--------------------------------------------------------------------------------
/internal/mcp/stack_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 TestHandleGetStacks(t *testing.T) {
	tests := []struct {
		name        string
		mockStacks  []models.Stack
		mockError   error
		expectError bool
	}{
		{
			name: "successful stacks retrieval",
			mockStacks: []models.Stack{
				{ID: 1, Name: "stack1"},
				{ID: 2, Name: "stack2"},
			},
			mockError:   nil,
			expectError: false,
		},
		{
			name:        "api error",
			mockStacks:  nil,
			mockError:   fmt.Errorf("api error"),
			expectError: true,
		},
	}

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

			server := &PortainerMCPServer{
				cli: mockClient,
			}

			handler := server.HandleGetStacks()
			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 stacks []models.Stack
				err = json.Unmarshal([]byte(textContent.Text), &stacks)
				assert.NoError(t, err)
				assert.Equal(t, tt.mockStacks, stacks)
			}

			mockClient.AssertExpectations(t)
		})
	}
}

func TestHandleGetStackFile(t *testing.T) {
	tests := []struct {
		name        string
		inputID     int
		mockContent string
		mockError   error
		expectError bool
		setupParams func(request *mcp.CallToolRequest)
	}{
		{
			name:        "successful file retrieval",
			inputID:     1,
			mockContent: "version: '3'\nservices:\n  web:\n    image: nginx",
			mockError:   nil,
			expectError: false,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id": float64(1),
				}
			},
		},
		{
			name:        "api error",
			inputID:     1,
			mockContent: "",
			mockError:   fmt.Errorf("api error"),
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id": float64(1),
				}
			},
		},
		{
			name:        "missing id parameter",
			inputID:     0,
			mockContent: "",
			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("GetStackFile", tt.inputID).Return(tt.mockContent, tt.mockError)
			}

			server := &PortainerMCPServer{
				cli: mockClient,
			}

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

			handler := server.HandleGetStackFile()
			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.Equal(t, tt.mockContent, textContent.Text)
			}

			mockClient.AssertExpectations(t)
		})
	}
}

func TestHandleCreateStack(t *testing.T) {
	tests := []struct {
		name             string
		inputName        string
		inputFile        string
		inputEnvGroupIDs []int
		mockID           int
		mockError        error
		expectError      bool
		setupParams      func(request *mcp.CallToolRequest)
	}{
		{
			name:             "successful stack creation",
			inputName:        "test-stack",
			inputFile:        "version: '3'\nservices:\n  web:\n    image: nginx",
			inputEnvGroupIDs: []int{1, 2},
			mockID:           1,
			mockError:        nil,
			expectError:      false,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"name":                "test-stack",
					"file":                "version: '3'\nservices:\n  web:\n    image: nginx",
					"environmentGroupIds": []any{float64(1), float64(2)},
				}
			},
		},
		{
			name:             "api error",
			inputName:        "test-stack",
			inputFile:        "version: '3'\nservices:\n  web:\n    image: nginx",
			inputEnvGroupIDs: []int{1, 2},
			mockID:           0,
			mockError:        fmt.Errorf("api error"),
			expectError:      true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"name":                "test-stack",
					"file":                "version: '3'\nservices:\n  web:\n    image: nginx",
					"environmentGroupIds": []any{float64(1), float64(2)},
				}
			},
		},
		{
			name:             "missing name parameter",
			inputName:        "",
			inputFile:        "version: '3'\nservices:\n  web:\n    image: nginx",
			inputEnvGroupIDs: []int{1, 2},
			mockID:           0,
			mockError:        nil,
			expectError:      true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"file":                "version: '3'\nservices:\n  web:\n    image: nginx",
					"environmentGroupIds": []any{float64(1), float64(2)},
				}
			},
		},
		{
			name:             "missing file parameter",
			inputName:        "test-stack",
			inputFile:        "",
			inputEnvGroupIDs: []int{1, 2},
			mockID:           0,
			mockError:        nil,
			expectError:      true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"name":                "test-stack",
					"environmentGroupIds": []any{float64(1), float64(2)},
				}
			},
		},
		{
			name:             "missing environmentGroupIds parameter",
			inputName:        "test-stack",
			inputFile:        "version: '3'\nservices:\n  web:\n    image: nginx",
			inputEnvGroupIDs: nil,
			mockID:           0,
			mockError:        nil,
			expectError:      true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"name": "test-stack",
					"file": "version: '3'\nservices:\n  web:\n    image: nginx",
				}
			},
		},
	}

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

			server := &PortainerMCPServer{
				cli: mockClient,
			}

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

			handler := server.HandleCreateStack()
			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 TestHandleUpdateStack(t *testing.T) {
	tests := []struct {
		name             string
		inputID          int
		inputFile        string
		inputEnvGroupIDs []int
		mockError        error
		expectError      bool
		setupParams      func(request *mcp.CallToolRequest)
	}{
		{
			name:             "successful stack update",
			inputID:          1,
			inputFile:        "version: '3'\nservices:\n  web:\n    image: nginx",
			inputEnvGroupIDs: []int{1, 2},
			mockError:        nil,
			expectError:      false,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id":                  float64(1),
					"file":                "version: '3'\nservices:\n  web:\n    image: nginx",
					"environmentGroupIds": []any{float64(1), float64(2)},
				}
			},
		},
		{
			name:             "api error",
			inputID:          1,
			inputFile:        "version: '3'\nservices:\n  web:\n    image: nginx",
			inputEnvGroupIDs: []int{1, 2},
			mockError:        fmt.Errorf("api error"),
			expectError:      true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id":                  float64(1),
					"file":                "version: '3'\nservices:\n  web:\n    image: nginx",
					"environmentGroupIds": []any{float64(1), float64(2)},
				}
			},
		},
		{
			name:             "missing id parameter",
			inputID:          0,
			inputFile:        "version: '3'\nservices:\n  web:\n    image: nginx",
			inputEnvGroupIDs: []int{1, 2},
			mockError:        nil,
			expectError:      true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"file":                "version: '3'\nservices:\n  web:\n    image: nginx",
					"environmentGroupIds": []any{float64(1), float64(2)},
				}
			},
		},
		{
			name:             "missing file parameter",
			inputID:          1,
			inputFile:        "",
			inputEnvGroupIDs: []int{1, 2},
			mockError:        nil,
			expectError:      true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id":                  float64(1),
					"environmentGroupIds": []any{float64(1), float64(2)},
				}
			},
		},
		{
			name:             "missing environmentGroupIds parameter",
			inputID:          1,
			inputFile:        "version: '3'\nservices:\n  web:\n    image: nginx",
			inputEnvGroupIDs: nil,
			mockError:        nil,
			expectError:      true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id":   float64(1),
					"file": "version: '3'\nservices:\n  web:\n    image: nginx",
				}
			},
		},
	}

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

			server := &PortainerMCPServer{
				cli: mockClient,
			}

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

			handler := server.HandleUpdateStack()
			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)
		})
	}
}

```

--------------------------------------------------------------------------------
/tests/integration/access_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 (
	testAccessGroupName      = "test-access-group"
	testAccessGroupNewName   = "test-access-group-updated"
	testTeamAccessGroupName  = "test-team-for-access-group"
	testUserAccessGroupName  = "test-user-for-access-group"
	testAccGroupPassword     = "testpassword"
	accGroupUserRoleStandard = 2 // Portainer API role ID for Standard User
	accGroupEndpointName     = "test-endpoint-for-access-group"
)

// prepareAccessGroupTestEnvironment creates test resources needed for access group tests
// including users, teams, and environments
func prepareAccessGroupTestEnvironment(t *testing.T, env *helpers.TestEnv) (int, int, int) {
	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")

	// Create a test user
	testUserID, err := env.RawClient.CreateUser(testUserAccessGroupName, testAccGroupPassword, accGroupUserRoleStandard)
	require.NoError(t, err, "Failed to create test user via raw client")

	// Create a test team
	testTeamID, err := env.RawClient.CreateTeam(testTeamAccessGroupName)
	require.NoError(t, err, "Failed to create test team via raw client")

	// Create a test environment
	testEnvID, err := env.RawClient.CreateEdgeDockerEndpoint(accGroupEndpointName)
	require.NoError(t, err, "Failed to create test environment via raw client")

	return int(testUserID), int(testTeamID), int(testEnvID)
}

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

	// Prepare the test environment
	testUserID, testTeamID, testEnvID := prepareAccessGroupTestEnvironment(t, env)

	var testAccessGroupID int

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

		result, err := handler(env.Ctx, request)
		require.NoError(t, err, "Failed to create access 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 and extract ID for later tests
		assert.Contains(t, textContent.Text, "Access group created successfully with ID:", "Success message prefix mismatch")

		// Verify by fetching access group directly via raw client
		rawAccessGroup, err := env.RawClient.GetEndpointGroupByName(testAccessGroupName)
		require.NoError(t, err, "Failed to get access group directly via raw client")
		assert.Equal(t, testAccessGroupName, rawAccessGroup.Name, "Access group name mismatch")

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

	// Subtest: Access Groups Listing
	// Verifies that:
	// - The access group list can be retrieved via the HandleGetAccessGroups handler
	// - The list contains the expected access group
	// - The access group has the correct name and properties
	t.Run("Access Groups Listing", func(t *testing.T) {
		handler := env.MCPServer.HandleGetAccessGroups()
		result, err := handler(env.Ctx, mcp.CreateMCPRequest(nil))
		require.NoError(t, err, "Failed to get access 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 retrievedAccessGroups []models.AccessGroup
		err = json.Unmarshal([]byte(textContent.Text), &retrievedAccessGroups)
		require.NoError(t, err, "Failed to unmarshal retrieved access groups")
		require.Len(t, retrievedAccessGroups, 2, "Expected exactly two access groups after unmarshalling")

		accessGroup := retrievedAccessGroups[1]
		assert.Equal(t, testAccessGroupName, accessGroup.Name, "Access group name mismatch")

		// Fetch the same access group directly via the client
		rawAccessGroup, err := env.RawClient.GetEndpointGroup(int64(testAccessGroupID))
		require.NoError(t, err, "Failed to get access group directly via client")

		// Convert the raw access group to the expected AccessGroup model
		rawEndpoints, err := env.RawClient.ListEndpoints()
		require.NoError(t, err, "Failed to list endpoints")

		expectedAccessGroup := models.ConvertEndpointGroupToAccessGroup(rawAccessGroup, rawEndpoints)
		assert.Equal(t, expectedAccessGroup, accessGroup, "Access group mismatch between MCP handler and direct client call")
	})

	// Subtest: Access Group Name Update
	// Verifies that:
	// - An access group's name can be updated via the HandleUpdateAccessGroupName handler
	// - The handler response indicates success
	// - The access group name is actually updated when checked directly via Raw Client
	t.Run("Access Group Name Update", func(t *testing.T) {
		handler := env.MCPServer.HandleUpdateAccessGroupName()
		request := mcp.CreateMCPRequest(map[string]any{
			"id":   float64(testAccessGroupID),
			"name": testAccessGroupNewName,
		})

		result, err := handler(env.Ctx, request)
		require.NoError(t, err, "Failed to update access 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, "Access group name updated successfully", "Success message mismatch")

		// Verify by fetching access group directly via raw client
		updatedAccessGroup, err := env.RawClient.GetEndpointGroup(int64(testAccessGroupID))
		require.NoError(t, err, "Failed to get access group directly via client")
		assert.Equal(t, testAccessGroupNewName, updatedAccessGroup.Name, "Access group name was not updated")
	})

	// Subtest: Access Group User Accesses Update
	// Verifies that:
	// - User access policies can be updated via the HandleUpdateAccessGroupUserAccesses handler
	// - The handler response indicates success
	// - The access policies are correctly updated when checked directly via Raw Client
	t.Run("Access Group User Accesses Update", func(t *testing.T) {
		handler := env.MCPServer.HandleUpdateAccessGroupUserAccesses()
		request := mcp.CreateMCPRequest(map[string]any{
			"id": float64(testAccessGroupID),
			"userAccesses": []any{
				map[string]any{"id": float64(testUserID), "access": "environment_administrator"},
			},
		})

		result, err := handler(env.Ctx, request)
		require.NoError(t, err, "Failed to update access group user accesses 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, "Access group user accesses updated successfully", "Success message mismatch")

		// Verify by fetching access group directly via raw client and checking user accesses
		updatedAccessGroup, err := env.RawClient.GetEndpointGroup(int64(testAccessGroupID))
		require.NoError(t, err, "Failed to get access group directly via client")

		rawEndpoints, err := env.RawClient.ListEndpoints()
		require.NoError(t, err, "Failed to list endpoints")

		convertedAccessGroup := models.ConvertEndpointGroupToAccessGroup(updatedAccessGroup, rawEndpoints)
		userAccess, exists := convertedAccessGroup.UserAccesses[testUserID]
		assert.True(t, exists, "User access policy not found")
		assert.Equal(t, "environment_administrator", userAccess, "User access level mismatch")
	})

	// Subtest: Access Group Team Accesses Update
	// Verifies that:
	// - Team access policies can be updated via the HandleUpdateAccessGroupTeamAccesses handler
	// - The handler response indicates success
	// - The access policies are correctly updated when checked directly via Raw Client
	t.Run("Access Group Team Accesses Update", func(t *testing.T) {
		handler := env.MCPServer.HandleUpdateAccessGroupTeamAccesses()
		request := mcp.CreateMCPRequest(map[string]any{
			"id": float64(testAccessGroupID),
			"teamAccesses": []any{
				map[string]any{"id": float64(testTeamID), "access": "standard_user"},
			},
		})

		result, err := handler(env.Ctx, request)
		require.NoError(t, err, "Failed to update access group team accesses 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, "Access group team accesses updated successfully", "Success message mismatch")

		// Verify by fetching access group directly via raw client and checking team accesses
		updatedAccessGroup, err := env.RawClient.GetEndpointGroup(int64(testAccessGroupID))
		require.NoError(t, err, "Failed to get access group directly via client")

		rawEndpoints, err := env.RawClient.ListEndpoints()
		require.NoError(t, err, "Failed to list endpoints")

		convertedAccessGroup := models.ConvertEndpointGroupToAccessGroup(updatedAccessGroup, rawEndpoints)
		teamAccess, exists := convertedAccessGroup.TeamAccesses[testTeamID]
		assert.True(t, exists, "Team access policy not found")
		assert.Equal(t, "standard_user", teamAccess, "Team access level mismatch")
	})

	// Subtest: Remove Environment From Access Group
	// Verifies that:
	// - An environment can be removed from an access group via the HandleRemoveEnvironmentFromAccessGroup handler
	// - The handler response indicates success
	// - The environment is actually removed when checked directly via Raw Client
	t.Run("Remove Environment From Access Group", func(t *testing.T) {
		handler := env.MCPServer.HandleRemoveEnvironmentFromAccessGroup()
		request := mcp.CreateMCPRequest(map[string]any{
			"id":            float64(testAccessGroupID),
			"environmentId": float64(testEnvID),
		})

		result, err := handler(env.Ctx, request)
		require.NoError(t, err, "Failed to remove environment from access group 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 removed from access group successfully", "Success message mismatch")

		// Verify by fetching access group directly via raw client and checking environments
		updatedAccessGroup, err := env.RawClient.GetEndpointGroup(int64(testAccessGroupID))
		require.NoError(t, err, "Failed to get access group directly via client")

		rawEndpoints, err := env.RawClient.ListEndpoints()
		require.NoError(t, err, "Failed to list endpoints")

		convertedAccessGroup := models.ConvertEndpointGroupToAccessGroup(updatedAccessGroup, rawEndpoints)
		assert.ElementsMatch(t, []int{}, convertedAccessGroup.EnvironmentIds, "Environment was not removed from access group")
	})

	// Subtest: Add Environment To Access Group
	// Verifies that:
	// - An environment can be added back to an access group via the HandleAddEnvironmentToAccessGroup handler
	// - The handler response indicates success
	// - The environment is actually added when checked directly via Raw Client
	// Note: This test is run after the remove test to verify both operations work correctly
	t.Run("Add Environment To Access Group", func(t *testing.T) {
		handler := env.MCPServer.HandleAddEnvironmentToAccessGroup()
		request := mcp.CreateMCPRequest(map[string]any{
			"id":            float64(testAccessGroupID),
			"environmentId": float64(testEnvID),
		})

		result, err := handler(env.Ctx, request)
		require.NoError(t, err, "Failed to add environment to access group 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 added to access group successfully", "Success message mismatch")

		// Verify by fetching access group directly via raw client and checking environments
		updatedAccessGroup, err := env.RawClient.GetEndpointGroup(int64(testAccessGroupID))
		require.NoError(t, err, "Failed to get access group directly via client")

		rawEndpoints, err := env.RawClient.ListEndpoints()
		require.NoError(t, err, "Failed to list endpoints")

		convertedAccessGroup := models.ConvertEndpointGroupToAccessGroup(updatedAccessGroup, rawEndpoints)
		assert.ElementsMatch(t, []int{testEnvID}, convertedAccessGroup.EnvironmentIds, "Environment was not added to access group")
	})
}

```

--------------------------------------------------------------------------------
/internal/mcp/group_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 TestHandleGetEnvironmentGroups(t *testing.T) {
	tests := []struct {
		name        string
		mockGroups  []models.Group
		mockError   error
		expectError bool
	}{
		{
			name: "successful groups retrieval",
			mockGroups: []models.Group{
				{ID: 1, Name: "group1"},
				{ID: 2, Name: "group2"},
			},
			mockError:   nil,
			expectError: false,
		},
		{
			name:        "api error",
			mockGroups:  nil,
			mockError:   fmt.Errorf("api error"),
			expectError: true,
		},
	}

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

			server := &PortainerMCPServer{
				cli: mockClient,
			}

			handler := server.HandleGetEnvironmentGroups()
			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 groups []models.Group
				err = json.Unmarshal([]byte(textContent.Text), &groups)
				assert.NoError(t, err)
				assert.Equal(t, tt.mockGroups, groups)
			}

			mockClient.AssertExpectations(t)
		})
	}
}

func TestHandleCreateEnvironmentGroup(t *testing.T) {
	tests := []struct {
		name        string
		inputName   string
		inputEnvIDs []int
		mockID      int
		mockError   error
		expectError bool
		setupParams func(request *mcp.CallToolRequest)
	}{
		{
			name:        "successful group creation",
			inputName:   "group1",
			inputEnvIDs: []int{1, 2, 3},
			mockID:      1,
			mockError:   nil,
			expectError: false,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"name":           "group1",
					"environmentIds": []any{float64(1), float64(2), float64(3)},
				}
			},
		},
		{
			name:        "api error",
			inputName:   "group1",
			inputEnvIDs: []int{1, 2, 3},
			mockID:      0,
			mockError:   fmt.Errorf("api error"),
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"name":           "group1",
					"environmentIds": []any{float64(1), float64(2), float64(3)},
				}
			},
		},
		{
			name:        "missing name parameter",
			inputEnvIDs: []int{1, 2, 3},
			mockError:   nil,
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"environmentIds": []any{float64(1), float64(2), float64(3)},
				}
			},
		},
		{
			name:        "missing environmentIds parameter",
			inputName:   "group1",
			mockError:   nil,
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"name": "group1",
				}
			},
		},
	}

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

			server := &PortainerMCPServer{
				cli: mockClient,
			}

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

			handler := server.HandleCreateEnvironmentGroup()
			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 TestHandleUpdateEnvironmentGroupName(t *testing.T) {
	tests := []struct {
		name        string
		inputID     int
		inputName   string
		mockError   error
		expectError bool
		setupParams func(request mcp.CallToolRequest) mcp.CallToolRequest
	}{
		{
			name:        "successful name update",
			inputID:     1,
			inputName:   "newname",
			mockError:   nil,
			expectError: false,
			setupParams: func(request mcp.CallToolRequest) mcp.CallToolRequest {
				request.Params.Arguments = map[string]any{
					"id":   float64(1),
					"name": "newname",
				}
				return request
			},
		},
		{
			name:        "api error",
			inputID:     1,
			inputName:   "newname",
			mockError:   fmt.Errorf("api error"),
			expectError: true,
			setupParams: func(request mcp.CallToolRequest) mcp.CallToolRequest {
				request.Params.Arguments = map[string]any{
					"id":   float64(1),
					"name": "newname",
				}
				return request
			},
		},
		{
			name:        "missing id parameter",
			inputName:   "newname",
			mockError:   nil,
			expectError: true,
			setupParams: func(request mcp.CallToolRequest) mcp.CallToolRequest {
				request.Params.Arguments = map[string]any{
					"name": "newname",
				}
				return request
			},
		},
		{
			name:        "missing name parameter",
			inputID:     1,
			mockError:   nil,
			expectError: true,
			setupParams: func(request mcp.CallToolRequest) mcp.CallToolRequest {
				request.Params.Arguments = map[string]any{
					"id": float64(1),
				}
				return request
			},
		},
	}

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

			server := &PortainerMCPServer{
				cli: mockClient,
			}

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

			handler := server.HandleUpdateEnvironmentGroupName()
			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 TestHandleUpdateEnvironmentGroupEnvironments(t *testing.T) {
	tests := []struct {
		name        string
		inputID     int
		inputEnvIDs []int
		mockError   error
		expectError bool
		setupParams func(request mcp.CallToolRequest) mcp.CallToolRequest
	}{
		{
			name:        "successful environments update",
			inputID:     1,
			inputEnvIDs: []int{1, 2, 3},
			mockError:   nil,
			expectError: false,
			setupParams: func(request mcp.CallToolRequest) mcp.CallToolRequest {
				request.Params.Arguments = map[string]any{
					"id":             float64(1),
					"environmentIds": []any{float64(1), float64(2), float64(3)},
				}
				return request
			},
		},
		{
			name:        "api error",
			inputID:     1,
			inputEnvIDs: []int{1, 2, 3},
			mockError:   fmt.Errorf("api error"),
			expectError: true,
			setupParams: func(request mcp.CallToolRequest) mcp.CallToolRequest {
				request.Params.Arguments = map[string]any{
					"id":             float64(1),
					"environmentIds": []any{float64(1), float64(2), float64(3)},
				}
				return request
			},
		},
		{
			name:        "missing id parameter",
			inputEnvIDs: []int{1, 2, 3},
			mockError:   nil,
			expectError: true,
			setupParams: func(request mcp.CallToolRequest) mcp.CallToolRequest {
				request.Params.Arguments = map[string]any{
					"environmentIds": []any{float64(1), float64(2), float64(3)},
				}
				return request
			},
		},
		{
			name:        "missing environmentIds parameter",
			inputID:     1,
			mockError:   nil,
			expectError: true,
			setupParams: func(request mcp.CallToolRequest) mcp.CallToolRequest {
				request.Params.Arguments = map[string]any{
					"id":   float64(1),
					"name": "group1",
				}
				return request
			},
		},
	}

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

			server := &PortainerMCPServer{
				cli: mockClient,
			}

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

			handler := server.HandleUpdateEnvironmentGroupEnvironments()
			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 TestHandleUpdateEnvironmentGroupTags(t *testing.T) {
	tests := []struct {
		name        string
		inputID     int
		inputTagIDs []int
		mockError   error
		expectError bool
		setupParams func(request mcp.CallToolRequest) mcp.CallToolRequest
	}{
		{
			name:        "successful tags update",
			inputID:     1,
			inputTagIDs: []int{1, 2, 3},
			mockError:   nil,
			expectError: false,
			setupParams: func(request mcp.CallToolRequest) mcp.CallToolRequest {
				request.Params.Arguments = map[string]any{
					"id":     float64(1),
					"tagIds": []any{float64(1), float64(2), float64(3)},
				}
				return request
			},
		},
		{
			name:        "api error",
			inputID:     1,
			inputTagIDs: []int{1, 2, 3},
			mockError:   fmt.Errorf("api error"),
			expectError: true,
			setupParams: func(request mcp.CallToolRequest) mcp.CallToolRequest {
				request.Params.Arguments = map[string]any{
					"id":     float64(1),
					"tagIds": []any{float64(1), float64(2), float64(3)},
				}
				return request
			},
		},
		{
			name:        "missing id parameter",
			inputTagIDs: []int{1, 2, 3},
			mockError:   nil,
			expectError: true,
			setupParams: func(request mcp.CallToolRequest) mcp.CallToolRequest {
				request.Params.Arguments = map[string]any{
					"tagIds": []any{float64(1), float64(2), float64(3)},
				}
				return request
			},
		},
		{
			name:        "missing tagIds parameter",
			inputID:     1,
			mockError:   nil,
			expectError: true,
			setupParams: func(request mcp.CallToolRequest) mcp.CallToolRequest {
				request.Params.Arguments = map[string]any{
					"id": float64(1),
				}
				return request
			},
		},
	}

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

			server := &PortainerMCPServer{
				cli: mockClient,
			}

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

			handler := server.HandleUpdateEnvironmentGroupTags()
			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/toolgen/yaml_test.go:
--------------------------------------------------------------------------------

```go
package toolgen

import (
	"os"
	"path/filepath"
	"reflect"
	"testing"

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

func TestLoadToolsFromYAML(t *testing.T) {
	// Create a minimal test YAML file
	tmpDir := t.TempDir()
	validYamlPath := filepath.Join(tmpDir, "valid.yaml")
	validYamlContent := `version: "v1.0.0"
tools:
  - name: testTool
    description: A test tool
    parameters:
      - name: param1
        type: string
        required: true
        description: A test parameter
    annotations:
      title: Test Tool Title
      readOnlyHint: true
      destructiveHint: false
      idempotentHint: true
      openWorldHint: false`

	err := os.WriteFile(validYamlPath, []byte(validYamlContent), 0644)
	if err != nil {
		t.Fatalf("Failed to create test YAML file: %v", err)
	}

	// Create a newer version YAML file
	newerVersionPath := filepath.Join(tmpDir, "newer.yaml")
	newerVersionContent := `version: "v1.2.0"
tools:
  - name: testTool
    description: A test tool
    parameters:
      - name: param1
        type: string
        required: true
        description: A test parameter
    annotations:
      title: Test Tool Title
      readOnlyHint: true
      destructiveHint: false
      idempotentHint: true
      openWorldHint: false`

	err = os.WriteFile(newerVersionPath, []byte(newerVersionContent), 0644)
	if err != nil {
		t.Fatalf("Failed to create newer version YAML file: %v", err)
	}

	// Create an older version YAML file (will fail version check)
	olderVersionPath := filepath.Join(tmpDir, "older.yaml")
	olderVersionContent := `version: "v0.9.0"
tools:
  - name: testTool
    description: A test tool
    parameters:
      - name: param1
        type: string
        required: true
        description: A test parameter
    # Annotations potentially missing, but version check fails first
`

	err = os.WriteFile(olderVersionPath, []byte(olderVersionContent), 0644)
	if err != nil {
		t.Fatalf("Failed to create older version YAML file: %v", err)
	}

	// Create a file with missing version (will fail version check)
	missingVersionPath := filepath.Join(tmpDir, "missing_version.yaml")
	missingVersionContent := `tools:
  - name: testTool
    description: A test tool
    parameters:
      - name: param1
        type: string
        required: true
        description: A test parameter
    # Annotations potentially missing, but version check fails first
`

	err = os.WriteFile(missingVersionPath, []byte(missingVersionContent), 0644)
	if err != nil {
		t.Fatalf("Failed to create missing version YAML file: %v", err)
	}

	// Create a file with invalid version format (will fail version check)
	invalidVersionPath := filepath.Join(tmpDir, "invalid_version.yaml")
	invalidVersionContent := `version: "1.0"
tools:
  - name: testTool
    description: A test tool
    parameters:
      - name: param1
        type: string
        required: true
        description: A test parameter
    # Annotations potentially missing, but version check fails first
`

	err = os.WriteFile(invalidVersionPath, []byte(invalidVersionContent), 0644)
	if err != nil {
		t.Fatalf("Failed to create invalid version YAML file: %v", err)
	}

	// Create a file with missing annotations block (should fail annotation check)
	missingAnnotationsPath := filepath.Join(tmpDir, "missing_annotations.yaml")
	missingAnnotationsContent := `version: "v1.0.0"
tools:
  - name: toolWithoutAnnotations
    description: A test tool missing annotations
    parameters:
      - name: param1
        type: string
        required: true
        description: A test parameter
  - name: toolWithAnnotations
    description: A test tool with annotations
    annotations:
      title: Some Title
      readOnlyHint: false
      destructiveHint: false
      idempotentHint: false
      openWorldHint: false
`
	err = os.WriteFile(missingAnnotationsPath, []byte(missingAnnotationsContent), 0644)
	if err != nil {
		t.Fatalf("Failed to create missing annotations YAML file: %v", err)
	}

	tests := []struct {
		name           string
		filePath       string
		minimumVersion string
		wantErr        bool
		wantTool       string // name of tool we expect to find
		wantToolCount  int    // expected number of tools loaded
	}{
		{
			name:           "valid yaml file",
			filePath:       validYamlPath,
			minimumVersion: "v1.0.0",
			wantErr:        false,
			wantTool:       "testTool",
			wantToolCount:  1,
		},
		{
			name:           "valid yaml file with newer minimum version",
			filePath:       validYamlPath,
			minimumVersion: "v1.1.0",
			wantErr:        true, // Error because file version is below minimum
		},
		{
			name:           "newer version yaml file",
			filePath:       newerVersionPath,
			minimumVersion: "v1.0.0",
			wantErr:        false,
			wantTool:       "testTool",
			wantToolCount:  1,
		},
		{
			name:           "older version yaml file",
			filePath:       olderVersionPath,
			minimumVersion: "v1.0.0",
			wantErr:        true, // Error because file version is below minimum
		},
		{
			name:           "missing version in yaml",
			filePath:       missingVersionPath,
			minimumVersion: "v1.0.0",
			wantErr:        true,
		},
		{
			name:           "invalid version format",
			filePath:       invalidVersionPath,
			minimumVersion: "v1.0.0",
			wantErr:        true, // Error because version format is invalid
		},
		{
			name:           "missing annotations block",
			filePath:       missingAnnotationsPath,
			minimumVersion: "v1.0.0",
			wantErr:        false,                 // LoadToolsFromYAML itself doesn't error, but skips the invalid tool
			wantTool:       "toolWithAnnotations", // Only the tool with annotations should load
			wantToolCount:  1,                     // Expect only one tool to be loaded successfully
		},
		{
			name:           "non-existent file",
			filePath:       "nonexistent.yaml",
			minimumVersion: "v1.0.0",
			wantErr:        true,
		},
		{
			name:           "invalid yaml content",
			filePath:       createInvalidYAMLFile(t),
			minimumVersion: "v1.0.0",
			wantErr:        true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tools, err := LoadToolsFromYAML(tt.filePath, tt.minimumVersion)
			if (err != nil) != tt.wantErr {
				t.Errorf("LoadToolsFromYAML() error = %v, wantErr %v", err, tt.wantErr)
				return
			}

			if !tt.wantErr {
				if len(tools) != tt.wantToolCount {
					t.Errorf("LoadToolsFromYAML() loaded %d tools, want %d", len(tools), tt.wantToolCount)
				}
				if tt.wantTool != "" {
					tool, exists := tools[tt.wantTool]
					if !exists {
						t.Errorf("Expected tool '%s' not found in loaded tools: %v", tt.wantTool, tools)
						return
					}
					if tool.Name != tt.wantTool {
						t.Errorf("Tool name mismatch, got %s, want %s", tool.Name, tt.wantTool)
					}
					if tool.Description == "" {
						t.Errorf("Tool %s has no description", tt.wantTool)
					}
					// Basic check to ensure annotations were processed (more detailed checks in TestConvertToolDefinition)
					if tool.Annotations.Title == "" { // Check a field within Annotations
						t.Errorf("Tool %s seems to be missing processed annotations", tt.wantTool)
					}
				}
			}
		})
	}
}

// Helper function to create an invalid YAML file for testing
func createInvalidYAMLFile(t *testing.T) string {
	tmpDir := t.TempDir()
	path := filepath.Join(tmpDir, "invalid.yaml")
	// Add annotations to avoid failing that check first
	content := `version: "v1.0.0"
tools:
  - name: invalid
    description: [invalid yaml content
    annotations:
      title: Invalid Tool`

	err := os.WriteFile(path, []byte(content), 0644)
	if err != nil {
		t.Fatalf("Failed to create invalid YAML file: %v", err)
	}
	return path
}

func TestConvertToolDefinition(t *testing.T) {
	// Define a valid annotation struct to reuse
	validAnnotations := Annotations{
		Title:           "Valid Title",
		ReadOnlyHint:    true,
		DestructiveHint: false,
		IdempotentHint:  true,
		OpenWorldHint:   false,
	}

	tests := []struct {
		name          string
		def           ToolDefinition
		wantErr       bool
		wantErrSubstr string              // Optional: check for specific error message content
		want          *mcp.ToolAnnotation // Expected annotation output
	}{
		{
			name: "valid tool definition",
			def: ToolDefinition{
				Name:        "validTool",
				Description: "A valid tool description",
				Annotations: validAnnotations,
			},
			wantErr: false,
			want: &mcp.ToolAnnotation{
				Title:           "Valid Title",
				ReadOnlyHint:    &validAnnotations.ReadOnlyHint,
				DestructiveHint: &validAnnotations.DestructiveHint,
				IdempotentHint:  &validAnnotations.IdempotentHint,
				OpenWorldHint:   &validAnnotations.OpenWorldHint,
			},
		},
		{
			name: "empty name",
			def: ToolDefinition{
				Name:        "",
				Description: "A tool with empty name",
				Annotations: validAnnotations, // Needs annotations even if name is invalid
			},
			wantErr:       true,
			wantErrSubstr: "tool name is required",
		},
		{
			name: "empty description",
			def: ToolDefinition{
				Name:        "noDescTool",
				Description: "",
				Annotations: validAnnotations, // Needs annotations even if desc is invalid
			},
			wantErr:       true,
			wantErrSubstr: "tool description is required",
		},
		{
			name: "missing annotations",
			def: ToolDefinition{
				Name:        "noAnnotationTool",
				Description: "Tool without annotations",
				Annotations: Annotations{}, // Zero value simulates missing block
			},
			wantErr:       true,
			wantErrSubstr: "annotations block is required",
		},
		{
			name: "with parameters",
			def: ToolDefinition{
				Name:        "paramTool",
				Description: "Tool with parameters",
				Parameters: []ParameterDefinition{
					{
						Name:        "param1",
						Type:        "string",
						Required:    true,
						Description: "A test parameter",
					},
				},
				Annotations: validAnnotations,
			},
			wantErr: false,
			want: &mcp.ToolAnnotation{
				Title:           "Valid Title",
				ReadOnlyHint:    &validAnnotations.ReadOnlyHint,
				DestructiveHint: &validAnnotations.DestructiveHint,
				IdempotentHint:  &validAnnotations.IdempotentHint,
				OpenWorldHint:   &validAnnotations.OpenWorldHint,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tool, err := convertToolDefinition(tt.def)
			if tt.wantErr {
				assert.Error(t, err)
				if tt.wantErrSubstr != "" {
					assert.Contains(t, err.Error(), tt.wantErrSubstr)
				}
			} else {
				assert.NoError(t, err)
				assert.Equal(t, tt.def.Name, tool.Name)
				assert.Equal(t, tt.def.Description, tool.Description)
				assert.Equal(t, *tt.want, tool.Annotations)

			}
		})
	}
}

func TestConvertToolDefinitions(t *testing.T) {
	// Define a valid annotation struct to reuse
	validAnnotations := Annotations{
		Title:           "Valid Title",
		ReadOnlyHint:    true,
		DestructiveHint: false,
		IdempotentHint:  true,
		OpenWorldHint:   false,
	}

	tests := []struct {
		name string
		defs []ToolDefinition
		want int // number of tools expected to be successfully converted
	}{
		{
			name: "empty definitions",
			defs: []ToolDefinition{},
			want: 0,
		},
		{
			name: "single valid tool",
			defs: []ToolDefinition{
				{
					Name:        "tool1",
					Description: "Test tool 1",
					Parameters: []ParameterDefinition{
						{
							Name:        "param1",
							Type:        "string",
							Required:    true,
							Description: "Test parameter",
						},
					},
					Annotations: validAnnotations,
				},
			},
			want: 1,
		},
		{
			name: "multiple valid tools",
			defs: []ToolDefinition{
				{
					Name:        "tool1",
					Description: "Test tool 1",
					Annotations: validAnnotations,
				},
				{
					Name:        "tool2",
					Description: "Test tool 2",
					Annotations: validAnnotations,
				},
			},
			want: 2,
		},
		{
			name: "invalid tools are skipped",
			defs: []ToolDefinition{
				{
					Name:        "validTool1",
					Description: "Test tool 1",
					Annotations: validAnnotations,
				},
				{
					Name:        "", // Invalid: empty name
					Description: "Tool with empty name",
					Annotations: validAnnotations,
				},
				{
					Name:        "noDescTool", // Invalid: empty description
					Description: "",
					Annotations: validAnnotations,
				},
				{
					Name:        "noAnnotationTool", // Invalid: missing annotations
					Description: "Tool missing annotations",
					Annotations: Annotations{}, // Zero value
				},
				{
					Name:        "validTool2",
					Description: "Test tool 2",
					Annotations: validAnnotations,
				},
			},
			want: 2, // Only 2 valid tools should be returned
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := convertToolDefinitions(tt.defs)
			assert.Len(t, got, tt.want)

			// Verify each tool expected to be converted exists and is valid
			for _, def := range tt.defs {
				// Skip definitions that are expected to cause errors
				if def.Name == "" || def.Description == "" || (def.Annotations == Annotations{}) {
					continue
				}

				tool, exists := got[def.Name]
				assert.True(t, exists, "Tool %s not found in result", def.Name)
				if exists {
					assert.Equal(t, def.Name, tool.Name)
					assert.Equal(t, def.Description, tool.Description)
					assert.NotEmpty(t, tool.Annotations.Title) // Basic check that title is populated
				}
			}
		})
	}
}

func TestConvertParameter(t *testing.T) {
	tests := []struct {
		name  string
		param ParameterDefinition
		want  reflect.Type // We'll check the type of the returned option
	}{
		{
			name: "string parameter",
			param: ParameterDefinition{
				Name:        "strParam",
				Type:        "string",
				Required:    true,
				Description: "A string parameter",
			},
			want: reflect.TypeOf(mcp.WithString("", mcp.Description(""))),
		},
		{
			name: "number parameter",
			param: ParameterDefinition{
				Name:        "numParam",
				Type:        "number",
				Required:    true,
				Description: "A number parameter",
			},
			want: reflect.TypeOf(mcp.WithNumber("", mcp.Description(""))),
		},
		{
			name: "boolean parameter",
			param: ParameterDefinition{
				Name:        "boolParam",
				Type:        "boolean",
				Required:    true,
				Description: "A boolean parameter",
			},
			want: reflect.TypeOf(mcp.WithBoolean("", mcp.Description(""))),
		},
		{
			name: "array parameter",
			param: ParameterDefinition{
				Name:        "arrayParam",
				Type:        "array",
				Required:    true,
				Description: "An array parameter",
				Items: map[string]any{
					"type": "string",
				},
			},
			want: reflect.TypeOf(mcp.WithArray("", mcp.Description(""))),
		},
		{
			name: "object parameter",
			param: ParameterDefinition{
				Name:        "objParam",
				Type:        "object",
				Required:    true,
				Description: "An object parameter",
				Items: map[string]any{
					"type": "object",
					"properties": map[string]any{
						"key": map[string]any{
							"type": "string",
						},
					},
				},
			},
			want: reflect.TypeOf(mcp.WithObject("", mcp.Description(""))),
		},
		{
			name: "enum parameter",
			param: ParameterDefinition{
				Name:        "enumParam",
				Type:        "string",
				Required:    true,
				Description: "An enum parameter",
				Enum:        []string{"val1", "val2"},
			},
			want: reflect.TypeOf(mcp.WithString("", mcp.Description(""))),
		},
		{
			name: "unknown type parameter",
			param: ParameterDefinition{
				Name:        "unknownParam",
				Type:        "unknown",
				Required:    true,
				Description: "An unknown parameter",
			},
			want: reflect.TypeOf(mcp.WithString("", mcp.Description(""))), // defaults to string
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := convertParameter(tt.param)
			gotType := reflect.TypeOf(got)
			if gotType != tt.want {
				t.Errorf("convertParameter() returned %v, want %v", gotType, tt.want)
			}
		})
	}
}

// Optional: Add a specific test for convertAnnotation if desired, though it's simple
func TestConvertAnnotation(t *testing.T) {
	input := Annotations{
		Title:           "Test Title",
		ReadOnlyHint:    true,
		DestructiveHint: true,
		IdempotentHint:  false,
		OpenWorldHint:   false,
	}
	want := mcp.ToolAnnotation{
		Title:           "Test Title",
		ReadOnlyHint:    &input.ReadOnlyHint,
		DestructiveHint: &input.DestructiveHint,
		IdempotentHint:  &input.IdempotentHint,
		OpenWorldHint:   &input.OpenWorldHint,
	}

	dummyTool := &mcp.Tool{}
	option := convertAnnotation(input)
	option(dummyTool)

	assert.NotNil(t, option)
	assert.Equal(t, want, dummyTool.Annotations)
}

```

--------------------------------------------------------------------------------
/internal/mcp/kubernetes_test.go:
--------------------------------------------------------------------------------

```go
package mcp

import (
	"context"
	"errors"
	"net/http"
	"testing"

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

func TestHandleKubernetesProxy_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),
				"kubernetesAPIPath": "/api/v1/pods",
				"method":            "POST",
				"body":              true, // Invalid type for body
			},
			expectedErrorMsg: "body must be a string",
		},
		{
			name: "missing environmentId",
			inputParams: map[string]any{
				"kubernetesAPIPath": "/api/v1/pods",
				"method":            "GET",
			},
			expectedErrorMsg: "environmentId is required",
		},
		{
			name: "missing kubernetesAPIPath",
			inputParams: map[string]any{
				"environmentId": float64(1),
				"method":        "GET",
			},
			expectedErrorMsg: "kubernetesAPIPath is required",
		},
		{
			name: "missing method",
			inputParams: map[string]any{
				"environmentId":     float64(1),
				"kubernetesAPIPath": "/api/v1/pods",
			},
			expectedErrorMsg: "method is required",
		},
		{
			name: "invalid kubernetesAPIPath (no leading slash)",
			inputParams: map[string]any{
				"environmentId":     float64(1),
				"kubernetesAPIPath": "api/v1/pods",
				"method":            "GET",
			},
			expectedErrorMsg: "kubernetesAPIPath must start with a leading slash",
		},
		{
			name: "invalid HTTP method",
			inputParams: map[string]any{
				"environmentId":     float64(1),
				"kubernetesAPIPath": "/api/v1/pods",
				"method":            "INVALID",
			},
			expectedErrorMsg: "invalid method: INVALID",
		},
		{
			name: "invalid queryParams type (not an array)",
			inputParams: map[string]any{
				"environmentId":     float64(1),
				"kubernetesAPIPath": "/api/v1/pods",
				"method":            "GET",
				"queryParams":       "not-an-array",
			},
			expectedErrorMsg: "queryParams must be an array",
		},
		{
			name: "invalid queryParams content (value not string)",
			inputParams: map[string]any{
				"environmentId":     float64(1),
				"kubernetesAPIPath": "/api/v1/pods",
				"method":            "GET",
				"queryParams":       []any{map[string]any{"key": "namespace", "value": false}},
			},
			expectedErrorMsg: "invalid query params: invalid value: false",
		},
		{
			name: "invalid headers type (not an array)",
			inputParams: map[string]any{
				"environmentId":     float64(1),
				"kubernetesAPIPath": "/api/v1/pods",
				"method":            "GET",
				"headers":           "header-string",
			},
			expectedErrorMsg: "headers must be an array",
		},
		{
			name: "invalid headers content (missing value)",
			inputParams: map[string]any{
				"environmentId":     float64(1),
				"kubernetesAPIPath": "/api/v1/pods",
				"method":            "GET",
				"headers":           []any{map[string]any{"key": "Content-Type"}},
			},
			expectedErrorMsg: "invalid headers: invalid value: <nil>",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			server := &PortainerMCPServer{} // No client needed for param validation

			request := CreateMCPRequest(tt.inputParams)
			handler := server.HandleKubernetesProxy()
			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 TestHandleKubernetesProxy_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 with query params",
			input: map[string]any{
				"environmentId":     float64(1),
				"kubernetesAPIPath": "/api/v1/pods",
				"method":            "GET",
				"queryParams": []any{
					map[string]any{"key": "namespace", "value": "default"},
					map[string]any{"key": "labelSelector", "value": "app=myApp"},
				},
			},
			mock: struct {
				response *http.Response
				err      error
			}{
				response: createMockHttpResponse(http.StatusOK, `{"kind":"PodList","items":[]}`),
				err:      nil,
			},
			expect: struct {
				errSubstring string
				resultText   string
			}{
				resultText: `{"kind":"PodList","items":[]}`,
			},
		},
		{
			name: "successful POST request with body and headers",
			input: map[string]any{
				"environmentId":     float64(2),
				"kubernetesAPIPath": "/api/v1/namespaces/test/services",
				"method":            "POST",
				"body":              `{"apiVersion":"v1","kind":"Service","metadata":{"name":"my-service"}}`,
				"headers": []any{
					map[string]any{"key": "Content-Type", "value": "application/json"},
				},
			},
			mock: struct {
				response *http.Response
				err      error
			}{
				response: createMockHttpResponse(http.StatusCreated, `{"metadata":{"name":"my-service"}}`),
				err:      nil,
			},
			expect: struct {
				errSubstring string
				resultText   string
			}{
				resultText: `{"metadata":{"name":"my-service"}}`,
			},
		},
		{
			name: "client API error",
			input: map[string]any{
				"environmentId":     float64(3),
				"kubernetesAPIPath": "/version",
				"method":            "GET",
			},
			mock: struct {
				response *http.Response
				err      error
			}{
				response: nil,
				err:      errors.New("k8s api error"),
			},
			expect: struct {
				errSubstring string
				resultText   string
			}{
				errSubstring: "failed to send Kubernetes API request: k8s api error",
			},
		},
		{
			name: "error reading response body",
			input: map[string]any{
				"environmentId":     float64(4),
				"kubernetesAPIPath": "/healthz",
				"method":            "GET",
			},
			mock: struct {
				response *http.Response
				err      error
			}{
				response: &http.Response{
					StatusCode: http.StatusOK,
					Body:       &errorReader{}, // Simulate read error
				},
				err: nil,
			},
			expect: struct {
				errSubstring string
				resultText   string
			}{
				errSubstring: "failed to read Kubernetes API response: simulated read error",
			},
		},
	}

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

			mockClient.On("ProxyKubernetesRequest", mock.AnythingOfType("models.KubernetesProxyRequestOptions")).
				Return(tc.mock.response, tc.mock.err)

			server := &PortainerMCPServer{
				cli: mockClient,
			}

			request := CreateMCPRequest(tc.input)
			handler := server.HandleKubernetesProxy()
			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)
		})
	}
}

func TestHandleKubernetesProxyStripped_ParameterValidation(t *testing.T) {
	tests := []struct {
		name             string
		inputParams      map[string]any
		expectedErrorMsg string
	}{
		{
			name: "missing environmentId",
			inputParams: map[string]any{
				"kubernetesAPIPath": "/api/v1/pods",
			},
			expectedErrorMsg: "environmentId is required",
		},
		{
			name: "missing kubernetesAPIPath",
			inputParams: map[string]any{
				"environmentId": float64(1),
			},
			expectedErrorMsg: "kubernetesAPIPath is required",
		},
		{
			name: "invalid kubernetesAPIPath (no leading slash)",
			inputParams: map[string]any{
				"environmentId":     float64(1),
				"kubernetesAPIPath": "api/v1/pods",
			},
			expectedErrorMsg: "kubernetesAPIPath must start with a leading slash",
		},
		{
			name: "invalid queryParams type (not an array)",
			inputParams: map[string]any{
				"environmentId":     float64(1),
				"kubernetesAPIPath": "/api/v1/pods",
				"queryParams":       "not-an-array",
			},
			expectedErrorMsg: "queryParams must be an array",
		},
		{
			name: "invalid queryParams content (value not string)",
			inputParams: map[string]any{
				"environmentId":     float64(1),
				"kubernetesAPIPath": "/api/v1/pods",
				"queryParams":       []any{map[string]any{"key": "namespace", "value": false}},
			},
			expectedErrorMsg: "invalid query params: invalid value: false",
		},
		{
			name: "invalid headers type (not an array)",
			inputParams: map[string]any{
				"environmentId":     float64(1),
				"kubernetesAPIPath": "/api/v1/pods",
				"headers":           "header-string",
			},
			expectedErrorMsg: "headers must be an array",
		},
		{
			name: "invalid headers content (missing value)",
			inputParams: map[string]any{
				"environmentId":     float64(1),
				"kubernetesAPIPath": "/api/v1/pods",
				"headers":           []any{map[string]any{"key": "Content-Type"}},
			},
			expectedErrorMsg: "invalid headers: invalid value: <nil>",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			server := &PortainerMCPServer{} // No client needed for param validation

			request := CreateMCPRequest(tt.inputParams)
			handler := server.HandleKubernetesProxyStripped()
			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 TestHandleKubernetesProxyStripped_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 with managedFields stripped",
			input: map[string]any{
				"environmentId":     float64(1),
				"kubernetesAPIPath": "/api/v1/pods",
				"queryParams": []any{
					map[string]any{"key": "namespace", "value": "default"},
					map[string]any{"key": "labelSelector", "value": "app=myApp"},
				},
			},
			mock: struct {
				response *http.Response
				err      error
			}{
				response: createMockHttpResponse(http.StatusOK, `{
					"apiVersion": "v1",
					"kind": "PodList",
					"items": [
						{
							"apiVersion": "v1",
							"kind": "Pod",
							"metadata": {
								"name": "test-pod-1",
								"namespace": "default",
								"managedFields": [
									{
										"manager": "kubectl-client-side-apply",
										"operation": "Update",
										"apiVersion": "v1",
										"time": "2023-01-01T00:00:00Z"
									}
								]
							},
							"spec": {
								"containers": [
									{
										"name": "test-container",
										"image": "nginx"
									}
								]
							}
						}
					]
				}`),
				err: nil,
			},
			expect: struct {
				errSubstring string
				resultText   string
			}{
				resultText: `{"apiVersion":"v1","items":[{"apiVersion":"v1","kind":"Pod","metadata":{"name":"test-pod-1","namespace":"default"},"spec":{"containers":[{"image":"nginx","name":"test-container"}]}}],"kind":"PodList"}`,
			},
		},
		{
			name: "successful GET request with headers",
			input: map[string]any{
				"environmentId":     float64(2),
				"kubernetesAPIPath": "/api/v1/namespaces/default/pods",
				"headers": []any{
					map[string]any{"key": "X-Custom-Header", "value": "test-value"},
					map[string]any{"key": "Authorization", "value": "Bearer abc"},
				},
			},
			mock: struct {
				response *http.Response
				err      error
			}{
				response: createMockHttpResponse(http.StatusOK, `{
					"apiVersion": "v1",
					"kind": "Pod",
					"metadata": {
						"name": "test-pod",
						"namespace": "default",
						"managedFields": [
							{
								"manager": "kubectl-client-side-apply",
								"operation": "Update",
								"apiVersion": "v1",
								"time": "2023-01-01T00:00:00Z"
							}
						]
					},
					"spec": {
						"containers": [
							{
								"name": "test-container",
								"image": "nginx"
							}
						]
					}
				}`),
				err: nil,
			},
			expect: struct {
				errSubstring string
				resultText   string
			}{
				resultText: `{"apiVersion":"v1","kind":"Pod","metadata":{"name":"test-pod","namespace":"default"},"spec":{"containers":[{"image":"nginx","name":"test-container"}]}}`,
			},
		},
		{
			name: "client API error",
			input: map[string]any{
				"environmentId":     float64(3),
				"kubernetesAPIPath": "/version",
			},
			mock: struct {
				response *http.Response
				err      error
			}{
				response: nil,
				err:      errors.New("k8s api error"),
			},
			expect: struct {
				errSubstring string
				resultText   string
			}{
				errSubstring: "failed to send Kubernetes API request: k8s api error",
			},
		},
		{
			name: "error processing response body",
			input: map[string]any{
				"environmentId":     float64(4),
				"kubernetesAPIPath": "/healthz",
			},
			mock: struct {
				response *http.Response
				err      error
			}{
				response: &http.Response{
					StatusCode: http.StatusOK,
					Body:       &errorReader{}, // Simulate read error
				},
				err: nil,
			},
			expect: struct {
				errSubstring string
				resultText   string
			}{
				errSubstring: "failed to process Kubernetes API response: failed to read response body: simulated read error",
			},
		},
		{
			name: "empty response body",
			input: map[string]any{
				"environmentId":     float64(5),
				"kubernetesAPIPath": "/api/v1/namespaces",
			},
			mock: struct {
				response *http.Response
				err      error
			}{
				response: createMockHttpResponse(http.StatusNoContent, ""),
				err:      nil,
			},
			expect: struct {
				errSubstring string
				resultText   string
			}{
				resultText: "",
			},
		},
		{
			name: "invalid JSON response",
			input: map[string]any{
				"environmentId":     float64(6),
				"kubernetesAPIPath": "/api/v1/pods",
			},
			mock: struct {
				response *http.Response
				err      error
			}{
				response: createMockHttpResponse(http.StatusOK, "invalid json"),
				err:      nil,
			},
			expect: struct {
				errSubstring string
				resultText   string
			}{
				errSubstring: "failed to process Kubernetes API response: failed to unmarshal JSON into Unstructured",
			},
		},
	}

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

			mockClient.On("ProxyKubernetesRequest", mock.AnythingOfType("models.KubernetesProxyRequestOptions")).
				Return(tc.mock.response, tc.mock.err)

			server := &PortainerMCPServer{
				cli: mockClient,
			}

			request := CreateMCPRequest(tc.input)
			handler := server.HandleKubernetesProxyStripped()
			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)
				if tc.expect.resultText == "" {
					assert.Equal(t, tc.expect.resultText, textContent.Text)
				} else {
					assert.JSONEq(t, tc.expect.resultText, textContent.Text)
				}
			}

			mockClient.AssertExpectations(t)
		})
	}
}

```

--------------------------------------------------------------------------------
/internal/mcp/access_group_test.go:
--------------------------------------------------------------------------------

```go
package mcp

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

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

func TestHandleGetAccessGroups(t *testing.T) {
	tests := []struct {
		name        string
		mockGroups  []models.AccessGroup
		mockError   error
		expectError bool
	}{
		{
			name: "successful groups retrieval",
			mockGroups: []models.AccessGroup{
				{ID: 1, Name: "group1"},
				{ID: 2, Name: "group2"},
			},
			mockError:   nil,
			expectError: false,
		},
		{
			name:        "api error",
			mockGroups:  nil,
			mockError:   fmt.Errorf("api error"),
			expectError: true,
		},
	}

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

			server := &PortainerMCPServer{
				cli: mockClient,
			}

			handler := server.HandleGetAccessGroups()
			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 groups []models.AccessGroup
				err = json.Unmarshal([]byte(textContent.Text), &groups)
				assert.NoError(t, err)
				assert.Equal(t, tt.mockGroups, groups)
			}

			mockClient.AssertExpectations(t)
		})
	}
}

func TestHandleCreateAccessGroup(t *testing.T) {
	tests := []struct {
		name        string
		inputName   string
		inputEnvIDs []int
		mockID      int
		mockError   error
		expectError bool
		setupParams func(request *mcp.CallToolRequest)
	}{
		{
			name:        "successful group creation",
			inputName:   "group1",
			inputEnvIDs: []int{1, 2, 3},
			mockID:      1,
			mockError:   nil,
			expectError: false,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"name":           "group1",
					"environmentIds": []any{float64(1), float64(2), float64(3)},
				}
			},
		},
		{
			name:        "api error",
			inputName:   "group1",
			inputEnvIDs: []int{1, 2, 3},
			mockID:      0,
			mockError:   fmt.Errorf("api error"),
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"name":           "group1",
					"environmentIds": []any{float64(1), float64(2), float64(3)},
				}
			},
		},
		{
			name:        "missing name parameter",
			inputEnvIDs: []int{1, 2, 3},
			mockError:   nil,
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"environmentIds": []any{float64(1), float64(2), float64(3)},
				}
			},
		},
		{
			name:        "invalid environmentIds - not an array",
			inputName:   "group1",
			mockError:   nil,
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"name":           "group1",
					"environmentIds": "not an array",
				}
			},
		},
		{
			name:        "invalid environmentIds - array with non-numbers",
			inputName:   "group1",
			mockError:   nil,
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"name":           "group1",
					"environmentIds": []any{"1", "2", "3"},
				}
			},
		},
		{
			name:        "invalid environmentIds - array with mixed types",
			inputName:   "group1",
			mockError:   nil,
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"name":           "group1",
					"environmentIds": []any{float64(1), "2", float64(3)},
				}
			},
		},
	}

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

			server := &PortainerMCPServer{
				cli: mockClient,
			}

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

			handler := server.HandleCreateAccessGroup()
			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 TestHandleUpdateAccessGroupName(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:   "newname",
			mockError:   nil,
			expectError: false,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id":   float64(1),
					"name": "newname",
				}
			},
		},
		{
			name:        "api error",
			inputID:     1,
			inputName:   "newname",
			mockError:   fmt.Errorf("api error"),
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id":   float64(1),
					"name": "newname",
				}
			},
		},
		{
			name:        "missing id parameter",
			inputName:   "newname",
			mockError:   nil,
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"name": "newname",
				}
			},
		},
		{
			name:        "missing name parameter",
			inputID:     1,
			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("UpdateAccessGroupName", tt.inputID, tt.inputName).Return(tt.mockError)
			}

			server := &PortainerMCPServer{
				cli: mockClient,
			}

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

			handler := server.HandleUpdateAccessGroupName()
			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 TestHandleUpdateAccessGroupUserAccesses(t *testing.T) {
	tests := []struct {
		name          string
		inputID       int
		inputAccesses []map[string]any
		mockError     error
		expectError   bool
		setupParams   func(request *mcp.CallToolRequest)
	}{
		{
			name:    "successful user accesses update",
			inputID: 1,
			inputAccesses: []map[string]any{
				{"id": float64(1), "access": "environment_administrator"},
				{"id": float64(2), "access": "standard_user"},
			},
			mockError:   nil,
			expectError: false,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id": float64(1),
					"userAccesses": []any{
						map[string]any{"id": float64(1), "access": "environment_administrator"},
						map[string]any{"id": float64(2), "access": "standard_user"},
					},
				}
			},
		},
		{
			name:    "api error",
			inputID: 1,
			inputAccesses: []map[string]any{
				{"id": float64(1), "access": "environment_administrator"},
			},
			mockError:   fmt.Errorf("api error"),
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id": float64(1),
					"userAccesses": []any{
						map[string]any{"id": float64(1), "access": "environment_administrator"},
					},
				}
			},
		},
		{
			name:        "missing id parameter",
			mockError:   nil,
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"userAccesses": []any{
						map[string]any{"id": float64(1), "access": "environment_administrator"},
					},
				}
			},
		},
		{
			name:        "missing userAccesses parameter",
			inputID:     1,
			mockError:   nil,
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id": float64(1),
				}
			},
		},
		{
			name:    "invalid access level",
			inputID: 1,
			inputAccesses: []map[string]any{
				{"id": float64(1), "access": "invalid_access"},
			},
			mockError:   nil,
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id": float64(1),
					"userAccesses": []any{
						map[string]any{"id": float64(1), "access": "invalid_access"},
					},
				}
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockClient := &MockPortainerClient{}
			if !tt.expectError || tt.mockError != nil {
				expectedMap := make(map[int]string)
				for _, access := range tt.inputAccesses {
					id := int(access["id"].(float64))
					expectedMap[id] = access["access"].(string)
				}
				mockClient.On("UpdateAccessGroupUserAccesses", tt.inputID, expectedMap).Return(tt.mockError)
			}

			server := &PortainerMCPServer{
				cli: mockClient,
			}

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

			handler := server.HandleUpdateAccessGroupUserAccesses()
			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/validation errors")
					if strings.Contains(tt.name, "invalid access level") {
						assert.Contains(t, textContent.Text, "invalid user accesses")
					}
				}
			} 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 TestHandleUpdateAccessGroupTeamAccesses(t *testing.T) {
	tests := []struct {
		name          string
		inputID       int
		inputAccesses []map[string]any
		mockError     error
		expectError   bool
		setupParams   func(request *mcp.CallToolRequest)
	}{
		{
			name:    "successful team accesses update",
			inputID: 1,
			inputAccesses: []map[string]any{
				{"id": float64(1), "access": "environment_administrator"},
				{"id": float64(2), "access": "standard_user"},
			},
			mockError:   nil,
			expectError: false,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id": float64(1),
					"teamAccesses": []any{
						map[string]any{"id": float64(1), "access": "environment_administrator"},
						map[string]any{"id": float64(2), "access": "standard_user"},
					},
				}
			},
		},
		{
			name:    "api error",
			inputID: 1,
			inputAccesses: []map[string]any{
				{"id": float64(1), "access": "environment_administrator"},
			},
			mockError:   fmt.Errorf("api error"),
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id": float64(1),
					"teamAccesses": []any{
						map[string]any{"id": float64(1), "access": "environment_administrator"},
					},
				}
			},
		},
		{
			name:        "missing id parameter",
			mockError:   nil,
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"teamAccesses": []any{
						map[string]any{"id": float64(1), "access": "environment_administrator"},
					},
				}
			},
		},
		{
			name:        "missing teamAccesses parameter",
			inputID:     1,
			mockError:   nil,
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id": float64(1),
				}
			},
		},
		{
			name:    "invalid access level",
			inputID: 1,
			inputAccesses: []map[string]any{
				{"id": float64(1), "access": "invalid_access"},
			},
			mockError:   nil,
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id": float64(1),
					"teamAccesses": []any{
						map[string]any{"id": float64(1), "access": "invalid_access"},
					},
				}
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockClient := &MockPortainerClient{}
			if !tt.expectError || tt.mockError != nil {
				expectedMap := make(map[int]string)
				for _, access := range tt.inputAccesses {
					id := int(access["id"].(float64))
					expectedMap[id] = access["access"].(string)
				}
				mockClient.On("UpdateAccessGroupTeamAccesses", tt.inputID, expectedMap).Return(tt.mockError)
			}

			server := &PortainerMCPServer{
				cli: mockClient,
			}

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

			handler := server.HandleUpdateAccessGroupTeamAccesses()
			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/validation errors")
					if strings.Contains(tt.name, "invalid access level") {
						assert.Contains(t, textContent.Text, "invalid team accesses")
					}
				}
			} 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 TestHandleAddEnvironmentToAccessGroup(t *testing.T) {
	tests := []struct {
		name        string
		inputID     int
		inputEnvID  int
		mockError   error
		expectError bool
		setupParams func(request *mcp.CallToolRequest)
	}{
		{
			name:        "successful environment addition",
			inputID:     1,
			inputEnvID:  2,
			mockError:   nil,
			expectError: false,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id":            float64(1),
					"environmentId": float64(2),
				}
			},
		},
		{
			name:        "api error",
			inputID:     1,
			inputEnvID:  2,
			mockError:   fmt.Errorf("api error"),
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id":            float64(1),
					"environmentId": float64(2),
				}
			},
		},
		{
			name:        "missing id parameter",
			inputEnvID:  2,
			mockError:   nil,
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"environmentId": float64(2),
				}
			},
		},
		{
			name:        "missing environmentId parameter",
			inputID:     1,
			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("AddEnvironmentToAccessGroup", tt.inputID, tt.inputEnvID).Return(tt.mockError)
			}

			server := &PortainerMCPServer{
				cli: mockClient,
			}

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

			handler := server.HandleAddEnvironmentToAccessGroup()
			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 TestHandleRemoveEnvironmentFromAccessGroup(t *testing.T) {
	tests := []struct {
		name        string
		inputID     int
		inputEnvID  int
		mockError   error
		expectError bool
		setupParams func(request *mcp.CallToolRequest)
	}{
		{
			name:        "successful environment removal",
			inputID:     1,
			inputEnvID:  2,
			mockError:   nil,
			expectError: false,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id":            float64(1),
					"environmentId": float64(2),
				}
			},
		},
		{
			name:        "api error",
			inputID:     1,
			inputEnvID:  2,
			mockError:   fmt.Errorf("api error"),
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"id":            float64(1),
					"environmentId": float64(2),
				}
			},
		},
		{
			name:        "missing id parameter",
			inputEnvID:  2,
			mockError:   nil,
			expectError: true,
			setupParams: func(request *mcp.CallToolRequest) {
				request.Params.Arguments = map[string]any{
					"environmentId": float64(2),
				}
			},
		},
		{
			name:        "missing environmentId parameter",
			inputID:     1,
			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("RemoveEnvironmentFromAccessGroup", tt.inputID, tt.inputEnvID).Return(tt.mockError)
			}

			server := &PortainerMCPServer{
				cli: mockClient,
			}

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

			handler := server.HandleRemoveEnvironmentFromAccessGroup()
			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)
		})
	}
}

```

--------------------------------------------------------------------------------
/internal/tooldef/tools.yaml:
--------------------------------------------------------------------------------

```yaml
---
version: v1.2
tools:
  ## Access Groups
  ## An access group is the equivalent of an Endpoint Group in Portainer.
  ## ------------------------------------------------------------
  - name: listAccessGroups
    description: List all available access groups
    annotations:
      title: List Access Groups
      readOnlyHint: true
      destructiveHint: false
      idempotentHint: true
      openWorldHint: false
  - name: createAccessGroup
    description: Create a new access group. Use access groups when you want to define
      accesses on more than one environment. Otherwise, define the accesses on
      the environment level.
    parameters:
      - name: name
        description: The name of the access group
        type: string
        required: true
      - name: environmentIds
        description: "The IDs of the environments that are part of the access group.
          Must include all the environment IDs that are part of the group - this
          includes new environments and the existing environments that are
          already associated with the group. Example: [1, 2, 3]"
        type: array
        items:
          type: number
    annotations:
      title: Create Access Group
      readOnlyHint: false
      destructiveHint: false
      idempotentHint: false
      openWorldHint: false
  - name: updateAccessGroupName
    description: Update the name of an existing access group.
    parameters:
      - name: id
        description: The ID of the access group to update
        type: number
        required: true
      - name: name
        description: The name of the access group
        type: string
        required: true
    annotations:
      title: Update Access Group Name
      readOnlyHint: false
      destructiveHint: false
      idempotentHint: true
      openWorldHint: false
  - name: updateAccessGroupUserAccesses
    description: Update the user accesses of an existing access group.
    parameters:
      - name: id
        description: The ID of the access group to update
        type: number
        required: true
      - name: userAccesses
        description: "The user accesses that are associated with all the environments in
          the access group. The ID is the user ID of the user in Portainer.
          Example: [{id: 1, access: 'environment_administrator'}, {id: 2,
          access: 'standard_user'}]"
        type: array
        required: true
        items:
          type: object
          properties:
            id:
              description: The ID of the user
              type: number
            access:
              description: The access level of the user. Can be environment_administrator,
                helpdesk_user, standard_user, readonly_user or operator_user
              type: string
              enum:
                - environment_administrator
                - helpdesk_user
                - standard_user
                - readonly_user
                - operator_user
    annotations:
      title: Update Access Group User Accesses
      readOnlyHint: false
      destructiveHint: false
      idempotentHint: true
      openWorldHint: false
  - name: updateAccessGroupTeamAccesses
    description: Update the team accesses of an existing access group.
    parameters:
      - name: id
        description: The ID of the access group to update
        type: number
        required: true
      - name: teamAccesses
        description: "The team accesses that are associated with all the environments in
          the access group. The ID is the team ID of the team in Portainer.
          Example: [{id: 1, access: 'environment_administrator'}, {id: 2,
          access: 'standard_user'}]"
        type: array
        required: true
        items:
          type: object
          properties:
            id:
              description: The ID of the team
              type: number
            access:
              description: The access level of the team. Can be environment_administrator,
                helpdesk_user, standard_user, readonly_user or operator_user
              type: string
              enum:
                - environment_administrator
                - helpdesk_user
                - standard_user
                - readonly_user
                - operator_user
    annotations:
      title: Update Access Group Team Accesses
      readOnlyHint: false
      destructiveHint: false
      idempotentHint: true
      openWorldHint: false
  - name: addEnvironmentToAccessGroup
    description: Add an environment to an access group.
    parameters:
      - name: id
        description: The ID of the access group to update
        type: number
        required: true
      - name: environmentId
        description: The ID of the environment to add to the access group
        type: number
        required: true
    annotations:
      title: Add Environment To Access Group
      readOnlyHint: false
      destructiveHint: false
      idempotentHint: true
      openWorldHint: false
  - name: removeEnvironmentFromAccessGroup
    description: Remove an environment from an access group.
    parameters:
      - name: id
        description: The ID of the access group to update
        type: number
        required: true
      - name: environmentId
        description: The ID of the environment to remove from the access group
        type: number
        required: true
    annotations:
      title: Remove Environment From Access Group
      readOnlyHint: false
      destructiveHint: true
      idempotentHint: true
      openWorldHint: false
  ## Environment
  ## ------------------------------------------------------------
  - name: listEnvironments
    description: List all available environments
    annotations:
      title: List Environments
      readOnlyHint: true
      destructiveHint: false
      idempotentHint: true
      openWorldHint: false
  - name: updateEnvironmentTags
    description: Update the tags associated with an environment
    parameters:
      - name: id
        description: The ID of the environment to update
        type: number
        required: true
      - name: tagIds
        description: >-
          The IDs of the tags that are associated with the environment.
          Must include all the tag IDs that should be associated with the environment - this includes new tags and existing tags.
          Providing an empty array will remove all tags.
          Example: [1, 2, 3]
        type: array
        required: true
        items:
          type: number
    annotations:
      title: Update Environment Tags
      readOnlyHint: false
      destructiveHint: false
      idempotentHint: true
      openWorldHint: false
  - name: updateEnvironmentUserAccesses
    description: Update the user access policies of an environment
    parameters:
      - name: id
        description: The ID of the environment to update
        type: number
        required: true
      - name: userAccesses
        description: >-
          The user accesses that are associated with the environment.
          The ID is the user ID of the user in Portainer.
          Must include all the access policies for all users that should be associated with the environment.
          Providing an empty array will remove all user accesses.
          Example: [{id: 1, access: 'environment_administrator'}, {id: 2, access: 'standard_user'}]
        type: array
        required: true
        items:
          type: object
          properties:
            id:
              description: The ID of the user
              type: number
            access:
              description: The access level of the user
              type: string
              enum:
                - environment_administrator
                - helpdesk_user
                - standard_user
                - readonly_user
                - operator_user
    annotations:
      title: Update Environment User Accesses
      readOnlyHint: false
      destructiveHint: false
      idempotentHint: true
      openWorldHint: false
  - name: updateEnvironmentTeamAccesses
    description: Update the team access policies of an environment
    parameters:
      - name: id
        description: The ID of the environment to update
        type: number
        required: true
      - name: teamAccesses
        description: >-
          The team accesses that are associated with the environment.
          The ID is the team ID of the team in Portainer.
          Must include all the access policies for all teams that should be associated with the environment.
          Providing an empty array will remove all team accesses.
          Example: [{id: 1, access: 'environment_administrator'}, {id: 2, access: 'standard_user'}]
        type: array
        required: true
        items:
          type: object
          properties:
            id:
              description: The ID of the team
              type: number
            access:
              description: The access level of the team
              type: string
              enum:
                - environment_administrator
                - helpdesk_user
                - standard_user
                - readonly_user
                - operator_user
    annotations:
      title: Update Environment Team Accesses
      readOnlyHint: false
      destructiveHint: false
      idempotentHint: true
      openWorldHint: false
  ## Environment Groups
  ## An environment group is the equivalent of an Edge Group in Portainer.
  ## ------------------------------------------------------------
  - name: createEnvironmentGroup
    description: Create a new environment group. Environment groups are the equivalent of Edge Groups in Portainer.
    parameters:
      - name: name
        description: The name of the environment group
        type: string
        required: true
      - name: environmentIds
        description: The IDs of the environments to add to the group
        type: array
        required: true
        items:
          type: number
    annotations:
      title: Create Environment Group
      readOnlyHint: false
      destructiveHint: false
      idempotentHint: false
      openWorldHint: false
  - name: listEnvironmentGroups
    description: List all available environment groups. Environment groups are the equivalent of Edge Groups in Portainer.
    annotations:
      title: List Environment Groups
      readOnlyHint: true
      destructiveHint: false
      idempotentHint: true
      openWorldHint: false
  - name: updateEnvironmentGroupName
    description: Update the name of an environment group. Environment groups are the equivalent of Edge Groups in Portainer.
    parameters:
      - name: id
        description: The ID of the environment group to update
        type: number
        required: true
      - name: name
        description: The new name for the environment group
        type: string
        required: true
    annotations:
      title: Update Environment Group Name
      readOnlyHint: false
      destructiveHint: false
      idempotentHint: true
      openWorldHint: false
  - name: updateEnvironmentGroupEnvironments
    description: Update the environments associated with an environment group. Environment groups are the equivalent of Edge Groups in Portainer.
    parameters:
      - name: id
        description: The ID of the environment group to update
        type: number
        required: true
      - name: environmentIds
        description: >-
          The IDs of the environments that should be part of the group.
          Must include all environment IDs that should be associated with the group.
          Providing an empty array will remove all environments from the group.
          Example: [1, 2, 3]
        type: array
        required: true
        items:
          type: number
    annotations:
      title: Update Environment Group Environments
      readOnlyHint: false
      destructiveHint: false
      idempotentHint: true
      openWorldHint: false
  - name: updateEnvironmentGroupTags
    description: Update the tags associated with an environment group. Environment groups are the equivalent of Edge Groups in Portainer.
    parameters:
      - name: id
        description: The ID of the environment group to update
        type: number
        required: true
      - name: tagIds
        description: >-
          The IDs of the tags that should be associated with the group.
          Must include all tag IDs that should be associated with the group.
          Providing an empty array will remove all tags from the group.
          Example: [1, 2, 3]
        type: array
        required: true
        items:
          type: number
    annotations:
      title: Update Environment Group Tags
      readOnlyHint: false
      destructiveHint: false
      idempotentHint: true
      openWorldHint: false
  ## Settings
  ## ------------------------------------------------------------
  - name: getSettings
    description: Get the settings of the Portainer instance
    annotations:
      title: Get Settings
      readOnlyHint: true
      destructiveHint: false
      idempotentHint: true
      openWorldHint: false
  ## Stacks
  ## ------------------------------------------------------------
  - name: listStacks
    description: List all available stacks
    annotations:
      title: List Stacks
      readOnlyHint: true
      destructiveHint: false
      idempotentHint: true
      openWorldHint: false
  - name: getStackFile
    description: Get the compose file for a specific stack ID
    parameters:
      - name: id
        description: The ID of the stack to get the compose file for
        type: number
        required: true
    annotations:
      title: Get Stack File
      readOnlyHint: true
      destructiveHint: false
      idempotentHint: true
      openWorldHint: false
  - name: createStack
    description: Create a new stack
    parameters:
      - name: name
        description: Name of the stack. Stack name must only consist of lowercase alpha
          characters, numbers, hyphens, or underscores as well as start with a
          lowercase character or number
        type: string
        required: true
      - name: file
        description: >-
          Content of the stack file. The file must be a valid
          docker-compose.yml file. example: services:
           web:
             image:nginx
        type: string
        required: true
      - name: environmentGroupIds
        description: "The IDs of the environment groups that the stack belongs to. Must
          include at least one environment group ID. Example: [1, 2, 3]"
        type: array
        required: true
        items:
          type: number
    annotations:
      title: Create Stack
      readOnlyHint: false
      destructiveHint: false
      idempotentHint: false
      openWorldHint: false
  - name: updateStack
    description: Update an existing stack
    parameters:
      - name: id
        description: The ID of the stack to update
        type: number
        required: true
      - name: file
        description: >-
          Content of the stack file. The file must be a valid
          docker-compose.yml file. example: version: 3
           services:
             web:
               image:nginx
        type: string
        required: true
      - name: environmentGroupIds
        description: "The IDs of the environment groups that the stack belongs to. Must
          include at least one environment group ID. Example: [1, 2, 3]"
        type: array
        required: true
        items:
          type: number
    annotations:
      title: Update Stack
      readOnlyHint: false
      destructiveHint: false
      idempotentHint: true
      openWorldHint: false
  ## Tags
  ## ------------------------------------------------------------
  - name: createEnvironmentTag
    description: Create a new environment tag
    parameters:
      - name: name
        description: The name of the tag
        type: string
        required: true
    annotations:
      title: Create Environment Tag
      readOnlyHint: false
      destructiveHint: false
      idempotentHint: false
      openWorldHint: false
  - name: listEnvironmentTags
    description: List all available environment tags
    annotations:
      title: List Environment Tags
      readOnlyHint: true
      destructiveHint: false
      idempotentHint: true
      openWorldHint: false
  ## Teams
  ## ------------------------------------------------------------
  - name: createTeam
    description: Create a new team
    parameters:
      - name: name
        description: The name of the team
        type: string
        required: true
    annotations:
      title: Create Team
      readOnlyHint: false
      destructiveHint: false
      idempotentHint: false
      openWorldHint: false
  - name: listTeams
    description: List all available teams
    annotations:
      title: List Teams
      readOnlyHint: true
      destructiveHint: false
      idempotentHint: true
      openWorldHint: false
  - name: updateTeamName
    description: Update the name of an existing team
    parameters:
      - name: id
        description: The ID of the team to update
        type: number
        required: true
      - name: name
        description: The new name of the team
        type: string
        required: true
    annotations:
      title: Update Team Name
      readOnlyHint: false
      destructiveHint: false
      idempotentHint: true
      openWorldHint: false
  - name: updateTeamMembers
    description: Update the members of an existing team
    parameters:
      - name: id
        description: The ID of the team to update
        type: number
        required: true
      - name: userIds
        description: "The IDs of the users that are part of the team. Must include all
          the user IDs that are part of the team - this includes new users and
          the existing users that are already associated with the team. Example:
          [1, 2, 3]"
        type: array
        required: true
        items:
          type: number
    annotations:
      title: Update Team Members
      readOnlyHint: false
      destructiveHint: false
      idempotentHint: true
      openWorldHint: false

  ## Users
  ## ------------------------------------------------------------
  - name: listUsers
    description: List all available users
    annotations:
      title: List Users
      readOnlyHint: true
      destructiveHint: false
      idempotentHint: true
      openWorldHint: false
  - name: updateUserRole
    description: Update an existing user
    parameters:
      - name: id
        description: The ID of the user to update
        type: number
        required: true
      - name: role
        description: The role of the user. Can be admin, user or edge_admin
        type: string
        required: true
        enum:
          - admin
          - user
          - edge_admin
    annotations:
      title: Update User Role
      readOnlyHint: false
      destructiveHint: false
      idempotentHint: true
      openWorldHint: false

  ## Docker Proxy
  ## ------------------------------------------------------------
  - name: dockerProxy
    description: Proxy Docker requests to a specific Portainer environment.
      This tool can be used with any Docker API operation as documented in the Docker Engine API specification (https://docs.docker.com/reference/api/engine/version/v1.48/).
    parameters:
      - name: environmentId
        description: The ID of the environment to proxy Docker requests to
        type: number
        required: true
      - name: method
        description: The HTTP method to use to proxy the Docker API operation
        type: string
        required: true
        enum:
          - GET
          - POST
          - PUT
          - DELETE
          - HEAD
      - name: dockerAPIPath
        description: "The route of the Docker API operation to proxy. Must include the leading slash. Example: /containers/json"
        type: string
        required: true
      - name: queryParams
        description: "The query parameters to include in the Docker API operation. Must be an array of key-value pairs.
          Example: [{key: 'all', value: 'true'}, {key: 'filter', value: 'dangling'}]"
        type: array
        required: false
        items:
          type: object
          properties:
            key:
              type: string
              description: The key of the query parameter
            value:
              type: string
              description: The value of the query parameter
      - name: headers
        description: "The headers to include in the Docker API operation. Must be an array of key-value pairs.
          Example: [{key: 'Content-Type', value: 'application/json'}]"
        type: array
        required: false
        items:
          type: object
          properties:
            key:
              type: string
              description: The key of the header
            value:
              type: string
              description: The value of the header
      - name: body
        description: "The body of the Docker API operation to proxy. Must be a JSON string.
          Example: {'Image': 'nginx:latest', 'Name': 'my-container'}"
        type: string
        required: false
    annotations:
      title: Docker Proxy
      readOnlyHint: true
      destructiveHint: true
      idempotentHint: true
      openWorldHint: false

  ## Kubernetes Proxy
  ## ------------------------------------------------------------
  - name: kubernetesProxy
    description: Proxy Kubernetes requests to a specific Portainer environment.
      This tool can be used with any Kubernetes API operation as documented in the Kubernetes API specification (https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/).
    parameters:
      - name: environmentId
        description: The ID of the environment to proxy Kubernetes requests to
        type: number
        required: true
      - name: method
        description: The HTTP method to use to proxy the Kubernetes API operation
        type: string
        required: true
        enum:
          - GET
          - POST
          - PUT
          - DELETE
          - HEAD
      - name: kubernetesAPIPath
        description: "The route of the Kubernetes API operation to proxy. Must include the leading slash. Example: /api/v1/namespaces/default/pods"
        type: string
        required: true
      - name: queryParams
        description: "The query parameters to include in the Kubernetes API operation. Must be an array of key-value pairs.
          Example: [{key: 'watch', value: 'true'}, {key: 'fieldSelector', value: 'metadata.name=my-pod'}]"
        type: array
        required: false
        items:
          type: object
          properties:
            key:
              type: string
              description: The key of the query parameter
            value:
              type: string
              description: The value of the query parameter
      - name: headers
        description: "The headers to include in the Kubernetes API operation. Must be an array of key-value pairs.
          Example: [{key: 'Content-Type', value: 'application/json'}]"
        type: array
        required: false
        items:
          type: object
          properties:
            key:
              type: string
              description: The key of the header
            value:
              type: string
              description: The value of the header
      - name: body
        description: "The body of the Kubernetes API operation to proxy. Must be a JSON string.
          Example: {'apiVersion': 'v1', 'kind': 'Pod', 'metadata': {'name': 'my-pod'}}"
        type: string
        required: false
    annotations:
      title: Kubernetes Proxy
      readOnlyHint: true
      destructiveHint: true
      idempotentHint: true
      openWorldHint: false
  - name: getKubernetesResourceStripped
    description: >-
      Proxy GET requests to a specific Portainer environment for Kubernetes resources,
      and automatically strips verbose metadata fields (such as 'managedFields') from the API response
      to reduce its size. This tool is intended for retrieving Kubernetes resource
      information where a leaner payload is desired.
      This tool can be used with any GET Kubernetes API operation as documented
      in the Kubernetes API specification (https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/).
      For other methods (POST, PUT, DELETE, HEAD), use the 'kubernetesProxy' tool.
    parameters:
      - name: environmentId
        description: The ID of the environment to proxy Kubernetes GET requests to
        type: number
        required: true
      - name: kubernetesAPIPath
        description: "The route of the Kubernetes API GET operation to proxy. Must include the leading slash. Example: /api/v1/namespaces/default/pods"
        type: string
        required: true
      - name: queryParams
        description: "The query parameters to include in the Kubernetes API operation. Must be an array of key-value pairs.
          Example: [{key: 'watch', value: 'true'}, {key: 'fieldSelector', value: 'metadata.name=my-pod'}]"
        type: array
        required: false
        items:
          type: object
          properties:
            key:
              type: string
              description: The key of the query parameter
            value:
              type: string
              description: The value of the query parameter
      - name: headers
        description: "The headers to include in the Kubernetes API operation. Must be an array of key-value pairs.
          Example: [{key: 'Accept', value: 'application/json'}]"
        type: array
        required: false
        items:
          type: object
          properties:
            key:
              type: string
              description: The key of the header
            value:
              type: string
              description: The value of the header
    annotations:
      title: Get Kubernetes Resource (Stripped)
      readOnlyHint: true
      destructiveHint: false
      idempotentHint: true
      openWorldHint: false
```
Page 3/4FirstPrevNextLast