Skip to content

Commit 45bebc7

Browse files
authored
feat: introduce a flag to disable version check (#33)
* feat: add disable version check functionality for Portainer MCP - Introduced a `--disable-version-check` flag to allow connections to unsupported Portainer versions, enhancing flexibility for users. - Updated the README to include instructions and warnings regarding the use of the new flag. - Modified the MCP server implementation to conditionally skip version validation based on the flag. - Enhanced unit tests to cover scenarios involving the disabled version check, ensuring robust error handling and functionality. * test: add integration test for disabled version check in Portainer MCP server - Implemented a new test to verify that the Portainer MCP server can connect to unsupported Portainer versions when the version check is disabled. - Ensured proper setup and teardown of the Portainer container during the test. - Validated the successful creation of the MCP server and the retrieval of settings, confirming functionality without version validation.
1 parent ee543d0 commit 45bebc7

File tree

5 files changed

+129
-11
lines changed

5 files changed

+129
-11
lines changed

README.md

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ MCP (Model Context Protocol) is an open protocol that standardizes how applicati
1616

1717
This implementation focuses on exposing Portainer environment data through the MCP protocol, allowing AI assistants and other tools to interact with your containerized infrastructure in a secure and standardized way.
1818

19-
See the [Portainer Version Support](#portainer-version-support) and [Supported Capabilities](#supported-capabilities) sections for more details on compatibility and available features.
19+
> [!NOTE]
20+
> This tool is designed to work with specific Portainer versions. If your Portainer version doesn't match the supported version, you can use the `--disable-version-check` flag to attempt connection anyway. See [Portainer Version Support](#portainer-version-support) for compatible versions and [Disable Version Check](#disable-version-check) for bypass instructions.
21+
22+
See the [Supported Capabilities](#supported-capabilities) sections for more details on compatibility and available features.
2023

2124
*Note: This project is currently under development.*
2225

@@ -89,6 +92,42 @@ Replace `[IP]`, `[PORT]` and `[TOKEN]` with the IP, port and API access token as
8992
> [!NOTE]
9093
> By default, the tool looks for "tools.yaml" in the same directory as the binary. If the file does not exist, it will be created there with the default tool definitions. You may need to modify this path as described above, particularly when using AI assistants like Claude that have restricted write permissions to the working directory.
9194
95+
## Disable Version Check
96+
97+
By default, the application validates that your Portainer server version matches the supported version and will fail to start if there's a mismatch. If you have a Portainer server version that doesn't have a corresponding Portainer MCP version available, you can disable this version check to attempt connection anyway.
98+
99+
To disable the version check, add the `-disable-version-check` flag to your command arguments:
100+
101+
```
102+
{
103+
"mcpServers": {
104+
"portainer": {
105+
"command": "/path/to/portainer-mcp",
106+
"args": [
107+
"-server",
108+
"[IP]:[PORT]",
109+
"-token",
110+
"[TOKEN]",
111+
"-disable-version-check"
112+
]
113+
}
114+
}
115+
}
116+
```
117+
118+
> [!WARNING]
119+
> Disabling the version check may result in unexpected behavior or API incompatibilities if your Portainer server version differs significantly from the supported version. The tool may work partially or not at all with unsupported versions.
120+
121+
When using this flag:
122+
- The application will skip Portainer server version validation at startup
123+
- Some features may not work correctly due to API differences between versions
124+
- Newer Portainer versions may have API changes that cause errors
125+
- Older Portainer versions may be missing APIs that the tool expects
126+
127+
This flag is useful when:
128+
- You're running a newer Portainer version that doesn't have MCP support yet
129+
- You're running an older Portainer version and want to try the tool anyway
130+
92131
## Tool Customization
93132

94133
By default, the tool definitions are embedded in the binary. The application will create a tools file at the default location if one doesn't already exist.
@@ -160,6 +199,9 @@ This tool is pinned to support a specific version of Portainer. The application
160199
| 0.4.1 | 2.29.2 |
161200
| 0.5.0 | 2.30.0 |
162201

202+
> [!NOTE]
203+
> If you need to connect to an unsupported Portainer version, you can use the `-disable-version-check` flag to bypass version validation. See the [Disable Version Check](#disable-version-check) section for more details and important warnings about using this feature.
204+
163205
# Supported Capabilities
164206

165207
The following table lists the currently (latest version) supported operations through MCP tools:

cmd/portainer-mcp/mcp.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ func main() {
2727
tokenFlag := flag.String("token", "", "The authentication token for the Portainer server")
2828
toolsFlag := flag.String("tools", "", "The path to the tools YAML file")
2929
readOnlyFlag := flag.Bool("read-only", false, "Run in read-only mode")
30+
disableVersionCheckFlag := flag.Bool("disable-version-check", false, "Disable Portainer server version check")
31+
3032
flag.Parse()
3133

3234
if *serverFlag == "" || *tokenFlag == "" {
@@ -55,9 +57,10 @@ func main() {
5557
Str("portainer-host", *serverFlag).
5658
Str("tools-path", toolsPath).
5759
Bool("read-only", *readOnlyFlag).
60+
Bool("disable-version-check", *disableVersionCheckFlag).
5861
Msg("starting MCP server")
5962

60-
server, err := mcp.NewPortainerMCPServer(*serverFlag, *tokenFlag, toolsPath, mcp.WithReadOnly(*readOnlyFlag))
63+
server, err := mcp.NewPortainerMCPServer(*serverFlag, *tokenFlag, toolsPath, mcp.WithReadOnly(*readOnlyFlag), mcp.WithDisableVersionCheck(*disableVersionCheckFlag))
6164
if err != nil {
6265
log.Fatal().Err(err).Msg("failed to create server")
6366
}

internal/mcp/server.go

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,9 @@ type ServerOption func(*serverOptions)
9090

9191
// serverOptions contains all configurable options for the server
9292
type serverOptions struct {
93-
client PortainerClient
94-
readOnly bool
93+
client PortainerClient
94+
readOnly bool
95+
disableVersionCheck bool
9596
}
9697

9798
// WithClient sets a custom client for the server.
@@ -110,6 +111,14 @@ func WithReadOnly(readOnly bool) ServerOption {
110111
}
111112
}
112113

114+
// WithDisableVersionCheck disables the Portainer server version check.
115+
// This allows connecting to unsupported Portainer versions.
116+
func WithDisableVersionCheck(disable bool) ServerOption {
117+
return func(opts *serverOptions) {
118+
opts.disableVersionCheck = disable
119+
}
120+
}
121+
113122
// NewPortainerMCPServer creates a new Portainer MCP server.
114123
//
115124
// This server provides an implementation of the MCP protocol for Portainer,
@@ -148,13 +157,15 @@ func NewPortainerMCPServer(serverURL, token, toolsPath string, options ...Server
148157
portainerClient = client.NewPortainerClient(serverURL, token, client.WithSkipTLSVerify(true))
149158
}
150159

151-
version, err := portainerClient.GetVersion()
152-
if err != nil {
153-
return nil, fmt.Errorf("failed to get Portainer server version: %w", err)
154-
}
160+
if !opts.disableVersionCheck {
161+
version, err := portainerClient.GetVersion()
162+
if err != nil {
163+
return nil, fmt.Errorf("failed to get Portainer server version: %w", err)
164+
}
155165

156-
if version != SupportedPortainerVersion {
157-
return nil, fmt.Errorf("unsupported Portainer server version: %s, only version %s is supported", version, SupportedPortainerVersion)
166+
if version != SupportedPortainerVersion {
167+
return nil, fmt.Errorf("unsupported Portainer server version: %s, only version %s is supported", version, SupportedPortainerVersion)
168+
}
158169
}
159170

160171
return &PortainerMCPServer{

internal/mcp/server_test.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,16 @@ func TestNewPortainerMCPServer(t *testing.T) {
7575
expectError: true,
7676
errorContains: "unsupported Portainer server version",
7777
},
78+
{
79+
name: "unsupported version with disabled version check",
80+
serverURL: "https://portainer.example.com",
81+
token: "valid-token",
82+
toolsPath: validToolsPath,
83+
mockSetup: func(m *MockPortainerClient) {
84+
// No GetVersion call expected when version check is disabled
85+
},
86+
expectError: false,
87+
},
7888
}
7989

8090
for _, tt := range tests {
@@ -84,11 +94,19 @@ func TestNewPortainerMCPServer(t *testing.T) {
8494
tt.mockSetup(mockClient)
8595

8696
// Create server with mock client using the WithClient option
97+
var options []ServerOption
98+
options = append(options, WithClient(mockClient))
99+
100+
// Add WithDisableVersionCheck for the specific test case
101+
if tt.name == "unsupported version with disabled version check" {
102+
options = append(options, WithDisableVersionCheck(true))
103+
}
104+
87105
server, err := NewPortainerMCPServer(
88106
tt.serverURL,
89107
tt.token,
90108
tt.toolsPath,
91-
WithClient(mockClient),
109+
options...,
92110
)
93111

94112
if tt.expectError {

tests/integration/server_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"testing"
77

8+
mcpmodels "github.com/mark3labs/mcp-go/mcp"
89
"github.com/portainer/portainer-mcp/internal/mcp"
910
"github.com/portainer/portainer-mcp/tests/integration/containers"
1011
"github.com/stretchr/testify/assert"
@@ -74,3 +75,46 @@ func TestServerInitializationUnsupportedVersion(t *testing.T) {
7475
assert.Contains(t, err.Error(), "unsupported Portainer server version", "Error should indicate version mismatch")
7576
assert.Nil(t, mcpServer, "Server should be nil when version check fails")
7677
}
78+
79+
// TestServerInitializationDisabledVersionCheck verifies that the Portainer MCP server
80+
// can successfully connect to unsupported Portainer versions when version check is disabled.
81+
func TestServerInitializationDisabledVersionCheck(t *testing.T) {
82+
// Start a Portainer container with unsupported version
83+
ctx := context.Background()
84+
85+
portainer, err := containers.NewPortainerContainer(ctx, containers.WithImage(unsupportedImage))
86+
require.NoError(t, err, "Failed to start unsupported Portainer container")
87+
88+
// Ensure container is terminated at the end of the test
89+
defer func() {
90+
if err := portainer.Terminate(ctx); err != nil {
91+
t.Logf("Failed to terminate container: %v", err)
92+
}
93+
}()
94+
95+
// Get the host and port for the Portainer API
96+
host, port := portainer.GetHostAndPort()
97+
serverURL := fmt.Sprintf("%s:%s", host, port)
98+
apiToken := portainer.GetAPIToken()
99+
100+
// Create the MCP server with disabled version check - should succeed despite unsupported version
101+
mcpServer, err := mcp.NewPortainerMCPServer(serverURL, apiToken, toolsPath, mcp.WithDisableVersionCheck(true))
102+
103+
// Assert the server was created successfully
104+
require.NoError(t, err, "Failed to create MCP server with disabled version check")
105+
require.NotNil(t, mcpServer, "MCP server should not be nil when version check is disabled")
106+
107+
// Verify basic functionality by testing settings retrieval
108+
handler := mcpServer.HandleGetSettings()
109+
request := mcp.CreateMCPRequest(nil) // GetSettings doesn't require parameters
110+
111+
result, err := handler(ctx, request)
112+
require.NoError(t, err, "Failed to get settings via MCP handler with disabled version check")
113+
require.NotNil(t, result, "Settings result should not be nil")
114+
require.Len(t, result.Content, 1, "Expected exactly one content block in settings result")
115+
116+
// Verify the response contains valid content
117+
textContent, ok := result.Content[0].(mcpmodels.TextContent)
118+
require.True(t, ok, "Expected text content in settings MCP response")
119+
assert.NotEmpty(t, textContent.Text, "Settings response should not be empty")
120+
}

0 commit comments

Comments
 (0)