package server // ============================================================================= // MCP Integration Tests // ============================================================================= // // This file contains tests for the MCP (Model Context Protocol) implementation. // // Test Categories: // // 1. Client Tests (TestMCPClient*) // - Client initialization and lifecycle // - Environment variable filtering // - Timeout handling // // 2. Security Tests (TestDangerous*, TestShellInjection*, TestSecure*) // - Command blocklist validation // - Shell metacharacter detection // - Credential filtering // // 3. Manager Tests (TestMCPManager*, TestToolResult*, TestParallel*) // - Server registration // - Tool caching // - Parallel execution // // 4. Auto-Enable Tests (TestAutoEnable*) // - Mode: never, always, with_path, if_match // - Conditions: file_exists, env_set // // Run all MCP tests: // go test -v ./server/... -run "TestMCP|TestSecure|TestShell|TestTool|TestDanger|TestParallel|TestAutoEnable" // // ============================================================================= import ( "context" "fmt" "os" "path/filepath" "strings" "testing" "time" "github.com/stretchr/testify/require" "github.com/ollama/ollama/api" ) // TestMCPClientInitialization tests the MCP client initialization func TestMCPClientInitialization(t *testing.T) { client := NewMCPClient("test", "echo", []string{"test"}, nil) require.Equal(t, "test", client.name) require.Equal(t, "echo", client.command) require.False(t, client.initialized, "Client should not be initialized on creation") } // TestSecureEnvironmentFiltering tests environment variable filtering func TestSecureEnvironmentFiltering(t *testing.T) { // Set some test environment variables os.Setenv("TEST_SAFE_VAR", "safe_value") os.Setenv("AWS_SECRET_ACCESS_KEY", "secret_key") os.Setenv("PATH", "/usr/local/bin:/usr/bin:/bin:/root/bin") defer os.Unsetenv("TEST_SAFE_VAR") defer os.Unsetenv("AWS_SECRET_ACCESS_KEY") client := NewMCPClient("test", "echo", []string{}, nil) env := client.buildSecureEnvironment() // Check that sensitive variables are filtered out for _, e := range env { require.False(t, strings.HasPrefix(e, "AWS_SECRET_ACCESS_KEY="), "Sensitive AWS_SECRET_ACCESS_KEY should be filtered out") require.False(t, strings.Contains(e, "/root/bin"), "Dangerous PATH component /root/bin should be filtered out") } // Check that PATH is present but sanitized hasPath := false for _, e := range env { if strings.HasPrefix(e, "PATH=") { hasPath = true require.NotContains(t, e, "/root", "PATH should not contain /root directories") } } require.True(t, hasPath, "PATH should be present in environment") } // TestMCPManagerAddServer tests adding MCP servers to the manager func TestMCPManagerAddServer(t *testing.T) { manager := NewMCPManager(5) // Test adding a valid server config config := api.MCPServerConfig{ Name: "test_server", Command: "python", Args: []string{"-m", "test_module"}, Env: map[string]string{"TEST": "value"}, } // This will fail in test environment but validates the validation logic err := manager.AddServer(config) if err != nil { require.Contains(t, err.Error(), "failed to initialize", "Expected initialization failure in test environment") } // Test invalid server names invalidConfigs := []api.MCPServerConfig{ {Name: "", Command: "python"}, // Empty name {Name: strings.Repeat("a", 101), Command: "python"}, // Too long {Name: "test/server", Command: "python"}, // Invalid characters } for _, cfg := range invalidConfigs { err := manager.validateServerConfig(cfg) require.Error(t, err, "Should reject invalid config: %+v", cfg) } } // TestDangerousCommandValidation tests rejection of dangerous commands func TestDangerousCommandValidation(t *testing.T) { manager := NewMCPManager(5) dangerousConfigs := []api.MCPServerConfig{ {Name: "test1", Command: "bash"}, {Name: "test2", Command: "/bin/sh"}, {Name: "test3", Command: "sudo"}, {Name: "test4", Command: "rm"}, {Name: "test5", Command: "curl"}, {Name: "test6", Command: "eval"}, } for _, cfg := range dangerousConfigs { err := manager.validateServerConfig(cfg) require.Error(t, err, "Should reject dangerous command: %s", cfg.Command) require.Contains(t, err.Error(), "not allowed for security", "Expected security error for command %s", cfg.Command) } // Test that safe commands are allowed safeConfigs := []api.MCPServerConfig{ {Name: "test1", Command: "python"}, {Name: "test2", Command: "node"}, {Name: "test3", Command: "/usr/bin/python3"}, } for _, cfg := range safeConfigs { err := manager.validateServerConfig(cfg) require.NoError(t, err, "Should allow safe command %s", cfg.Command) } } // TestShellInjectionPrevention tests prevention of shell injection func TestShellInjectionPrevention(t *testing.T) { manager := NewMCPManager(5) // Test arguments with shell metacharacters injectionConfigs := []api.MCPServerConfig{ { Name: "test1", Command: "python", Args: []string{"; rm -rf /"}, }, { Name: "test2", Command: "python", Args: []string{"test", "| cat /etc/passwd"}, }, { Name: "test3", Command: "python", Args: []string{"$(whoami)"}, }, { Name: "test4", Command: "python", Args: []string{"`id`"}, }, } for _, cfg := range injectionConfigs { err := manager.validateServerConfig(cfg) require.Error(t, err, "Should reject shell injection attempt in args: %v", cfg.Args) require.Contains(t, err.Error(), "shell metacharacters", "Expected shell metacharacter error") } } // TestParallelToolExecution tests parallel execution of tools func TestParallelToolExecution(t *testing.T) { manager := NewMCPManager(5) // Create test tool calls toolCalls := []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "tool1", Arguments: map[string]interface{}{"test": "1"}, }, }, { Function: api.ToolCallFunction{ Name: "tool2", Arguments: map[string]interface{}{"test": "2"}, }, }, { Function: api.ToolCallFunction{ Name: "tool3", Arguments: map[string]interface{}{"test": "3"}, }, }, } // Execute in parallel (will fail but tests the mechanism) results := manager.ExecuteToolsParallel(toolCalls) require.Len(t, results, len(toolCalls)) // All should have errors since no MCP servers are connected for i, result := range results { require.Error(t, result.Error, "Expected error for tool call %d", i) } } // TestMCPClientTimeout tests timeout handling for tool execution func TestMCPClientTimeout(t *testing.T) { client := NewMCPClient("test", "sleep", []string{"60"}, nil) // Create a context with very short timeout ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel() // Try to call with timeout (will fail but tests the mechanism) req := mcpCallToolRequest{ Name: "test_tool", Arguments: map[string]interface{}{}, } var resp mcpCallToolResponse err := client.callWithContext(ctx, "tools/call", req, &resp) // Should timeout or fail require.Error(t, err, "Expected timeout or error") } // TestEnvironmentVariableValidation tests validation of environment variables func TestEnvironmentVariableValidation(t *testing.T) { manager := NewMCPManager(5) // Test invalid environment variable names invalidEnvConfigs := []api.MCPServerConfig{ { Name: "test1", Command: "python", Env: map[string]string{"VAR=BAD": "value"}, }, { Name: "test2", Command: "python", Env: map[string]string{"VAR;CMD": "value"}, }, { Name: "test3", Command: "python", Env: map[string]string{"VAR|PIPE": "value"}, }, } for _, cfg := range invalidEnvConfigs { err := manager.validateServerConfig(cfg) require.Error(t, err, "Should reject invalid environment variable names: %v", cfg.Env) } // Test valid environment variables validConfig := api.MCPServerConfig{ Name: "test", Command: "python", Env: map[string]string{ "PYTHONPATH": "/usr/lib/python3", "MY_VAR": "value", "TEST_123": "test", }, } err := manager.validateServerConfig(validConfig) require.NoError(t, err, "Should allow valid environment variables") } // BenchmarkToolExecution benchmarks tool execution performance func BenchmarkToolExecution(b *testing.B) { manager := NewMCPManager(10) toolCall := api.ToolCall{ Function: api.ToolCallFunction{ Name: "test_tool", Arguments: map[string]interface{}{"param": "value"}, }, } b.ResetTimer() for i := 0; i < b.N; i++ { _ = manager.ExecuteTool(toolCall) } } // BenchmarkParallelToolExecution benchmarks parallel tool execution func BenchmarkParallelToolExecution(b *testing.B) { manager := NewMCPManager(10) toolCalls := make([]api.ToolCall, 10) for i := range toolCalls { toolCalls[i] = api.ToolCall{ Function: api.ToolCallFunction{ Name: fmt.Sprintf("tool_%d", i), Arguments: map[string]interface{}{"param": i}, }, } } b.ResetTimer() for i := 0; i < b.N; i++ { _ = manager.ExecuteToolsParallel(toolCalls) } } // ============================================================================= // Auto-Enable Unit Tests // ============================================================================= // TestAutoEnableMode_Never verifies servers with auto_enable:"never" don't auto-enable func TestAutoEnableMode_Never(t *testing.T) { defs := &MCPDefinitions{ Servers: map[string]MCPServerDefinition{ "never_server": { Name: "never_server", Command: "python", AutoEnable: AutoEnableNever, }, "empty_mode": { Name: "empty_mode", Command: "python", // AutoEnable not set - defaults to never }, }, } ctx := AutoEnableContext{ToolsPath: "/some/path"} servers := defs.GetAutoEnableServers(ctx) require.Empty(t, servers, "Expected 0 auto-enabled servers for 'never' mode") } // TestAutoEnableMode_Always verifies servers with auto_enable:"always" always enable func TestAutoEnableMode_Always(t *testing.T) { defs := &MCPDefinitions{ Servers: map[string]MCPServerDefinition{ "always_server": { Name: "always_server", Command: "python", Args: []string{"-m", "server"}, AutoEnable: AutoEnableAlways, }, }, } // Should enable even with empty path ctx := AutoEnableContext{ToolsPath: ""} servers := defs.GetAutoEnableServers(ctx) require.Len(t, servers, 1) require.Equal(t, "always_server", servers[0].Name) // Should also enable with path ctx = AutoEnableContext{ToolsPath: "/tmp"} servers = defs.GetAutoEnableServers(ctx) require.Len(t, servers, 1) } // TestAutoEnableMode_WithPath verifies servers with auto_enable:"with_path" enable only when path is provided func TestAutoEnableMode_WithPath(t *testing.T) { tmpDir, err := os.MkdirTemp("", "mcp-test-*") require.NoError(t, err) defer os.RemoveAll(tmpDir) defs := &MCPDefinitions{ Servers: map[string]MCPServerDefinition{ "path_server": { Name: "path_server", Command: "python", Args: []string{"-m", "server"}, RequiresPath: true, PathArgIndex: -1, AutoEnable: AutoEnableWithPath, }, }, } // Should NOT enable without path ctx := AutoEnableContext{ToolsPath: ""} servers := defs.GetAutoEnableServers(ctx) require.Empty(t, servers, "Expected 0 servers without path") // Should enable with valid path ctx = AutoEnableContext{ToolsPath: tmpDir} servers = defs.GetAutoEnableServers(ctx) require.Len(t, servers, 1) // Verify path was appended to args expectedArgs := []string{"-m", "server", tmpDir} require.Equal(t, expectedArgs, servers[0].Args) } // TestAutoEnableMode_IfMatch_FileExists verifies file_exists condition func TestAutoEnableMode_IfMatch_FileExists(t *testing.T) { tmpDir, err := os.MkdirTemp("", "mcp-test-*") require.NoError(t, err) defer os.RemoveAll(tmpDir) // Create .git directory to simulate git repo gitDir := filepath.Join(tmpDir, ".git") require.NoError(t, os.Mkdir(gitDir, 0755)) defs := &MCPDefinitions{ Servers: map[string]MCPServerDefinition{ "git_server": { Name: "git_server", Command: "python", Args: []string{"-m", "git_server"}, RequiresPath: true, PathArgIndex: -1, AutoEnable: AutoEnableIfMatch, EnableIf: EnableCondition{FileExists: ".git"}, }, }, } // Should enable when .git exists ctx := AutoEnableContext{ToolsPath: tmpDir} servers := defs.GetAutoEnableServers(ctx) require.Len(t, servers, 1, "Expected 1 server when .git exists") // Should NOT enable in directory without .git noGitDir, err := os.MkdirTemp("", "mcp-test-nogit-*") require.NoError(t, err) defer os.RemoveAll(noGitDir) ctx = AutoEnableContext{ToolsPath: noGitDir} servers = defs.GetAutoEnableServers(ctx) require.Empty(t, servers, "Expected 0 servers without .git") } // TestAutoEnableMode_IfMatch_EnvSet verifies env_set condition func TestAutoEnableMode_IfMatch_EnvSet(t *testing.T) { defs := &MCPDefinitions{ Servers: map[string]MCPServerDefinition{ "env_server": { Name: "env_server", Command: "python", AutoEnable: AutoEnableIfMatch, EnableIf: EnableCondition{EnvSet: "MCP_TEST_VAR"}, }, }, } // Test with env in context ctx := AutoEnableContext{ ToolsPath: "", Env: map[string]string{"MCP_TEST_VAR": "some_value"}, } servers := defs.GetAutoEnableServers(ctx) require.Len(t, servers, 1, "Expected 1 server when env is set in context") // Test with env NOT set ctx = AutoEnableContext{ ToolsPath: "", Env: map[string]string{}, } servers = defs.GetAutoEnableServers(ctx) require.Empty(t, servers, "Expected 0 servers when env not set") // Test with os.Getenv fallback os.Setenv("MCP_TEST_VAR_FALLBACK", "fallback_value") defer os.Unsetenv("MCP_TEST_VAR_FALLBACK") defsFallback := &MCPDefinitions{ Servers: map[string]MCPServerDefinition{ "env_server": { Name: "env_server", Command: "python", AutoEnable: AutoEnableIfMatch, EnableIf: EnableCondition{EnvSet: "MCP_TEST_VAR_FALLBACK"}, }, }, } ctx = AutoEnableContext{ToolsPath: "", Env: nil} servers = defsFallback.GetAutoEnableServers(ctx) require.Len(t, servers, 1, "Expected 1 server with os.Getenv fallback") } // TestAutoEnableMode_IfMatch_CombinedConditions verifies AND logic for conditions func TestAutoEnableMode_IfMatch_CombinedConditions(t *testing.T) { tmpDir, err := os.MkdirTemp("", "mcp-test-*") require.NoError(t, err) defer os.RemoveAll(tmpDir) markerFile := filepath.Join(tmpDir, ".marker") require.NoError(t, os.WriteFile(markerFile, []byte("test"), 0644)) defs := &MCPDefinitions{ Servers: map[string]MCPServerDefinition{ "combined_server": { Name: "combined_server", Command: "python", RequiresPath: true, PathArgIndex: -1, AutoEnable: AutoEnableIfMatch, EnableIf: EnableCondition{ FileExists: ".marker", EnvSet: "MCP_COMBINED_TEST", }, }, }, } // Should NOT enable when only file exists ctx := AutoEnableContext{ ToolsPath: tmpDir, Env: map[string]string{}, } servers := defs.GetAutoEnableServers(ctx) require.Empty(t, servers, "Expected 0 servers when only file condition matches") // Should NOT enable when only env is set ctx = AutoEnableContext{ ToolsPath: "/nonexistent", Env: map[string]string{"MCP_COMBINED_TEST": "value"}, } servers = defs.GetAutoEnableServers(ctx) require.Empty(t, servers, "Expected 0 servers when only env condition matches") // Should enable when BOTH conditions match ctx = AutoEnableContext{ ToolsPath: tmpDir, Env: map[string]string{"MCP_COMBINED_TEST": "value"}, } servers = defs.GetAutoEnableServers(ctx) require.Len(t, servers, 1, "Expected 1 server when both conditions match") } // TestGetAutoEnableServers_MultipleServers verifies multiple servers can auto-enable func TestGetAutoEnableServers_MultipleServers(t *testing.T) { tmpDir, err := os.MkdirTemp("", "mcp-test-*") require.NoError(t, err) defer os.RemoveAll(tmpDir) // Create .git directory require.NoError(t, os.Mkdir(filepath.Join(tmpDir, ".git"), 0755)) defs := &MCPDefinitions{ Servers: map[string]MCPServerDefinition{ "filesystem": { Name: "filesystem", Command: "npx", Args: []string{"-y", "@mcp/server-filesystem"}, RequiresPath: true, PathArgIndex: -1, AutoEnable: AutoEnableWithPath, }, "git": { Name: "git", Command: "python", Args: []string{"-m", "mcp_git"}, RequiresPath: true, PathArgIndex: -1, AutoEnable: AutoEnableIfMatch, EnableIf: EnableCondition{FileExists: ".git"}, }, "never_server": { Name: "never_server", Command: "python", AutoEnable: AutoEnableNever, }, }, } ctx := AutoEnableContext{ToolsPath: tmpDir} servers := defs.GetAutoEnableServers(ctx) require.Len(t, servers, 2, "Expected 2 auto-enabled servers") // Verify both filesystem and git are enabled names := make(map[string]bool) for _, s := range servers { names[s.Name] = true } require.True(t, names["filesystem"], "Expected 'filesystem' server to be auto-enabled") require.True(t, names["git"], "Expected 'git' server to be auto-enabled") require.False(t, names["never_server"], "'never_server' should NOT be auto-enabled") } // TestBuildConfigForAutoEnable_PathArgIndex verifies path insertion at different positions func TestBuildConfigForAutoEnable_PathArgIndex(t *testing.T) { tmpDir, err := os.MkdirTemp("", "mcp-test-*") require.NoError(t, err) defer os.RemoveAll(tmpDir) testCases := []struct { name string args []string pathArgIndex int expected []string }{ { name: "append at end (index -1)", args: []string{"arg1", "arg2"}, pathArgIndex: -1, expected: []string{"arg1", "arg2", tmpDir}, }, { name: "insert at beginning (index 0)", args: []string{"arg1", "arg2"}, pathArgIndex: 0, expected: []string{tmpDir, "arg1", "arg2"}, }, { name: "insert in middle (index 1)", args: []string{"arg1", "arg2"}, pathArgIndex: 1, expected: []string{"arg1", tmpDir, "arg2"}, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { defs := &MCPDefinitions{ Servers: map[string]MCPServerDefinition{ "test": { Name: "test", Command: "python", Args: tc.args, RequiresPath: true, PathArgIndex: tc.pathArgIndex, AutoEnable: AutoEnableWithPath, }, }, } ctx := AutoEnableContext{ToolsPath: tmpDir} servers := defs.GetAutoEnableServers(ctx) require.Len(t, servers, 1) require.Equal(t, tc.expected, servers[0].Args) }) } } // TestBuildConfigForAutoEnable_InvalidPath verifies error handling for invalid paths func TestBuildConfigForAutoEnable_InvalidPath(t *testing.T) { defs := &MCPDefinitions{ Servers: map[string]MCPServerDefinition{ "path_server": { Name: "path_server", Command: "python", RequiresPath: true, AutoEnable: AutoEnableWithPath, }, }, } // Should fail with non-existent path ctx := AutoEnableContext{ToolsPath: "/definitely/not/a/real/path/12345"} servers := defs.GetAutoEnableServers(ctx) // Server should be skipped due to invalid path require.Empty(t, servers, "Expected 0 servers with invalid path") } // TestBuildConfigForAutoEnable_EnvCopy verifies environment variables are copied func TestBuildConfigForAutoEnable_EnvCopy(t *testing.T) { defs := &MCPDefinitions{ Servers: map[string]MCPServerDefinition{ "env_server": { Name: "env_server", Command: "python", AutoEnable: AutoEnableAlways, Env: map[string]string{ "VAR1": "value1", "VAR2": "value2", }, }, }, } ctx := AutoEnableContext{} servers := defs.GetAutoEnableServers(ctx) require.Len(t, servers, 1) require.Equal(t, "value1", servers[0].Env["VAR1"]) require.Equal(t, "value2", servers[0].Env["VAR2"]) // Verify original wasn't mutated by modifying copy servers[0].Env["VAR1"] = "modified" original := defs.Servers["env_server"] require.Equal(t, "value1", original.Env["VAR1"], "Original server definition was mutated") } // TestEnableCondition_EmptyConditions verifies empty conditions always match func TestEnableCondition_EmptyConditions(t *testing.T) { defs := &MCPDefinitions{ Servers: map[string]MCPServerDefinition{ "empty_cond": { Name: "empty_cond", Command: "python", AutoEnable: AutoEnableIfMatch, EnableIf: EnableCondition{}, // Empty - should always match }, }, } ctx := AutoEnableContext{ToolsPath: ""} servers := defs.GetAutoEnableServers(ctx) require.Len(t, servers, 1, "Expected 1 server with empty conditions") }