From 10f84ebac9e248ad4896cf3ecf89c67598dadb46 Mon Sep 17 00:00:00 2001 From: Matvey Arye Date: Thu, 4 Dec 2025 17:29:35 +0100 Subject: [PATCH 1/4] Add public API for MCP server installation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces pkg/mcpinstall package that allows external projects to install MCP server configurations for various AI coding assistants. The API provides: - InstallForClient() function with configurable ServerName, Command, and Args - Client type constants (Cursor, ClaudeCode, VSCode, etc.) - Options struct for installation configuration Refactors internal implementation to separate generic InstallMCPForClient() from Tiger-specific installTigerMCPForClient() wrapper. šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- internal/tiger/cmd/mcp.go | 2 +- internal/tiger/cmd/mcp_install.go | 199 ++++++++++++++++--------- internal/tiger/cmd/mcp_install_test.go | 58 ++++--- pkg/mcpinstall/install.go | 47 ++++++ 4 files changed, 209 insertions(+), 97 deletions(-) create mode 100644 pkg/mcpinstall/install.go diff --git a/internal/tiger/cmd/mcp.go b/internal/tiger/cmd/mcp.go index 9ceeb232..f7c91917 100644 --- a/internal/tiger/cmd/mcp.go +++ b/internal/tiger/cmd/mcp.go @@ -107,7 +107,7 @@ Examples: clientName = args[0] } - return installMCPForClient(clientName, !noBackup, configPath) + return installTigerMCPForClient(clientName, !noBackup, configPath) }, } diff --git a/internal/tiger/cmd/mcp_install.go b/internal/tiger/cmd/mcp_install.go index 60c1a619..77833203 100644 --- a/internal/tiger/cmd/mcp_install.go +++ b/internal/tiger/cmd/mcp_install.go @@ -37,28 +37,44 @@ const ( KiroCLI MCPClient = "kiro-cli" ) -// TigerMCPServer represents the Tiger MCP server configuration -type TigerMCPServer struct { +// MCPServerConfig represents the MCP server configuration +type MCPServerConfig struct { Command string `json:"command"` Args []string `json:"args"` } +// InstallOptions configures the MCP server installation behavior +type InstallOptions struct { + // CreateBackup creates a backup of existing config files before modification (default: true for CLI) + CreateBackup bool + // CustomConfigPath overrides the default config file location + CustomConfigPath string + // ServerName is the name to register the MCP server as (default: "tiger") + ServerName string + // Command is the path to the MCP server binary (default: auto-detected) + Command string + // Args are the arguments to pass to the MCP server binary (default: ["mcp", "start"]) + Args []string +} + // clientConfig represents our own client configuration for Tiger MCP installation type clientConfig struct { ClientType MCPClient // Our internal client type Name string - EditorNames []string // Supported client names for this client - MCPServersPathPrefix string // JSON path prefix for MCP servers config (only for JSON config manipulation clients like Cursor/Windsurf) - ConfigPaths []string // Config file locations - used for backup on all clients, and for JSON manipulation on JSON-config clients - buildInstallCommand func(tigerPath string) []string // Function to build CLI install command (for CLI-based installation clients) + EditorNames []string // Supported client names for this client + MCPServersPathPrefix string // JSON path prefix for MCP servers config (only for JSON config manipulation clients like Cursor/Windsurf) + ConfigPaths []string // Config file locations - used for backup on all clients, and for JSON manipulation on JSON-config clients + // buildInstallCommand builds the CLI install command for CLI-based clients + // Parameters: serverName (name to register), command (binary path), args (arguments to binary) + buildInstallCommand func(serverName, command string, args []string) ([]string, error) } -// BuildInstallCommand constructs the install command with the given Tiger binary path -func (c *clientConfig) BuildInstallCommand(tigerPath string) []string { +// BuildInstallCommand constructs the install command with the given parameters +func (c *clientConfig) BuildInstallCommand(serverName, command string, args []string) ([]string, error) { if c.buildInstallCommand == nil { - return nil + return nil, nil } - return c.buildInstallCommand(tigerPath) + return c.buildInstallCommand(serverName, command, args) } // supportedClients defines the clients we support for Tiger MCP installation @@ -73,8 +89,8 @@ var supportedClients = []clientConfig{ ConfigPaths: []string{ "~/.claude.json", }, - buildInstallCommand: func(tigerPath string) []string { - return []string{"claude", "mcp", "add", "-s", "user", mcp.ServerName, tigerPath, "mcp", "start"} + buildInstallCommand: func(serverName, command string, args []string) ([]string, error) { + return append([]string{"claude", "mcp", "add", "-s", "user", serverName, command}, args...), nil }, }, { @@ -103,8 +119,8 @@ var supportedClients = []clientConfig{ "~/.codex/config.toml", "$CODEX_HOME/config.toml", }, - buildInstallCommand: func(tigerPath string) []string { - return []string{"codex", "mcp", "add", mcp.ServerName, tigerPath, "mcp", "start"} + buildInstallCommand: func(serverName, command string, args []string) ([]string, error) { + return append([]string{"codex", "mcp", "add", serverName, command}, args...), nil }, }, { @@ -114,8 +130,8 @@ var supportedClients = []clientConfig{ ConfigPaths: []string{ "~/.gemini/settings.json", }, - buildInstallCommand: func(tigerPath string) []string { - return []string{"gemini", "mcp", "add", "-s", "user", mcp.ServerName, tigerPath, "mcp", "start"} + buildInstallCommand: func(serverName, command string, args []string) ([]string, error) { + return append([]string{"gemini", "mcp", "add", "-s", "user", serverName, command}, args...), nil }, }, { @@ -127,8 +143,12 @@ var supportedClients = []clientConfig{ "~/Library/Application Support/Code/User/mcp.json", "~/AppData/Roaming/Code/User/mcp.json", }, - buildInstallCommand: func(tigerPath string) []string { - return []string{"code", "--add-mcp", fmt.Sprintf(`{"name":"%s","command":"%s","args":["mcp","start"]}`, mcp.ServerName, tigerPath)} + buildInstallCommand: func(serverName, command string, args []string) ([]string, error) { + argsJSON, err := json.Marshal(args) + if err != nil { + return nil, fmt.Errorf("failed to marshal args: %w", err) + } + return []string{"code", "--add-mcp", fmt.Sprintf(`{"name":"%s","command":"%s","args":%s}`, serverName, command, argsJSON)}, nil }, }, { @@ -147,8 +167,8 @@ var supportedClients = []clientConfig{ ConfigPaths: []string{ "~/.kiro/settings/mcp.json", }, - buildInstallCommand: func(tigerPath string) []string { - return []string{"kiro-cli", "mcp", "add", "--name", mcp.ServerName, "--command", tigerPath, "--args", "mcp,start"} + buildInstallCommand: func(serverName, command string, args []string) ([]string, error) { + return []string{"kiro-cli", "mcp", "add", "--name", serverName, "--command", command, "--args", strings.Join(args, ",")}, nil }, }, } @@ -162,8 +182,21 @@ func getValidEditorNames() []string { return validNames } -// installMCPForClient installs the Tiger MCP server configuration for the specified client -func installMCPForClient(clientName string, createBackup bool, customConfigPath string) error { +// InstallMCPForClient installs an MCP server configuration for the specified client. +// This is a generic, configurable function exported for use by external projects via pkg/mcpinstall. +// Required options: ServerName, Command, Args must all be provided. +func InstallMCPForClient(clientName string, opts InstallOptions) error { + // Validate required options + if opts.ServerName == "" { + return fmt.Errorf("ServerName is required") + } + if opts.Command == "" { + return fmt.Errorf("Command is required") + } + if opts.Args == nil { + return fmt.Errorf("Args is required") + } + // Find the client configuration by name clientCfg, err := findClientConfig(clientName) if err != nil { @@ -173,9 +206,9 @@ func installMCPForClient(clientName string, createBackup bool, customConfigPath mcpServersPathPrefix := clientCfg.MCPServersPathPrefix var configPath string - if customConfigPath != "" { + if opts.CustomConfigPath != "" { // Expand custom config path for ~ and environment variables, then use it directly - configPath = util.ExpandPath(customConfigPath) + configPath = util.ExpandPath(opts.CustomConfigPath) } else if len(clientCfg.ConfigPaths) > 0 { // Use manual config path discovery for clients with configured paths configPath, err = findClientConfigFile(clientCfg.ConfigPaths) @@ -188,33 +221,67 @@ func installMCPForClient(clientName string, createBackup bool, customConfigPath } // else: CLI-only client - configPath remains empty, will use buildInstallCommand - logging.Info("Installing Tiger MCP server configuration", + logging.Info("Installing MCP server configuration", zap.String("client", clientName), + zap.String("server_name", opts.ServerName), + zap.String("command", opts.Command), + zap.Strings("args", opts.Args), zap.String("config_path", configPath), zap.String("mcp_servers_path", mcpServersPathPrefix), - zap.Bool("create_backup", createBackup), + zap.Bool("create_backup", opts.CreateBackup), ) // Create backup if requested and we have a config file - var backupPath string - if createBackup && configPath != "" { - var err error - backupPath, err = createConfigBackup(configPath) + if opts.CreateBackup && configPath != "" { + _, err = createConfigBackup(configPath) if err != nil { return fmt.Errorf("failed to create backup: %w", err) } } - // Add Tiger MCP server to configuration + // Add MCP server to configuration if clientCfg.buildInstallCommand != nil { // Use CLI approach when install command builder is configured - if err := addTigerMCPServerViaCLI(clientCfg); err != nil { - return fmt.Errorf("failed to add Tiger MCP server configuration: %w", err) + if err := addMCPServerViaCLI(clientCfg, opts.ServerName, opts.Command, opts.Args); err != nil { + return fmt.Errorf("failed to add MCP server configuration: %w", err) } } else { // Use JSON patching approach for JSON-config clients - if err := addTigerMCPServerViaJSON(configPath, mcpServersPathPrefix); err != nil { - return fmt.Errorf("failed to add Tiger MCP server configuration: %w", err) + if err := addMCPServerViaJSON(configPath, mcpServersPathPrefix, opts.ServerName, opts.Command, opts.Args); err != nil { + return fmt.Errorf("failed to add MCP server configuration: %w", err) + } + } + + return nil +} + +// installTigerMCPForClient installs the Tiger MCP server configuration for the specified client. +// This is the Tiger-specific wrapper used by the CLI that handles defaults and success messages. +func installTigerMCPForClient(clientName string, createBackup bool, customConfigPath string) error { + // Get the Tiger executable path + command, err := getTigerExecutablePath() + if err != nil { + return fmt.Errorf("failed to get executable path: %w", err) + } + + opts := InstallOptions{ + CreateBackup: createBackup, + CustomConfigPath: customConfigPath, + ServerName: mcp.ServerName, + Command: command, + Args: []string{"mcp", "start"}, + } + + if err := InstallMCPForClient(clientName, opts); err != nil { + return err + } + + // Print Tiger-specific success messages + configPath := customConfigPath + if configPath == "" { + clientCfg, _ := findClientConfig(clientName) + if clientCfg != nil && len(clientCfg.ConfigPaths) > 0 { + configPath, _ = findClientConfigFile(clientCfg.ConfigPaths) } } @@ -225,10 +292,6 @@ func installMCPForClient(clientName string, createBackup bool, customConfigPath fmt.Printf("āš™ļø Configuration managed by %s\n", clientName) } - if createBackup && backupPath != "" { - fmt.Printf("šŸ’¾ Backup created: %s\n", backupPath) - } - fmt.Printf("\nšŸ’” Next steps:\n") fmt.Printf(" 1. Restart %s to load the new configuration\n", clientName) fmt.Printf(" 2. The Tiger MCP server will be available as '%s'\n", mcp.ServerName) @@ -465,22 +528,19 @@ func (m clientSelectModel) View() string { return s } -// addTigerMCPServerViaCLI adds Tiger MCP server using a CLI command configured in clientConfig -func addTigerMCPServerViaCLI(clientCfg *clientConfig) error { +// addMCPServerViaCLI adds an MCP server using a CLI command configured in clientConfig +func addMCPServerViaCLI(clientCfg *clientConfig, serverName, command string, args []string) error { if clientCfg.buildInstallCommand == nil { return fmt.Errorf("no install command configured for client %s", clientCfg.Name) } - // Get the full path of the currently executing Tiger binary - tigerPath, err := getTigerExecutablePath() + // Build the install command with the provided parameters + installCommand, err := clientCfg.BuildInstallCommand(serverName, command, args) if err != nil { - return fmt.Errorf("failed to get Tiger executable path: %w", err) + return fmt.Errorf("failed to build install command: %w", err) } - // Build the install command with the full Tiger path - installCommand := clientCfg.BuildInstallCommand(tigerPath) - - logging.Info("Adding Tiger MCP server using CLI", + logging.Info("Adding MCP server using CLI", zap.String("client", clientCfg.Name), zap.Strings("command", installCommand)) @@ -490,14 +550,14 @@ func addTigerMCPServerViaCLI(clientCfg *clientConfig) error { // Capture output output, err := cmd.CombinedOutput() if err != nil { - command := strings.Join(installCommand, " ") + cmdStr := strings.Join(installCommand, " ") if string(output) != "" { - return fmt.Errorf("failed to run %s installation command: %w\nCommand: %s\nOutput: %s", clientCfg.Name, err, command, string(output)) + return fmt.Errorf("failed to run %s installation command: %w\nCommand: %s\nOutput: %s", clientCfg.Name, err, cmdStr, string(output)) } - return fmt.Errorf("failed to run %s installation command: %w\nCommand: %s", clientCfg.Name, err, command) + return fmt.Errorf("failed to run %s installation command: %w\nCommand: %s", clientCfg.Name, err, cmdStr) } - logging.Info("Successfully added Tiger MCP server via CLI", + logging.Info("Successfully added MCP server via CLI", zap.String("client", clientCfg.Name), zap.String("output", string(output))) @@ -537,23 +597,18 @@ func createConfigBackup(configPath string) (string, error) { return backupPath, nil } -// addTigerMCPServerViaJSON adds the Tiger MCP server to the configuration file using JSON patching -func addTigerMCPServerViaJSON(configPath string, mcpServersPathPrefix string) error { +// addMCPServerViaJSON adds an MCP server to the configuration file using JSON patching +func addMCPServerViaJSON(configPath, mcpServersPathPrefix, serverName, command string, args []string) error { // Create configuration directory if it doesn't exist configDir := filepath.Dir(configPath) if err := os.MkdirAll(configDir, 0755); err != nil { return fmt.Errorf("failed to create configuration directory %s: %w", configDir, err) } - // Get the full path of the currently executing Tiger binary - tigerPath, err := getTigerExecutablePath() - if err != nil { - return fmt.Errorf("failed to get Tiger executable path: %w", err) - } - // Tiger MCP server configuration - tigerServer := TigerMCPServer{ - Command: tigerPath, - Args: []string{"mcp", "start"}, + // MCP server configuration + serverConfig := MCPServerConfig{ + Command: command, + Args: args, } // Get original file mode to preserve it, fallback to 0600 for new files @@ -593,14 +648,14 @@ func addTigerMCPServerViaJSON(configPath string, mcpServersPathPrefix string) er } } - // Marshal the Tiger MCP server data - dataJSON, err := json.Marshal(tigerServer) + // Marshal the MCP server data + dataJSON, err := json.Marshal(serverConfig) if err != nil { - return fmt.Errorf("failed to marshal Tiger MCP server data: %w", err) + return fmt.Errorf("failed to marshal MCP server data: %w", err) } - // Create JSON patch to add the Tiger MCP server - patch := fmt.Sprintf(`[{ "op": "add", "path": "%s/%s", "value": %s }]`, mcpServersPathPrefix, mcp.ServerName, dataJSON) + // Create JSON patch to add the MCP server + patch := fmt.Sprintf(`[{ "op": "add", "path": "%s/%s", "value": %s }]`, mcpServersPathPrefix, serverName, dataJSON) // Apply the patch if err := value.Patch([]byte(patch)); err != nil { @@ -618,10 +673,10 @@ func addTigerMCPServerViaJSON(configPath string, mcpServersPathPrefix string) er return fmt.Errorf("failed to write config file: %w", err) } - logging.Info("Added Tiger MCP server to configuration", - zap.String("server_name", mcp.ServerName), - zap.String("command", tigerServer.Command), - zap.Strings("args", tigerServer.Args), + logging.Info("Added MCP server to configuration", + zap.String("server_name", serverName), + zap.String("command", serverConfig.Command), + zap.Strings("args", serverConfig.Args), ) return nil diff --git a/internal/tiger/cmd/mcp_install_test.go b/internal/tiger/cmd/mcp_install_test.go index 87e8d851..d351ea89 100644 --- a/internal/tiger/cmd/mcp_install_test.go +++ b/internal/tiger/cmd/mcp_install_test.go @@ -245,8 +245,12 @@ func TestAddTigerMCPServer(t *testing.T) { err := os.WriteFile(configPath, []byte(tt.initialConfig), 0644) require.NoError(t, err) + // Get the executable path (uses mocked function) + execPath, err := getTigerExecutablePath() + require.NoError(t, err) + // Call the function under test - err = addTigerMCPServerViaJSON(configPath, tt.mcpServersPathPrefix) + err = addMCPServerViaJSON(configPath, tt.mcpServersPathPrefix, "tiger", execPath, []string{"mcp", "start"}) if tt.expectError { assert.Error(t, err) @@ -293,7 +297,9 @@ func TestAddTigerMCPServerFileOperations(t *testing.T) { _, err := os.Stat(filepath.Dir(configPath)) assert.True(t, os.IsNotExist(err)) - err = addTigerMCPServerViaJSON(configPath, "/mcpServers") + execPath, err := getTigerExecutablePath() + require.NoError(t, err) + err = addMCPServerViaJSON(configPath, "/mcpServers", "tiger", execPath, []string{"mcp", "start"}) require.NoError(t, err) // Directory should now exist @@ -309,7 +315,9 @@ func TestAddTigerMCPServerFileOperations(t *testing.T) { tempDir := t.TempDir() configPath := filepath.Join(tempDir, "nonexistent.json") - err := addTigerMCPServerViaJSON(configPath, "/mcpServers") + execPath, err := getTigerExecutablePath() + require.NoError(t, err) + err = addMCPServerViaJSON(configPath, "/mcpServers", "tiger", execPath, []string{"mcp", "start"}) require.NoError(t, err) // File should now exist with correct content @@ -339,7 +347,9 @@ func TestAddTigerMCPServerFileOperations(t *testing.T) { err := os.WriteFile(configPath, []byte(""), 0644) require.NoError(t, err) - err = addTigerMCPServerViaJSON(configPath, "/mcpServers") + execPath, err := getTigerExecutablePath() + require.NoError(t, err) + err = addMCPServerViaJSON(configPath, "/mcpServers", "tiger", execPath, []string{"mcp", "start"}) require.NoError(t, err) // File should now have correct content @@ -699,7 +709,7 @@ func TestFindClientConfig(t *testing.T) { }) } -func TestAddTigerMCPServerViaCLI(t *testing.T) { +func TestAddMCPServerViaCLI(t *testing.T) { t.Run("returns error when no install command configured", func(t *testing.T) { clientCfg := &clientConfig{ ClientType: "test-client", @@ -707,7 +717,7 @@ func TestAddTigerMCPServerViaCLI(t *testing.T) { buildInstallCommand: nil, // No build function } - err := addTigerMCPServerViaCLI(clientCfg) + err := addMCPServerViaCLI(clientCfg, "tiger", "/path/to/tiger", []string{"mcp", "start"}) assert.Error(t, err, "should error when no install command configured") assert.Contains(t, err.Error(), "no install command configured for client Test Client", "error should mention missing install command") }) @@ -717,12 +727,12 @@ func TestAddTigerMCPServerViaCLI(t *testing.T) { clientCfg := &clientConfig{ ClientType: "test-client", Name: "Test Client", - buildInstallCommand: func(tigerPath string) []string { - return []string{"nonexistent-command-12345", "arg1", "arg2"} + buildInstallCommand: func(serverName, command string, args []string) ([]string, error) { + return []string{"nonexistent-command-12345", "arg1", "arg2"}, nil }, } - err := addTigerMCPServerViaCLI(clientCfg) + err := addMCPServerViaCLI(clientCfg, "tiger", "/path/to/tiger", []string{"mcp", "start"}) // We expect this to fail since the command doesn't exist, but it shows we got past validation assert.Error(t, err, "should error when command execution fails") assert.Contains(t, err.Error(), "failed to run Test Client installation command", "error should mention installation command failure") @@ -732,12 +742,12 @@ func TestAddTigerMCPServerViaCLI(t *testing.T) { clientCfg := &clientConfig{ ClientType: "test-client", Name: "Test Client", - buildInstallCommand: func(tigerPath string) []string { - return []string{"echo"} // Command with no args - should work + buildInstallCommand: func(serverName, command string, args []string) ([]string, error) { + return []string{"echo"}, nil // Command with no args - should work }, } - err := addTigerMCPServerViaCLI(clientCfg) + err := addMCPServerViaCLI(clientCfg, "tiger", "/path/to/tiger", []string{"mcp", "start"}) // echo command should succeed assert.NoError(t, err, "should not error for valid echo command") }) @@ -746,12 +756,12 @@ func TestAddTigerMCPServerViaCLI(t *testing.T) { clientCfg := &clientConfig{ ClientType: "test-client", Name: "Test Client", - buildInstallCommand: func(tigerPath string) []string { - return []string{"echo", "test", "output"} // Command with args + buildInstallCommand: func(serverName, command string, args []string) ([]string, error) { + return []string{"echo", "test", "output"}, nil // Command with args }, } - err := addTigerMCPServerViaCLI(clientCfg) + err := addMCPServerViaCLI(clientCfg, "tiger", "/path/to/tiger", []string{"mcp", "start"}) // echo command should succeed assert.NoError(t, err, "should not error for valid echo command with args") }) @@ -850,9 +860,9 @@ func TestInstallMCPForEditor_Integration(t *testing.T) { err := os.WriteFile(configPath, []byte(initialConfig), 0644) require.NoError(t, err, "should create initial config file") - // Call installMCPForClient to install Tiger MCP server - err = installMCPForClient("cursor", false, configPath) - require.NoError(t, err, "installMCPForClient should succeed") + // Call installTigerMCPForClient to install Tiger MCP server + err = installTigerMCPForClient("cursor", false, configPath) + require.NoError(t, err, "installTigerMCPForClient should succeed") // Verify the config file was modified configContent, err := os.ReadFile(configPath) @@ -886,9 +896,9 @@ func TestInstallMCPForEditor_Integration(t *testing.T) { err := os.WriteFile(configPath, []byte(initialConfig), 0644) require.NoError(t, err) - // Call installMCPForClient with backup enabled for Cursor - err = installMCPForClient("cursor", true, configPath) - require.NoError(t, err, "installMCPForClient should succeed with backup") + // Call installTigerMCPForClient with backup enabled for Cursor + err = installTigerMCPForClient("cursor", true, configPath) + require.NoError(t, err, "installTigerMCPForClient should succeed with backup") // Check that a backup file was created backupFiles, err := filepath.Glob(configPath + ".backup.*") @@ -916,7 +926,7 @@ func TestInstallMCPForEditor_Integration(t *testing.T) { }) t.Run("handles unsupported editor", func(t *testing.T) { - err := installMCPForClient("unsupported-editor", false, "") + err := installTigerMCPForClient("unsupported-editor", false, "") assert.Error(t, err, "should error for unsupported editor") assert.Contains(t, err.Error(), "unsupported client", "error should mention unsupported client") }) @@ -946,7 +956,7 @@ func TestInstallMCPForEditor_Integration(t *testing.T) { require.NoError(t, err) // First installation (should update existing tiger entry) - err = installMCPForClient("cursor", false, configPath) + err = installTigerMCPForClient("cursor", false, configPath) require.NoError(t, err, "first installation should succeed") // Read config after first installation @@ -971,7 +981,7 @@ func TestInstallMCPForEditor_Integration(t *testing.T) { assert.Equal(t, "start", args[1], "second arg should be 'start'") // Second installation (should be idempotent, no changes) - err = installMCPForClient("cursor", false, configPath) + err = installTigerMCPForClient("cursor", false, configPath) require.NoError(t, err, "second installation should succeed") // Read config after second installation diff --git a/pkg/mcpinstall/install.go b/pkg/mcpinstall/install.go new file mode 100644 index 00000000..b912a57d --- /dev/null +++ b/pkg/mcpinstall/install.go @@ -0,0 +1,47 @@ +// Package mcpinstall provides a public API for installing MCP server configurations +// for various AI coding assistants and editors. +package mcpinstall + +import ( + "github.com/timescale/tiger-cli/internal/tiger/cmd" +) + +// Options configures the MCP server installation behavior. +type Options = cmd.InstallOptions + +// Client represents supported MCP client types. +type Client = cmd.MCPClient + +// Supported MCP clients. +const ( + ClaudeCode Client = cmd.ClaudeCode + Cursor Client = cmd.Cursor + Windsurf Client = cmd.Windsurf + Codex Client = cmd.Codex + Gemini Client = cmd.Gemini + VSCode Client = cmd.VSCode + Antigravity Client = cmd.Antigravity + KiroCLI Client = cmd.KiroCLI +) + +// InstallForClient installs an MCP server configuration for the specified client. +// +// Parameters: +// - clientName: The name of the client to configure (e.g., "claude-code", "cursor", "windsurf") +// - opts: Configuration options for the installation +// +// Required options: +// - ServerName: The name to register the MCP server as (e.g., "my-mcp-server") +// - Command: Path to the MCP server binary (e.g., "/usr/local/bin/my-server") +// - Args: Arguments to pass to the MCP server binary (e.g., []string{"serve", "--port", "8080"}) +// +// Optional fields: +// - CreateBackup: If true, creates a backup of the existing config file before modification +// - CustomConfigPath: Custom path to the config file (empty string uses default location) +// +// Supported clients: claude-code, cursor, windsurf, codex, gemini, vscode, antigravity, kiro-cli +// +// Returns an error if the client is not supported, required options are missing, or installation fails. +func InstallForClient(clientName string, opts Options) error { + return cmd.InstallMCPForClient(clientName, opts) +} From 19619ca703ac8d5f6e6322b793b569bbc790b42f Mon Sep 17 00:00:00 2001 From: Matvey Arye Date: Fri, 5 Dec 2025 10:34:54 +0100 Subject: [PATCH 2/4] Address PR review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix VSCode buildInstallCommand to marshal entire JSON struct including name, command, and args (not just args) - Fix InstallOptions comments to say "(required)" instead of incorrect "(default: ...)" mentions - Simplify tests to pass "tiger" string directly instead of roundabout getTigerExecutablePath() calls - Remove unused Client type and constants from public package since InstallForClient takes clientName string, not Client type šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- internal/tiger/cmd/mcp_install.go | 18 ++++++++----- internal/tiger/cmd/mcp_install_test.go | 36 +++----------------------- pkg/mcpinstall/install.go | 15 ----------- 3 files changed, 15 insertions(+), 54 deletions(-) diff --git a/internal/tiger/cmd/mcp_install.go b/internal/tiger/cmd/mcp_install.go index 77833203..e8b3efbc 100644 --- a/internal/tiger/cmd/mcp_install.go +++ b/internal/tiger/cmd/mcp_install.go @@ -45,15 +45,15 @@ type MCPServerConfig struct { // InstallOptions configures the MCP server installation behavior type InstallOptions struct { - // CreateBackup creates a backup of existing config files before modification (default: true for CLI) + // CreateBackup creates a backup of existing config files before modification CreateBackup bool // CustomConfigPath overrides the default config file location CustomConfigPath string - // ServerName is the name to register the MCP server as (default: "tiger") + // ServerName is the name to register the MCP server as (required) ServerName string - // Command is the path to the MCP server binary (default: auto-detected) + // Command is the path to the MCP server binary (required) Command string - // Args are the arguments to pass to the MCP server binary (default: ["mcp", "start"]) + // Args are the arguments to pass to the MCP server binary (required) Args []string } @@ -144,11 +144,15 @@ var supportedClients = []clientConfig{ "~/AppData/Roaming/Code/User/mcp.json", }, buildInstallCommand: func(serverName, command string, args []string) ([]string, error) { - argsJSON, err := json.Marshal(args) + j, err := json.Marshal(map[string]any{ + "name": serverName, + "command": command, + "args": args, + }) if err != nil { - return nil, fmt.Errorf("failed to marshal args: %w", err) + return nil, fmt.Errorf("failed to marshal MCP config: %w", err) } - return []string{"code", "--add-mcp", fmt.Sprintf(`{"name":"%s","command":"%s","args":%s}`, serverName, command, argsJSON)}, nil + return []string{"code", "--add-mcp", string(j)}, nil }, }, { diff --git a/internal/tiger/cmd/mcp_install_test.go b/internal/tiger/cmd/mcp_install_test.go index d351ea89..99b45fdf 100644 --- a/internal/tiger/cmd/mcp_install_test.go +++ b/internal/tiger/cmd/mcp_install_test.go @@ -127,15 +127,6 @@ func TestFindClientConfigFileEquivalentToToolhive(t *testing.T) { } func TestAddTigerMCPServer(t *testing.T) { - // Override getTigerExecutablePath to return "tiger" for tests - oldFunc := tigerExecutablePathFunc - tigerExecutablePathFunc = func() (string, error) { - return "tiger", nil - } - defer func() { - tigerExecutablePathFunc = oldFunc - }() - tests := []struct { name string initialConfig string @@ -245,12 +236,8 @@ func TestAddTigerMCPServer(t *testing.T) { err := os.WriteFile(configPath, []byte(tt.initialConfig), 0644) require.NoError(t, err) - // Get the executable path (uses mocked function) - execPath, err := getTigerExecutablePath() - require.NoError(t, err) - // Call the function under test - err = addMCPServerViaJSON(configPath, tt.mcpServersPathPrefix, "tiger", execPath, []string{"mcp", "start"}) + err = addMCPServerViaJSON(configPath, tt.mcpServersPathPrefix, "tiger", "tiger", []string{"mcp", "start"}) if tt.expectError { assert.Error(t, err) @@ -280,15 +267,6 @@ func TestAddTigerMCPServer(t *testing.T) { } func TestAddTigerMCPServerFileOperations(t *testing.T) { - // Override getTigerExecutablePath to return "tiger" for tests - oldFunc := tigerExecutablePathFunc - tigerExecutablePathFunc = func() (string, error) { - return "tiger", nil - } - defer func() { - tigerExecutablePathFunc = oldFunc - }() - t.Run("creates directory if it doesn't exist", func(t *testing.T) { tempDir := t.TempDir() configPath := filepath.Join(tempDir, "nested", "dir", "config.json") @@ -297,9 +275,7 @@ func TestAddTigerMCPServerFileOperations(t *testing.T) { _, err := os.Stat(filepath.Dir(configPath)) assert.True(t, os.IsNotExist(err)) - execPath, err := getTigerExecutablePath() - require.NoError(t, err) - err = addMCPServerViaJSON(configPath, "/mcpServers", "tiger", execPath, []string{"mcp", "start"}) + err = addMCPServerViaJSON(configPath, "/mcpServers", "tiger", "tiger", []string{"mcp", "start"}) require.NoError(t, err) // Directory should now exist @@ -315,9 +291,7 @@ func TestAddTigerMCPServerFileOperations(t *testing.T) { tempDir := t.TempDir() configPath := filepath.Join(tempDir, "nonexistent.json") - execPath, err := getTigerExecutablePath() - require.NoError(t, err) - err = addMCPServerViaJSON(configPath, "/mcpServers", "tiger", execPath, []string{"mcp", "start"}) + err := addMCPServerViaJSON(configPath, "/mcpServers", "tiger", "tiger", []string{"mcp", "start"}) require.NoError(t, err) // File should now exist with correct content @@ -347,9 +321,7 @@ func TestAddTigerMCPServerFileOperations(t *testing.T) { err := os.WriteFile(configPath, []byte(""), 0644) require.NoError(t, err) - execPath, err := getTigerExecutablePath() - require.NoError(t, err) - err = addMCPServerViaJSON(configPath, "/mcpServers", "tiger", execPath, []string{"mcp", "start"}) + err = addMCPServerViaJSON(configPath, "/mcpServers", "tiger", "tiger", []string{"mcp", "start"}) require.NoError(t, err) // File should now have correct content diff --git a/pkg/mcpinstall/install.go b/pkg/mcpinstall/install.go index b912a57d..59731861 100644 --- a/pkg/mcpinstall/install.go +++ b/pkg/mcpinstall/install.go @@ -9,21 +9,6 @@ import ( // Options configures the MCP server installation behavior. type Options = cmd.InstallOptions -// Client represents supported MCP client types. -type Client = cmd.MCPClient - -// Supported MCP clients. -const ( - ClaudeCode Client = cmd.ClaudeCode - Cursor Client = cmd.Cursor - Windsurf Client = cmd.Windsurf - Codex Client = cmd.Codex - Gemini Client = cmd.Gemini - VSCode Client = cmd.VSCode - Antigravity Client = cmd.Antigravity - KiroCLI Client = cmd.KiroCLI -) - // InstallForClient installs an MCP server configuration for the specified client. // // Parameters: From 531a6f0f73408a744a6f39a279e2f3ff50b1d3e9 Mon Sep 17 00:00:00 2001 From: Matvey Arye Date: Fri, 5 Dec 2025 10:50:35 +0100 Subject: [PATCH 3/4] Move ClientName into InstallOptions struct MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per PR review feedback, move clientName from a separate function parameter into the InstallOptions struct. This keeps all configuration in one place. - Add ClientName field to InstallOptions (required) - Change InstallMCPForClient signature from (clientName, opts) to just (opts) - Rename public API function from InstallForClient to Install - Reorder struct fields to group required fields together šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- internal/tiger/cmd/mcp_install.go | 30 ++++++++++++++++++------------ pkg/mcpinstall/install.go | 13 ++++--------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/internal/tiger/cmd/mcp_install.go b/internal/tiger/cmd/mcp_install.go index e8b3efbc..8cbc8eef 100644 --- a/internal/tiger/cmd/mcp_install.go +++ b/internal/tiger/cmd/mcp_install.go @@ -45,16 +45,18 @@ type MCPServerConfig struct { // InstallOptions configures the MCP server installation behavior type InstallOptions struct { - // CreateBackup creates a backup of existing config files before modification - CreateBackup bool - // CustomConfigPath overrides the default config file location - CustomConfigPath string + // ClientName is the name of the client to configure (required) + ClientName string // ServerName is the name to register the MCP server as (required) ServerName string // Command is the path to the MCP server binary (required) Command string // Args are the arguments to pass to the MCP server binary (required) Args []string + // CreateBackup creates a backup of existing config files before modification + CreateBackup bool + // CustomConfigPath overrides the default config file location + CustomConfigPath string } // clientConfig represents our own client configuration for Tiger MCP installation @@ -189,8 +191,11 @@ func getValidEditorNames() []string { // InstallMCPForClient installs an MCP server configuration for the specified client. // This is a generic, configurable function exported for use by external projects via pkg/mcpinstall. // Required options: ServerName, Command, Args must all be provided. -func InstallMCPForClient(clientName string, opts InstallOptions) error { +func InstallMCPForClient(opts InstallOptions) error { // Validate required options + if opts.ClientName == "" { + return fmt.Errorf("ClientName is required") + } if opts.ServerName == "" { return fmt.Errorf("ServerName is required") } @@ -202,7 +207,7 @@ func InstallMCPForClient(clientName string, opts InstallOptions) error { } // Find the client configuration by name - clientCfg, err := findClientConfig(clientName) + clientCfg, err := findClientConfig(opts.ClientName) if err != nil { return err } @@ -217,16 +222,16 @@ func InstallMCPForClient(clientName string, opts InstallOptions) error { // Use manual config path discovery for clients with configured paths configPath, err = findClientConfigFile(clientCfg.ConfigPaths) if err != nil { - return fmt.Errorf("failed to find configuration for %s: %w", clientName, err) + return fmt.Errorf("failed to find configuration for %s: %w", opts.ClientName, err) } } else if clientCfg.buildInstallCommand == nil { // Client has neither ConfigPaths nor buildInstallCommand - return fmt.Errorf("client %s has no ConfigPaths or buildInstallCommand defined", clientName) + return fmt.Errorf("client %s has no ConfigPaths or buildInstallCommand defined", opts.ClientName) } // else: CLI-only client - configPath remains empty, will use buildInstallCommand logging.Info("Installing MCP server configuration", - zap.String("client", clientName), + zap.String("client", opts.ClientName), zap.String("server_name", opts.ServerName), zap.String("command", opts.Command), zap.Strings("args", opts.Args), @@ -269,14 +274,15 @@ func installTigerMCPForClient(clientName string, createBackup bool, customConfig } opts := InstallOptions{ - CreateBackup: createBackup, - CustomConfigPath: customConfigPath, + ClientName: clientName, ServerName: mcp.ServerName, Command: command, Args: []string{"mcp", "start"}, + CreateBackup: createBackup, + CustomConfigPath: customConfigPath, } - if err := InstallMCPForClient(clientName, opts); err != nil { + if err := InstallMCPForClient(opts); err != nil { return err } diff --git a/pkg/mcpinstall/install.go b/pkg/mcpinstall/install.go index 59731861..be055c82 100644 --- a/pkg/mcpinstall/install.go +++ b/pkg/mcpinstall/install.go @@ -9,13 +9,10 @@ import ( // Options configures the MCP server installation behavior. type Options = cmd.InstallOptions -// InstallForClient installs an MCP server configuration for the specified client. -// -// Parameters: -// - clientName: The name of the client to configure (e.g., "claude-code", "cursor", "windsurf") -// - opts: Configuration options for the installation +// Install installs an MCP server configuration for the specified client. // // Required options: +// - ClientName: The name of the client to configure (e.g., "claude-code", "cursor", "windsurf") // - ServerName: The name to register the MCP server as (e.g., "my-mcp-server") // - Command: Path to the MCP server binary (e.g., "/usr/local/bin/my-server") // - Args: Arguments to pass to the MCP server binary (e.g., []string{"serve", "--port", "8080"}) @@ -24,9 +21,7 @@ type Options = cmd.InstallOptions // - CreateBackup: If true, creates a backup of the existing config file before modification // - CustomConfigPath: Custom path to the config file (empty string uses default location) // -// Supported clients: claude-code, cursor, windsurf, codex, gemini, vscode, antigravity, kiro-cli -// // Returns an error if the client is not supported, required options are missing, or installation fails. -func InstallForClient(clientName string, opts Options) error { - return cmd.InstallMCPForClient(clientName, opts) +func Install(opts Options) error { + return cmd.InstallMCPForClient(opts) } From d6afc52578e0cf69d6b5323224bde0bf9da9f959 Mon Sep 17 00:00:00 2001 From: Matvey Arye Date: Fri, 5 Dec 2025 11:07:13 +0100 Subject: [PATCH 4/4] Add SupportedClients function to public API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add ClientInfo struct and SupportedClients() function to expose available MCP clients. This allows external users to discover valid values for Options.ClientName. ClientInfo contains: - Name: Human-readable display name (e.g., "Claude Code") - ClientName: Identifier for Options.ClientName (e.g., "claude-code") šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- internal/tiger/cmd/mcp_install.go | 20 ++++++++++++++++++++ pkg/mcpinstall/install.go | 9 +++++++++ 2 files changed, 29 insertions(+) diff --git a/internal/tiger/cmd/mcp_install.go b/internal/tiger/cmd/mcp_install.go index 8cbc8eef..425a584d 100644 --- a/internal/tiger/cmd/mcp_install.go +++ b/internal/tiger/cmd/mcp_install.go @@ -188,6 +188,26 @@ func getValidEditorNames() []string { return validNames } +// ClientInfo contains information about a supported MCP client. +type ClientInfo struct { + // Name is the human-readable display name (e.g., "Claude Code", "Cursor") + Name string + // ClientName is the identifier to use in InstallOptions.ClientName (e.g., "claude-code", "cursor") + ClientName string +} + +// SupportedClients returns information about all supported MCP clients. +func SupportedClients() []ClientInfo { + clients := make([]ClientInfo, 0, len(supportedClients)) + for _, c := range supportedClients { + clients = append(clients, ClientInfo{ + Name: c.Name, + ClientName: c.EditorNames[0], + }) + } + return clients +} + // InstallMCPForClient installs an MCP server configuration for the specified client. // This is a generic, configurable function exported for use by external projects via pkg/mcpinstall. // Required options: ServerName, Command, Args must all be provided. diff --git a/pkg/mcpinstall/install.go b/pkg/mcpinstall/install.go index be055c82..bff962e6 100644 --- a/pkg/mcpinstall/install.go +++ b/pkg/mcpinstall/install.go @@ -9,6 +9,15 @@ import ( // Options configures the MCP server installation behavior. type Options = cmd.InstallOptions +// ClientInfo contains information about a supported MCP client. +type ClientInfo = cmd.ClientInfo + +// SupportedClients returns information about all supported MCP clients. +// Use this to get valid values for Options.ClientName. +func SupportedClients() []ClientInfo { + return cmd.SupportedClients() +} + // Install installs an MCP server configuration for the specified client. // // Required options: