From ff8da1cf7db92d7fabb1cefe3c3f7152f6e9dea9 Mon Sep 17 00:00:00 2001 From: delarea Date: Sun, 18 May 2025 10:03:09 +0300 Subject: [PATCH 01/21] stash --- general/ai/cli.go | 151 ++++++++++++++++++++++++++++++++++++++++++++++ main.go | 7 +++ 2 files changed, 158 insertions(+) diff --git a/general/ai/cli.go b/general/ai/cli.go index 8ef6c0e71..6ddece58b 100644 --- a/general/ai/cli.go +++ b/general/ai/cli.go @@ -18,6 +18,7 @@ import ( "io" "net/http" "os" + "os/exec" "strings" ) @@ -96,6 +97,156 @@ func HowCmd(c *cli.Context) error { } } +func McpCmd(c *cli.Context) error { + // Show help if needed + if show, err := cliutils.ShowCmdHelpIfNeeded(c, c.Args()); show || err != nil { + return err + } + //log.Output(coreutils.PrintBoldTitle("Welcome to the MCP-Start Command! 🚀")) + // TODO this should downloaded from releases and version should be a variable + //log.Output("Download MCP server binary version : v0.0.1 ... ") + // TODO need to decide where the executable is being downloaded..maybe the current dir is okay. + exePath, err := downloadServerExecutable() + if err != nil { + return err + } + //log.Output(fmt.Sprintf("✅ Successfully downloaded the MCP server binary to: %s", exePath)) + + //binaryPath, err := resolveOrDownloadMcpBinary() + //if err != nil { + // return fmt.Errorf("failed to get MCP binary: %w", err) + //} + + cmd := exec.Command(exePath) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Env = os.Environ() + + return cmd.Run() + // + //client, err := promptClientSelection() + //if err != nil { + // return err + //} + //log.Output(fmt.Sprintf("You selected: %s", client)) + // + //// Output the corresponding template + //if err = outputClientTemplate(client, exePath); err != nil { + // return err + //} + // + //log.Output("✅ Successfully completed the `mcp-start` process!\n") + //log.Output("â„šī¸ For further assistance, questions, or issues, please visit the repository: https://github.com/jfrog/mcp-jfrog-go") + //return nil +} + +func outputClientTemplate(client, path string) error { + templates := map[string]string{ + "Cursor IDE": fmt.Sprintf(`Add the following to your ~/.cursor/mcp.json file: + +{ + "mcpServers": { + "jfrog-cli-mcp-server": { + "command": "%s", + "capabilities": { + "tools": true + } + } + } +}`, path), + "VsCode": fmt.Sprintf(`Add the following to your VS Code settings (usually in settings.json): + +{ + "mcp": { + "servers": { + "JFrog-Cli": { + "type": "stdio", + "command": "%s", + } + } + } + } +}`, path), + "Calude": fmt.Sprintf(`Add the following to your VS Code settings (usually in settings.json): + +{ + "mcpServers": { + "JFrog-MCP-Server": { + "command": "%s", + "capabilities": { + "tools": true + } + } + } +}`, path), + } + + template, exists := templates[client] + if !exists { + return nil + } + + log.Output(coreutils.PrintBoldTitle("Configuration Template:")) + log.Output(template) + return nil +} + +func promptClientSelection() (string, error) { + clients := []string{"Calude", "VsCode", "Cursor IDE", "others"} + prompt := promptui.Select{ + Label: "Select your client", + Items: clients, + } + _, client, err := prompt.Run() + return client, err +} + +func downloadServerExecutable() (string, error) { + //binaryName := "mcp-jfrog-go" + // TODO this has to point to latest + repoPath := "v0/0.0.1" + + targetDir, err := coreutils.GetJfrogHomeDir() + if err != nil { + return "", fmt.Errorf("failed to get JFrog home directory: %w", err) + } + + // Create the target directory if it doesn't exist + if _, err := os.Stat(targetDir); os.IsNotExist(err) { + if err := os.Mkdir(targetDir, 0755); err != nil { + return "", fmt.Errorf("failed to create directory '%s': %w", targetDir, err) + } + } + + // Change into the target directory + if err := os.Chdir(targetDir); err != nil { + return "", fmt.Errorf("failed to cd into directory '%s': %w", targetDir, err) + } + + // Construct the full path for the binary + wd, err := os.Getwd() + if err != nil { + return "", fmt.Errorf("failed to get current working directory: %w", err) + } + + // Run the JFrog CLI download command + cmd := exec.Command("jf", "rt", "dl", targetDir, repoPath) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return "", fmt.Errorf("failed to download binary: %w", err) + } + fullPath := fmt.Sprintf("%s/%s", wd, repoPath) + // Make the binary executable + if err := os.Chmod(fullPath, 0755); err != nil { + return "", fmt.Errorf("failed to make binary executable: %w", err) + } + + // Return the full path of the binary + return fullPath, nil +} + type questionBody struct { Question string `json:"question"` } diff --git a/main.go b/main.go index 3aec5037a..746b6a237 100644 --- a/main.go +++ b/main.go @@ -273,6 +273,13 @@ func getCommands() ([]cli.Command, error) { BashComplete: corecommon.CreateBashCompletionFunc(), Action: ai.HowCmd, }, + { + Name: "mcp-start", + Usage: aiDocs.GetDescription(), + HelpName: corecommon.CreateUsage("how", aiDocs.GetDescription(), aiDocs.Usage), + BashComplete: corecommon.CreateBashCompletionFunc(), + Action: ai.McpCmd, + }, { Name: "access-token-create", Aliases: []string{"atc"}, From a7815f51b86c145967aeec5f9ca6d938b30c5a98 Mon Sep 17 00:00:00 2001 From: delarea Date: Sun, 18 May 2025 15:02:07 +0300 Subject: [PATCH 02/21] refactor --- docs/general/{ai => how}/help.go | 2 +- docs/general/mcp/help.go | 7 ++ general/{ai => how}/cli.go | 153 +------------------------------ general/mcp/cli.go | 99 ++++++++++++++++++++ main.go | 20 ++-- 5 files changed, 119 insertions(+), 162 deletions(-) rename docs/general/{ai => how}/help.go (93%) create mode 100644 docs/general/mcp/help.go rename general/{ai => how}/cli.go (63%) create mode 100644 general/mcp/cli.go diff --git a/docs/general/ai/help.go b/docs/general/how/help.go similarity index 93% rename from docs/general/ai/help.go rename to docs/general/how/help.go index 3ae224eb0..1585057ae 100644 --- a/docs/general/ai/help.go +++ b/docs/general/how/help.go @@ -1,4 +1,4 @@ -package ai +package how var Usage = []string{"how"} diff --git a/docs/general/mcp/help.go b/docs/general/mcp/help.go new file mode 100644 index 000000000..0646ecb34 --- /dev/null +++ b/docs/general/mcp/help.go @@ -0,0 +1,7 @@ +package howai + +var Usage = []string{"mcp"} + +func GetDescription() string { + return "Start the JFrog MCP server and begin using it with your MCP client of your choice." +} diff --git a/general/ai/cli.go b/general/how/cli.go similarity index 63% rename from general/ai/cli.go rename to general/how/cli.go index 6ddece58b..35970e6e3 100644 --- a/general/ai/cli.go +++ b/general/how/cli.go @@ -1,4 +1,4 @@ -package ai +package how import ( "bufio" @@ -18,7 +18,6 @@ import ( "io" "net/http" "os" - "os/exec" "strings" ) @@ -97,156 +96,6 @@ func HowCmd(c *cli.Context) error { } } -func McpCmd(c *cli.Context) error { - // Show help if needed - if show, err := cliutils.ShowCmdHelpIfNeeded(c, c.Args()); show || err != nil { - return err - } - //log.Output(coreutils.PrintBoldTitle("Welcome to the MCP-Start Command! 🚀")) - // TODO this should downloaded from releases and version should be a variable - //log.Output("Download MCP server binary version : v0.0.1 ... ") - // TODO need to decide where the executable is being downloaded..maybe the current dir is okay. - exePath, err := downloadServerExecutable() - if err != nil { - return err - } - //log.Output(fmt.Sprintf("✅ Successfully downloaded the MCP server binary to: %s", exePath)) - - //binaryPath, err := resolveOrDownloadMcpBinary() - //if err != nil { - // return fmt.Errorf("failed to get MCP binary: %w", err) - //} - - cmd := exec.Command(exePath) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Env = os.Environ() - - return cmd.Run() - // - //client, err := promptClientSelection() - //if err != nil { - // return err - //} - //log.Output(fmt.Sprintf("You selected: %s", client)) - // - //// Output the corresponding template - //if err = outputClientTemplate(client, exePath); err != nil { - // return err - //} - // - //log.Output("✅ Successfully completed the `mcp-start` process!\n") - //log.Output("â„šī¸ For further assistance, questions, or issues, please visit the repository: https://github.com/jfrog/mcp-jfrog-go") - //return nil -} - -func outputClientTemplate(client, path string) error { - templates := map[string]string{ - "Cursor IDE": fmt.Sprintf(`Add the following to your ~/.cursor/mcp.json file: - -{ - "mcpServers": { - "jfrog-cli-mcp-server": { - "command": "%s", - "capabilities": { - "tools": true - } - } - } -}`, path), - "VsCode": fmt.Sprintf(`Add the following to your VS Code settings (usually in settings.json): - -{ - "mcp": { - "servers": { - "JFrog-Cli": { - "type": "stdio", - "command": "%s", - } - } - } - } -}`, path), - "Calude": fmt.Sprintf(`Add the following to your VS Code settings (usually in settings.json): - -{ - "mcpServers": { - "JFrog-MCP-Server": { - "command": "%s", - "capabilities": { - "tools": true - } - } - } -}`, path), - } - - template, exists := templates[client] - if !exists { - return nil - } - - log.Output(coreutils.PrintBoldTitle("Configuration Template:")) - log.Output(template) - return nil -} - -func promptClientSelection() (string, error) { - clients := []string{"Calude", "VsCode", "Cursor IDE", "others"} - prompt := promptui.Select{ - Label: "Select your client", - Items: clients, - } - _, client, err := prompt.Run() - return client, err -} - -func downloadServerExecutable() (string, error) { - //binaryName := "mcp-jfrog-go" - // TODO this has to point to latest - repoPath := "v0/0.0.1" - - targetDir, err := coreutils.GetJfrogHomeDir() - if err != nil { - return "", fmt.Errorf("failed to get JFrog home directory: %w", err) - } - - // Create the target directory if it doesn't exist - if _, err := os.Stat(targetDir); os.IsNotExist(err) { - if err := os.Mkdir(targetDir, 0755); err != nil { - return "", fmt.Errorf("failed to create directory '%s': %w", targetDir, err) - } - } - - // Change into the target directory - if err := os.Chdir(targetDir); err != nil { - return "", fmt.Errorf("failed to cd into directory '%s': %w", targetDir, err) - } - - // Construct the full path for the binary - wd, err := os.Getwd() - if err != nil { - return "", fmt.Errorf("failed to get current working directory: %w", err) - } - - // Run the JFrog CLI download command - cmd := exec.Command("jf", "rt", "dl", targetDir, repoPath) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - return "", fmt.Errorf("failed to download binary: %w", err) - } - fullPath := fmt.Sprintf("%s/%s", wd, repoPath) - // Make the binary executable - if err := os.Chmod(fullPath, 0755); err != nil { - return "", fmt.Errorf("failed to make binary executable: %w", err) - } - - // Return the full path of the binary - return fullPath, nil -} - type questionBody struct { Question string `json:"question"` } diff --git a/general/mcp/cli.go b/general/mcp/cli.go new file mode 100644 index 000000000..91a8531e6 --- /dev/null +++ b/general/mcp/cli.go @@ -0,0 +1,99 @@ +package mcp + +import ( + "fmt" + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "github.com/jfrog/jfrog-cli/utils/cliutils" + "github.com/urfave/cli" + "os" + "os/exec" +) + +func McpCmd(c *cli.Context) error { + // Show help if needed + if show, err := cliutils.ShowCmdHelpIfNeeded(c, c.Args()); show || err != nil { + return err + } + //log.Output(coreutils.PrintBoldTitle("Welcome to the MCP-Start Command! 🚀")) + // TODO this should downloaded from releases and version should be a variable + //log.Output("Download MCP server binary version : v0.0.1 ... ") + // TODO need to decide where the executable is being downloaded..maybe the current dir is okay. + exePath, err := downloadServerExecutable() + if err != nil { + return err + } + //log.Output(fmt.Sprintf("✅ Successfully downloaded the MCP server binary to: %s", exePath)) + + //binaryPath, err := resolveOrDownloadMcpBinary() + //if err != nil { + // return fmt.Errorf("failed to get MCP binary: %w", err) + //} + + cmd := exec.Command(exePath) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Env = os.Environ() + + return cmd.Run() + // + //client, err := promptClientSelection() + //if err != nil { + // return err + //} + //log.Output(fmt.Sprintf("You selected: %s", client)) + // + //// Output the corresponding template + //if err = outputClientTemplate(client, exePath); err != nil { + // return err + //} + // + //log.Output("✅ Successfully completed the `mcp-start` process!\n") + //log.Output("â„šī¸ For further assistance, questions, or issues, please visit the repository: https://github.com/jfrog/mcp-jfrog-go") + //return nil +} + +func downloadServerExecutable() (string, error) { + //binaryName := "mcp-jfrog-go" + // TODO this has to point to latest + repoPath := "v0/0.0.1" + + targetDir, err := coreutils.GetJfrogHomeDir() + if err != nil { + return "", fmt.Errorf("failed to get JFrog home directory: %w", err) + } + + // Create the target directory if it doesn't exist + if _, err := os.Stat(targetDir); os.IsNotExist(err) { + if err := os.Mkdir(targetDir, 0755); err != nil { + return "", fmt.Errorf("failed to create directory '%s': %w", targetDir, err) + } + } + + // Change into the target directory + if err := os.Chdir(targetDir); err != nil { + return "", fmt.Errorf("failed to cd into directory '%s': %w", targetDir, err) + } + + // Construct the full path for the binary + wd, err := os.Getwd() + if err != nil { + return "", fmt.Errorf("failed to get current working directory: %w", err) + } + + // Run the JFrog CLI download command + cmd := exec.Command("jf", "rt", "dl", targetDir, repoPath) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return "", fmt.Errorf("failed to download binary: %w", err) + } + fullPath := fmt.Sprintf("%s/%s", wd, repoPath) + // Make the binary executable + if err := os.Chmod(fullPath, 0755); err != nil { + return "", fmt.Errorf("failed to make binary executable: %w", err) + } + + // Return the full path of the binary + return fullPath, nil +} diff --git a/main.go b/main.go index 746b6a237..cce69304e 100644 --- a/main.go +++ b/main.go @@ -18,13 +18,15 @@ import ( "github.com/jfrog/jfrog-cli/completion" "github.com/jfrog/jfrog-cli/config" "github.com/jfrog/jfrog-cli/docs/common" - aiDocs "github.com/jfrog/jfrog-cli/docs/general/ai" + aiHowDocs "github.com/jfrog/jfrog-cli/docs/general/how" loginDocs "github.com/jfrog/jfrog-cli/docs/general/login" + mcpDocs "github.com/jfrog/jfrog-cli/docs/general/mcp" oidcDocs "github.com/jfrog/jfrog-cli/docs/general/oidc" summaryDocs "github.com/jfrog/jfrog-cli/docs/general/summary" tokenDocs "github.com/jfrog/jfrog-cli/docs/general/token" - "github.com/jfrog/jfrog-cli/general/ai" + "github.com/jfrog/jfrog-cli/general/how" "github.com/jfrog/jfrog-cli/general/login" + "github.com/jfrog/jfrog-cli/general/mcp" "github.com/jfrog/jfrog-cli/general/summary" "github.com/jfrog/jfrog-cli/general/token" "github.com/jfrog/jfrog-cli/missioncontrol" @@ -268,17 +270,17 @@ func getCommands() ([]cli.Command, error) { }, { Name: "how", - Usage: aiDocs.GetDescription(), - HelpName: corecommon.CreateUsage("how", aiDocs.GetDescription(), aiDocs.Usage), + Usage: aiHowDocs.GetDescription(), + HelpName: corecommon.CreateUsage("how", aiHowDocs.GetDescription(), aiHowDocs.Usage), BashComplete: corecommon.CreateBashCompletionFunc(), - Action: ai.HowCmd, + Action: how.HowCmd, }, { - Name: "mcp-start", - Usage: aiDocs.GetDescription(), - HelpName: corecommon.CreateUsage("how", aiDocs.GetDescription(), aiDocs.Usage), + Name: "mcp", + Usage: mcpDocs.GetDescription(), + HelpName: corecommon.CreateUsage("mcp", mcpDocs.GetDescription(), mcpDocs.Usage), BashComplete: corecommon.CreateBashCompletionFunc(), - Action: ai.McpCmd, + Action: mcp.McpCmd, }, { Name: "access-token-create", From 14c282d3cdb512c6a737a8ef3fb0630f223c18e3 Mon Sep 17 00:00:00 2001 From: delarea Date: Sun, 18 May 2025 15:49:30 +0300 Subject: [PATCH 03/21] refactor --- general/mcp/cli.go | 48 ++++++++++++++++----------------- main.go | 3 ++- utils/cliutils/commandsflags.go | 17 ++++++++++++ 3 files changed, 42 insertions(+), 26 deletions(-) diff --git a/general/mcp/cli.go b/general/mcp/cli.go index 91a8531e6..282b324b3 100644 --- a/general/mcp/cli.go +++ b/general/mcp/cli.go @@ -14,20 +14,33 @@ func McpCmd(c *cli.Context) error { if show, err := cliutils.ShowCmdHelpIfNeeded(c, c.Args()); show || err != nil { return err } - //log.Output(coreutils.PrintBoldTitle("Welcome to the MCP-Start Command! 🚀")) - // TODO this should downloaded from releases and version should be a variable - //log.Output("Download MCP server binary version : v0.0.1 ... ") - // TODO need to decide where the executable is being downloaded..maybe the current dir is okay. + + // Require at least one argument (the subcommand, e.g. "start") + if c.NArg() < 1 { + return cliutils.WrongNumberOfArgumentsHandler(c) + } + cmdArg := c.Args().Get(0) + if cmdArg != "start" { + return cliutils.PrintHelpAndReturnError(fmt.Sprintf("Unknown subcommand: %s", cmdArg), c) + } + + // Accept --toolset and --tool-access from flags or env vars (flags win) + toolset := c.String(cliutils.McpToolsets) + if toolset == "" { + toolset = os.Getenv("JFROG_MCP_TOOLSETS") + } + toolsAccess := c.String(cliutils.McpToolAccess) + if toolsAccess == "" { + toolsAccess = os.Getenv("JFROG_MCP_TOOLS_ACCESS") + } + + fmt.Printf("Starting MCP with toolset: %s, toolsAccess: %s\n", toolset, toolsAccess) + + // TODO should be connected to releases instead exePath, err := downloadServerExecutable() if err != nil { return err } - //log.Output(fmt.Sprintf("✅ Successfully downloaded the MCP server binary to: %s", exePath)) - - //binaryPath, err := resolveOrDownloadMcpBinary() - //if err != nil { - // return fmt.Errorf("failed to get MCP binary: %w", err) - //} cmd := exec.Command(exePath) cmd.Stdin = os.Stdin @@ -36,21 +49,6 @@ func McpCmd(c *cli.Context) error { cmd.Env = os.Environ() return cmd.Run() - // - //client, err := promptClientSelection() - //if err != nil { - // return err - //} - //log.Output(fmt.Sprintf("You selected: %s", client)) - // - //// Output the corresponding template - //if err = outputClientTemplate(client, exePath); err != nil { - // return err - //} - // - //log.Output("✅ Successfully completed the `mcp-start` process!\n") - //log.Output("â„šī¸ For further assistance, questions, or issues, please visit the repository: https://github.com/jfrog/mcp-jfrog-go") - //return nil } func downloadServerExecutable() (string, error) { diff --git a/main.go b/main.go index cce69304e..65358a006 100644 --- a/main.go +++ b/main.go @@ -278,8 +278,9 @@ func getCommands() ([]cli.Command, error) { { Name: "mcp", Usage: mcpDocs.GetDescription(), - HelpName: corecommon.CreateUsage("mcp", mcpDocs.GetDescription(), mcpDocs.Usage), + HelpName: corecommon.CreateUsage("mcp start", mcpDocs.GetDescription(), mcpDocs.Usage), BashComplete: corecommon.CreateBashCompletionFunc(), + Flags: cliutils.GetCommandFlags(cliutils.Mcp), Action: mcp.McpCmd, }, { diff --git a/utils/cliutils/commandsflags.go b/utils/cliutils/commandsflags.go index 15b786258..8a0fd8459 100644 --- a/utils/cliutils/commandsflags.go +++ b/utils/cliutils/commandsflags.go @@ -118,6 +118,13 @@ const ( AccessTokenCreate = "access-token-create" ExchangeOidcToken = "exchange-oidc-token" + // MCP command key + Mcp = "mcp" + + // MCP flags + McpToolsets = "toolsets" + McpToolAccess = "tool-access" + // *** Artifactory Commands' flags *** // Base flags url = "url" @@ -1246,6 +1253,14 @@ var flagsMap = map[string]cli.Flag{ Name: TargetWorkingDir, Usage: "[Default: '/storage'] Local working directory on the target Artifactory server.` `", }, + McpToolsets: cli.StringFlag{ + Name: McpToolsets, + Usage: "Comma-separated list of toolsets (can also be set via JFROG_MCP_TOOLSETS env var)", + }, + McpToolAccess: cli.StringFlag{ + Name: McpToolAccess, + Usage: "Semicolon-separated list of tool access rights (can also be set via JFROG_MCP_TOOLS_ACCESS env var)", + }, // Distribution's commands Flags distUrl: cli.StringFlag{ @@ -1712,6 +1727,7 @@ var flagsMap = map[string]cli.Flag{ } var commandFlags = map[string][]string{ + // Common commands flags AddConfig: { interactive, EncPassword, configPlatformUrl, configRtUrl, configDistUrl, configXrUrl, configMcUrl, configPlUrl, configUser, configPassword, configAccessToken, sshKeyPath, sshPassphrase, ClientCertPath, ClientCertKeyPath, BasicAuthOnly, configInsecureTls, Overwrite, passwordStdin, accessTokenStdin, OidcTokenID, OidcProviderName, OidcAudience, OidcProviderType, ApplicationKey, @@ -2038,6 +2054,7 @@ var commandFlags = map[string][]string{ Setup: { serverId, url, user, password, accessToken, sshPassphrase, sshKeyPath, ClientCertPath, ClientCertKeyPath, Project, setupRepo, }, + Mcp: {McpToolsets, McpToolAccess}, } func GetCommandFlags(cmd string) []cli.Flag { From acdebb51f926fa6c8e3ad7aca24a946a7a813854 Mon Sep 17 00:00:00 2001 From: delarea Date: Sun, 18 May 2025 15:51:27 +0300 Subject: [PATCH 04/21] export env vars --- general/mcp/cli.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/general/mcp/cli.go b/general/mcp/cli.go index 282b324b3..bb4965079 100644 --- a/general/mcp/cli.go +++ b/general/mcp/cli.go @@ -9,6 +9,11 @@ import ( "os/exec" ) +const ( + mcpToolSetsEnvVar = "JFROG_MCP_TOOLSETS" + mcpToolAccessEnvVar = "JFROG_MCP_TOOL_ACCESS" +) + func McpCmd(c *cli.Context) error { // Show help if needed if show, err := cliutils.ShowCmdHelpIfNeeded(c, c.Args()); show || err != nil { @@ -27,11 +32,11 @@ func McpCmd(c *cli.Context) error { // Accept --toolset and --tool-access from flags or env vars (flags win) toolset := c.String(cliutils.McpToolsets) if toolset == "" { - toolset = os.Getenv("JFROG_MCP_TOOLSETS") + toolset = os.Getenv(mcpToolSetsEnvVar) } toolsAccess := c.String(cliutils.McpToolAccess) if toolsAccess == "" { - toolsAccess = os.Getenv("JFROG_MCP_TOOLS_ACCESS") + toolsAccess = os.Getenv(mcpToolAccessEnvVar) } fmt.Printf("Starting MCP with toolset: %s, toolsAccess: %s\n", toolset, toolsAccess) From 20efe8e3030e33047db894fed3b9ebb047ea58b4 Mon Sep 17 00:00:00 2001 From: delarea Date: Sun, 18 May 2025 17:48:12 +0300 Subject: [PATCH 05/21] Move from script to go --- general/mcp/cli.go | 98 +++++++++++++++++++++------------ main.go | 9 +-- utils/cliutils/commandsflags.go | 5 +- 3 files changed, 72 insertions(+), 40 deletions(-) diff --git a/general/mcp/cli.go b/general/mcp/cli.go index bb4965079..e3584241f 100644 --- a/general/mcp/cli.go +++ b/general/mcp/cli.go @@ -2,16 +2,23 @@ package mcp import ( "fmt" + "io" + "net/http" + "os" + "os/exec" + "path" + "runtime" + "strings" + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli/utils/cliutils" "github.com/urfave/cli" - "os" - "os/exec" ) const ( mcpToolSetsEnvVar = "JFROG_MCP_TOOLSETS" mcpToolAccessEnvVar = "JFROG_MCP_TOOL_ACCESS" + mcpServerBinaryName = "cli-mcp-server" ) func McpCmd(c *cli.Context) error { @@ -39,15 +46,12 @@ func McpCmd(c *cli.Context) error { toolsAccess = os.Getenv(mcpToolAccessEnvVar) } - fmt.Printf("Starting MCP with toolset: %s, toolsAccess: %s\n", toolset, toolsAccess) - - // TODO should be connected to releases instead - exePath, err := downloadServerExecutable() + executablePath, err := downloadServerExecutable() if err != nil { return err } - cmd := exec.Command(exePath) + cmd := exec.Command(executablePath, "--toolsets="+toolset, "--tools-access="+toolsAccess) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -57,46 +61,72 @@ func McpCmd(c *cli.Context) error { } func downloadServerExecutable() (string, error) { - //binaryName := "mcp-jfrog-go" - // TODO this has to point to latest - repoPath := "v0/0.0.1" + // TODO should be updated to [RELEASE] + const version = "0.0.1" + osName, arch, binaryName, err := getOsArchBinaryInfo() + if err != nil { + return "", err + } - targetDir, err := coreutils.GetJfrogHomeDir() + jfrogHomeDir, err := coreutils.GetJfrogHomeDir() if err != nil { return "", fmt.Errorf("failed to get JFrog home directory: %w", err) } - - // Create the target directory if it doesn't exist - if _, err := os.Stat(targetDir); os.IsNotExist(err) { - if err := os.Mkdir(targetDir, 0755); err != nil { - return "", fmt.Errorf("failed to create directory '%s': %w", targetDir, err) + targetDir := path.Join(jfrogHomeDir, "cli-mcp") + if err := os.MkdirAll(targetDir, 0777); err != nil { + return "", fmt.Errorf("failed to create directory '%s': %w", targetDir, err) + } + fullPath := path.Join(targetDir, binaryName) + fileInfo, err := os.Stat(fullPath) + if err == nil { + // On Unix, check if the file is executable + if runtime.GOOS != "windows" && fileInfo.Mode()&0111 == 0 { + fmt.Printf("File exists but is not executable, will re-download: %s\n", fullPath) + } else { + fmt.Printf("MCP server binary already present at: %s\n", fullPath) + return fullPath, nil } + } else if !os.IsNotExist(err) { + return "", fmt.Errorf("failed to stat '%s': %w", fullPath, err) } - // Change into the target directory - if err := os.Chdir(targetDir); err != nil { - return "", fmt.Errorf("failed to cd into directory '%s': %w", targetDir, err) + // Build the download URL (update as needed for your actual release location) + // TODO this should be updated to releases + url := fmt.Sprintf("https://entplus.jfrog.io/artifactory/ecosys-cli-mcp-server/v%s/%s-%s-%s", version, "mcp-jfrog-go", osName, arch) + fmt.Printf("Downloading MCP server from: %s\n", url) + resp, err := http.Get(url) + if err != nil { + return "", fmt.Errorf("failed to download MCP server: %w", err) } - // Construct the full path for the binary - wd, err := os.Getwd() + defer func() { + err = resp.Body.Close() + }() + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("failed to download MCP server: received status %s", resp.Status) + } + out, err := os.Create(fullPath) if err != nil { - return "", fmt.Errorf("failed to get current working directory: %w", err) + return "", fmt.Errorf("failed to create file '%s': %w", fullPath, err) } - - // Run the JFrog CLI download command - cmd := exec.Command("jf", "rt", "dl", targetDir, repoPath) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - return "", fmt.Errorf("failed to download binary: %w", err) + defer func() { + err = out.Close() + }() + if _, err := io.Copy(out, resp.Body); err != nil { + return "", fmt.Errorf("failed to write binary: %w", err) } - fullPath := fmt.Sprintf("%s/%s", wd, repoPath) - // Make the binary executable - if err := os.Chmod(fullPath, 0755); err != nil { + if err := os.Chmod(fullPath, 0755); err != nil && !strings.HasSuffix(binaryName, ".exe") { return "", fmt.Errorf("failed to make binary executable: %w", err) } - - // Return the full path of the binary return fullPath, nil } + +func getOsArchBinaryInfo() (osName, arch, binaryName string, err error) { + osName = runtime.GOOS + arch = runtime.GOARCH + binaryName = mcpServerBinaryName + if osName == "windows" { + binaryName += ".exe" + } + return +} diff --git a/main.go b/main.go index 65358a006..3adc7595e 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,11 @@ import ( "crypto/rand" "encoding/hex" "fmt" + "os" + "runtime" + "sort" + "strings" + "github.com/agnivade/levenshtein" artifactoryCLI "github.com/jfrog/jfrog-cli-artifactory/cli" corecommon "github.com/jfrog/jfrog-cli-core/v2/docs/common" @@ -41,10 +46,6 @@ import ( clientlog "github.com/jfrog/jfrog-client-go/utils/log" "github.com/urfave/cli" "golang.org/x/exp/slices" - "os" - "runtime" - "sort" - "strings" ) const commandHelpTemplate string = `{{.HelpName}}{{if .UsageText}} diff --git a/utils/cliutils/commandsflags.go b/utils/cliutils/commandsflags.go index 8a0fd8459..16b48e00d 100644 --- a/utils/cliutils/commandsflags.go +++ b/utils/cliutils/commandsflags.go @@ -2,10 +2,11 @@ package cliutils import ( "fmt" - "github.com/jfrog/jfrog-cli-artifactory/cliutils/flagkit" "sort" "strconv" + "github.com/jfrog/jfrog-cli-artifactory/cliutils/flagkit" + commonCliUtils "github.com/jfrog/jfrog-cli-core/v2/common/cliutils" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" @@ -123,7 +124,7 @@ const ( // MCP flags McpToolsets = "toolsets" - McpToolAccess = "tool-access" + McpToolAccess = "tools-access" // *** Artifactory Commands' flags *** // Base flags From 4a5a844f7aa7cb9452f672563658a0f8c394b51e Mon Sep 17 00:00:00 2001 From: delarea Date: Tue, 20 May 2025 17:42:26 +0300 Subject: [PATCH 06/21] Add download from releases --- general/mcp/cli.go | 33 +++++++++++++++++++++++---------- utils/cliutils/commandsflags.go | 11 ++++++++--- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/general/mcp/cli.go b/general/mcp/cli.go index e3584241f..555e601f7 100644 --- a/general/mcp/cli.go +++ b/general/mcp/cli.go @@ -12,6 +12,7 @@ import ( "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli/utils/cliutils" + "github.com/jfrog/jfrog-client-go/utils/log" "github.com/urfave/cli" ) @@ -46,7 +47,13 @@ func McpCmd(c *cli.Context) error { toolsAccess = os.Getenv(mcpToolAccessEnvVar) } - executablePath, err := downloadServerExecutable() + // Add a flag to allow specifying a specific version of the MCP server + mcpVersion := c.String(cliutils.McpServerVersion) + if mcpVersion == "" { + mcpVersion = "[RELEASE]" + } + + executablePath, err := downloadServerExecutable(mcpVersion) if err != nil { return err } @@ -56,13 +63,19 @@ func McpCmd(c *cli.Context) error { cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Env = os.Environ() - + displayToolset := toolset + if displayToolset == "" { + displayToolset = "--tools-access=read" + } + displayToolsAccess := toolsAccess + if displayToolsAccess == "" { + displayToolsAccess = "all-toolsets" + } + log.Debug("Starting MCP server with toolset:", displayToolset, "and tools access:", displayToolsAccess) return cmd.Run() } -func downloadServerExecutable() (string, error) { - // TODO should be updated to [RELEASE] - const version = "0.0.1" +func downloadServerExecutable(version string) (string, error) { osName, arch, binaryName, err := getOsArchBinaryInfo() if err != nil { return "", err @@ -81,9 +94,9 @@ func downloadServerExecutable() (string, error) { if err == nil { // On Unix, check if the file is executable if runtime.GOOS != "windows" && fileInfo.Mode()&0111 == 0 { - fmt.Printf("File exists but is not executable, will re-download: %s\n", fullPath) + log.Debug("File exists but is not executable, will re-download:", fullPath) } else { - fmt.Printf("MCP server binary already present at: %s\n", fullPath) + log.Debug("MCP server binary already present at:", fullPath) return fullPath, nil } } else if !os.IsNotExist(err) { @@ -91,9 +104,8 @@ func downloadServerExecutable() (string, error) { } // Build the download URL (update as needed for your actual release location) - // TODO this should be updated to releases - url := fmt.Sprintf("https://entplus.jfrog.io/artifactory/ecosys-cli-mcp-server/v%s/%s-%s-%s", version, "mcp-jfrog-go", osName, arch) - fmt.Printf("Downloading MCP server from: %s\n", url) + url := fmt.Sprintf("https://releases.jfrog.io/artifactory/cli-mcp-server/v0/%s/%s-%s/%s", version, osName, arch, "cli-mcp-server") + log.Debug("Downloading MCP server from:", url) resp, err := http.Get(url) if err != nil { return "", fmt.Errorf("failed to download MCP server: %w", err) @@ -118,6 +130,7 @@ func downloadServerExecutable() (string, error) { if err := os.Chmod(fullPath, 0755); err != nil && !strings.HasSuffix(binaryName, ".exe") { return "", fmt.Errorf("failed to make binary executable: %w", err) } + log.Debug("MCP server binary downloaded to:", fullPath) return fullPath, nil } diff --git a/utils/cliutils/commandsflags.go b/utils/cliutils/commandsflags.go index 16b48e00d..e378d2d99 100644 --- a/utils/cliutils/commandsflags.go +++ b/utils/cliutils/commandsflags.go @@ -123,8 +123,9 @@ const ( Mcp = "mcp" // MCP flags - McpToolsets = "toolsets" - McpToolAccess = "tools-access" + McpToolsets = "toolsets" + McpToolAccess = "tools-access" + McpServerVersion = "mcp-server-version" // *** Artifactory Commands' flags *** // Base flags @@ -1262,6 +1263,10 @@ var flagsMap = map[string]cli.Flag{ Name: McpToolAccess, Usage: "Semicolon-separated list of tool access rights (can also be set via JFROG_MCP_TOOLS_ACCESS env var)", }, + McpServerVersion: cli.StringFlag{ + Name: McpServerVersion, + Usage: "Specify the MCP server version to download. Leave empty to use the latest version.", + }, // Distribution's commands Flags distUrl: cli.StringFlag{ @@ -2055,7 +2060,7 @@ var commandFlags = map[string][]string{ Setup: { serverId, url, user, password, accessToken, sshPassphrase, sshKeyPath, ClientCertPath, ClientCertKeyPath, Project, setupRepo, }, - Mcp: {McpToolsets, McpToolAccess}, + Mcp: {McpToolsets, McpToolAccess, McpServerVersion}, } func GetCommandFlags(cmd string) []cli.Flag { From 7746cd003308169cb151f0d0f084e2c8140005b6 Mon Sep 17 00:00:00 2001 From: delarea Date: Thu, 22 May 2025 12:24:20 +0300 Subject: [PATCH 07/21] Add download from releases --- docs/general/mcp/help.go | 6 +- general/mcp/cli.go | 223 ++++++++++++++++++++++++++++++--------- main.go | 5 +- 3 files changed, 181 insertions(+), 53 deletions(-) diff --git a/docs/general/mcp/help.go b/docs/general/mcp/help.go index 0646ecb34..ed1ca0bd8 100644 --- a/docs/general/mcp/help.go +++ b/docs/general/mcp/help.go @@ -1,7 +1,7 @@ -package howai +package mcp -var Usage = []string{"mcp"} +var Usage = []string{"mcp start", "mcp update"} func GetDescription() string { - return "Start the JFrog MCP server and begin using it with your MCP client of your choice." + return "Start or update the JFrog MCP server and begin using it with your MCP client of your choice." } diff --git a/general/mcp/cli.go b/general/mcp/cli.go index 555e601f7..222ddf3c5 100644 --- a/general/mcp/cli.go +++ b/general/mcp/cli.go @@ -2,6 +2,7 @@ package mcp import ( "fmt" + "github.com/jfrog/jfrog-cli-core/v2/common/commands" "io" "net/http" "os" @@ -10,6 +11,8 @@ import ( "runtime" "strings" + "github.com/jfrog/jfrog-cli-core/v2/utils/config" + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli/utils/cliutils" "github.com/jfrog/jfrog-client-go/utils/log" @@ -17,106 +20,226 @@ import ( ) const ( - mcpToolSetsEnvVar = "JFROG_MCP_TOOLSETS" - mcpToolAccessEnvVar = "JFROG_MCP_TOOL_ACCESS" - mcpServerBinaryName = "cli-mcp-server" + mcpToolSetsEnvVar = "JFROG_MCP_TOOLSETS" + mcpToolAccessEnvVar = "JFROG_MCP_TOOL_ACCESS" + mcpServerBinaryName = "cli-mcp-server" + defaultServerVersion = "[RELEASE]" + cliMcpDirName = "cli-mcp" + defaultToolsets = "read" + defaultToolAccess = "all-toolsets" + mcpDownloadBaseURL = "https://releases.jfrog.io/artifactory/cli-mcp-server/v0" ) -func McpCmd(c *cli.Context) error { - // Show help if needed - if show, err := cliutils.ShowCmdHelpIfNeeded(c, c.Args()); show || err != nil { - return err - } +type Command struct { + serverDetails *config.ServerDetails + toolSets string + toolAccess string + serverVersion string +} - // Require at least one argument (the subcommand, e.g. "start") - if c.NArg() < 1 { - return cliutils.WrongNumberOfArgumentsHandler(c) - } - cmdArg := c.Args().Get(0) - if cmdArg != "start" { - return cliutils.PrintHelpAndReturnError(fmt.Sprintf("Unknown subcommand: %s", cmdArg), c) - } +// NewMcpCommand returns a new MCP command instance +func NewMcpCommand() *Command { + return &Command{} +} +// SetServerDetails sets the Artifactory server details for the command +func (mcp *Command) SetServerDetails(serverDetails *config.ServerDetails) { + mcp.serverDetails = serverDetails +} + +// ServerDetails returns the Artifactory server details associated with the command +func (mcp *Command) ServerDetails() (*config.ServerDetails, error) { + return mcp.serverDetails, nil +} + +// CommandName returns the name of the command for usage reporting +func (mcp *Command) CommandName() string { + return "jf_mcp_start" +} + +// getMCPServerArgs extracts and sets command arguments from CLI flags or environment variables +func (mcp *Command) getMCPServerArgs(c *cli.Context) { // Accept --toolset and --tool-access from flags or env vars (flags win) - toolset := c.String(cliutils.McpToolsets) - if toolset == "" { - toolset = os.Getenv(mcpToolSetsEnvVar) + mcp.toolSets = c.String(cliutils.McpToolsets) + if mcp.toolSets == "" { + mcp.toolSets = os.Getenv(mcpToolSetsEnvVar) } - toolsAccess := c.String(cliutils.McpToolAccess) - if toolsAccess == "" { - toolsAccess = os.Getenv(mcpToolAccessEnvVar) + mcp.toolAccess = c.String(cliutils.McpToolAccess) + if mcp.toolAccess == "" { + mcp.toolAccess = os.Getenv(mcpToolAccessEnvVar) } - // Add a flag to allow specifying a specific version of the MCP server - mcpVersion := c.String(cliutils.McpServerVersion) - if mcpVersion == "" { - mcpVersion = "[RELEASE]" + mcp.serverVersion = c.String(cliutils.McpServerVersion) + if mcp.serverVersion == "" { + mcp.serverVersion = defaultServerVersion } +} - executablePath, err := downloadServerExecutable(mcpVersion) +// Run executes the MCP command, downloading the server binary if needed and starting it +func (mcp *Command) Run() error { + executablePath, err := downloadServerExecutable(mcp.serverVersion) if err != nil { return err } - cmd := exec.Command(executablePath, "--toolsets="+toolset, "--tools-access="+toolsAccess) + // Create command to execute the MCP server + cmd := createMcpServerCommand(executablePath, mcp.toolSets, mcp.toolAccess) + + // Log startup information + logStartupInfo(mcp.toolSets, mcp.toolAccess) + + // Execute the command + return cmd.Run() +} + +// createMcpServerCommand creates the exec.Command for the MCP server +func createMcpServerCommand(executablePath, toolSets, toolAccess string) *exec.Cmd { + cmd := exec.Command( + executablePath, + cliutils.McpToolsets+toolSets, + cliutils.McpToolAccess+toolAccess, + ) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Env = os.Environ() - displayToolset := toolset + return cmd +} + +// logStartupInfo logs the MCP server startup parameters +func logStartupInfo(toolSets, toolAccess string) { + displayToolset := toolSets if displayToolset == "" { - displayToolset = "--tools-access=read" + displayToolset = defaultToolsets } - displayToolsAccess := toolsAccess + + displayToolsAccess := toolAccess if displayToolsAccess == "" { - displayToolsAccess = "all-toolsets" + displayToolsAccess = defaultToolAccess } + log.Debug("Starting MCP server with toolset:", displayToolset, "and tools access:", displayToolsAccess) - return cmd.Run() } +// Cmd handles the CLI command execution and argument parsing +func Cmd(c *cli.Context) error { + // Show help if needed + if show, err := cliutils.ShowCmdHelpIfNeeded(c, c.Args()); show || err != nil { + return err + } + // Validate arguments + cmdArg := c.Args().Get(0) + switch cmdArg { + case "update": + return updateMcpServerExecutable() + case "start": + cmd := createAndConfigureCommand(c) + return commands.Exec(cmd) + default: + return cliutils.PrintHelpAndReturnError(fmt.Sprintf("Unknown subcommand: %s", cmdArg), c) + } +} + +// updateMcpServerExecutable forces an update of the MCP server binary +func updateMcpServerExecutable() error { + log.Info("Updating MCP server binary...") + osName, arch, binaryName := getOsArchBinaryInfo() + fullPath, _, err := getLocalBinaryPath(binaryName) + if err != nil { + return err + } + // Always download the latest (or default) version + _, err = downloadBinary(fullPath, defaultServerVersion, osName, arch) + if err != nil { + return err + } + log.Info("MCP server binary updated successfully.") + return nil +} + +// createAndConfigureCommand creates and configures the MCP command +func createAndConfigureCommand(c *cli.Context) *Command { + serverDetails, err := cliutils.CreateArtifactoryDetailsByFlags(c) + if err != nil { + log.Error("Failed to create Artifactory details:", err) + return nil + } + + cmd := NewMcpCommand() + cmd.SetServerDetails(serverDetails) + cmd.getMCPServerArgs(c) + + return cmd +} + +// downloadServerExecutable downloads the MCP server binary if it doesn't exist locally func downloadServerExecutable(version string) (string, error) { - osName, arch, binaryName, err := getOsArchBinaryInfo() + osName, arch, binaryName := getOsArchBinaryInfo() + + fullPath, exists, err := getLocalBinaryPath(binaryName) if err != nil { return "", err } + if exists { + return fullPath, nil + } + + return downloadBinary(fullPath, version, osName, arch) +} + +// getLocalBinaryPath determines the path to the binary and checks if it exists +func getLocalBinaryPath(binaryName string) (fullPath string, exists bool, err error) { jfrogHomeDir, err := coreutils.GetJfrogHomeDir() if err != nil { - return "", fmt.Errorf("failed to get JFrog home directory: %w", err) + return "", false, fmt.Errorf("failed to get JFrog home directory: %w", err) } - targetDir := path.Join(jfrogHomeDir, "cli-mcp") + + targetDir := path.Join(jfrogHomeDir, cliMcpDirName) if err := os.MkdirAll(targetDir, 0777); err != nil { - return "", fmt.Errorf("failed to create directory '%s': %w", targetDir, err) + return "", false, fmt.Errorf("failed to create directory '%s': %w", targetDir, err) } - fullPath := path.Join(targetDir, binaryName) + + fullPath = path.Join(targetDir, binaryName) fileInfo, err := os.Stat(fullPath) if err == nil { // On Unix, check if the file is executable if runtime.GOOS != "windows" && fileInfo.Mode()&0111 == 0 { log.Debug("File exists but is not executable, will re-download:", fullPath) - } else { - log.Debug("MCP server binary already present at:", fullPath) - return fullPath, nil + return fullPath, false, nil } + log.Debug("MCP server binary already present at:", fullPath) + return fullPath, true, nil } else if !os.IsNotExist(err) { - return "", fmt.Errorf("failed to stat '%s': %w", fullPath, err) + return "", false, fmt.Errorf("failed to stat '%s': %w", fullPath, err) } - // Build the download URL (update as needed for your actual release location) - url := fmt.Sprintf("https://releases.jfrog.io/artifactory/cli-mcp-server/v0/%s/%s-%s/%s", version, osName, arch, "cli-mcp-server") + return fullPath, false, nil +} + +// downloadBinary downloads the binary from the remote server +func downloadBinary(fullPath, version, osName, arch string) (string, error) { + // Build the download URL + url := fmt.Sprintf("%s/%s/%s-%s/%s", mcpDownloadBaseURL, version, osName, arch, mcpServerBinaryName) log.Debug("Downloading MCP server from:", url) + resp, err := http.Get(url) if err != nil { return "", fmt.Errorf("failed to download MCP server: %w", err) } - defer func() { err = resp.Body.Close() }() + if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("failed to download MCP server: received status %s", resp.Status) } + + return saveAndMakeExecutable(fullPath, resp.Body) +} + +// saveAndMakeExecutable saves the binary to disk and makes it executable +func saveAndMakeExecutable(fullPath string, content io.Reader) (string, error) { out, err := os.Create(fullPath) if err != nil { return "", fmt.Errorf("failed to create file '%s': %w", fullPath, err) @@ -124,17 +247,21 @@ func downloadServerExecutable(version string) (string, error) { defer func() { err = out.Close() }() - if _, err := io.Copy(out, resp.Body); err != nil { + + if _, err = io.Copy(out, content); err != nil { return "", fmt.Errorf("failed to write binary: %w", err) } - if err := os.Chmod(fullPath, 0755); err != nil && !strings.HasSuffix(binaryName, ".exe") { + + if err = os.Chmod(fullPath, 0755); err != nil && !strings.HasSuffix(fullPath, ".exe") { return "", fmt.Errorf("failed to make binary executable: %w", err) } + log.Debug("MCP server binary downloaded to:", fullPath) return fullPath, nil } -func getOsArchBinaryInfo() (osName, arch, binaryName string, err error) { +// getOsArchBinaryInfo returns the current OS, architecture, and appropriate binary name +func getOsArchBinaryInfo() (osName, arch, binaryName string) { osName = runtime.GOOS arch = runtime.GOARCH binaryName = mcpServerBinaryName diff --git a/main.go b/main.go index 3adc7595e..cf974a244 100644 --- a/main.go +++ b/main.go @@ -279,10 +279,11 @@ func getCommands() ([]cli.Command, error) { { Name: "mcp", Usage: mcpDocs.GetDescription(), - HelpName: corecommon.CreateUsage("mcp start", mcpDocs.GetDescription(), mcpDocs.Usage), + HelpName: corecommon.CreateUsage("mcp", mcpDocs.GetDescription(), mcpDocs.Usage), BashComplete: corecommon.CreateBashCompletionFunc(), + ArgsUsage: common.CreateEnvVars(), Flags: cliutils.GetCommandFlags(cliutils.Mcp), - Action: mcp.McpCmd, + Action: mcp.Cmd, }, { Name: "access-token-create", From b08c50b356e73ddc4324cc833e7186c9c253e417 Mon Sep 17 00:00:00 2001 From: delarea Date: Thu, 22 May 2025 12:31:16 +0300 Subject: [PATCH 08/21] Add download from releases --- general/mcp/cli.go | 148 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 146 insertions(+), 2 deletions(-) diff --git a/general/mcp/cli.go b/general/mcp/cli.go index 222ddf3c5..d1a8769cd 100644 --- a/general/mcp/cli.go +++ b/general/mcp/cli.go @@ -10,6 +10,7 @@ import ( "path" "runtime" "strings" + "time" "github.com/jfrog/jfrog-cli-core/v2/utils/config" @@ -144,19 +145,64 @@ func Cmd(c *cli.Context) error { func updateMcpServerExecutable() error { log.Info("Updating MCP server binary...") osName, arch, binaryName := getOsArchBinaryInfo() - fullPath, _, err := getLocalBinaryPath(binaryName) + fullPath, exists, err := getLocalBinaryPath(binaryName) if err != nil { return err } - // Always download the latest (or default) version + + var currentVersion string + // Check current version if binary exists + if exists { + currentVersion, err = getMcpServerVersion(fullPath) + if err != nil { + log.Warn("Could not determine current MCP server version:", err) + } else { + log.Info("Current MCP server version:", currentVersion) + } + } + + // Check if we already have the latest version + if exists && currentVersion != "" { + latestVersion, err := getLatestMcpServerVersion(osName, arch) + if err != nil { + log.Warn("Could not determine latest MCP server version:", err) + } else if currentVersion == latestVersion { + log.Info("MCP server is already at the latest version:", currentVersion) + return nil + } else { + log.Info("A newer version is available:", latestVersion) + } + } + + // Download the latest version _, err = downloadBinary(fullPath, defaultServerVersion, osName, arch) if err != nil { return err } + + // Check new version after update + newVersion, err := getMcpServerVersion(fullPath) + if err != nil { + log.Warn("Could not determine new MCP server version:", err) + } else { + log.Info("Updated MCP server to version:", newVersion) + } + log.Info("MCP server binary updated successfully.") return nil } +// getMcpServerVersion runs the MCP server binary with --version flag to get its version +func getMcpServerVersion(binaryPath string) (string, error) { + cmd := exec.Command(binaryPath, "--version") + output, err := cmd.Output() + if err != nil { + return "", err + } + // Trim whitespace and return the output + return strings.TrimSpace(string(output)), nil +} + // createAndConfigureCommand creates and configures the MCP command func createAndConfigureCommand(c *cli.Context) *Command { serverDetails, err := cliutils.CreateArtifactoryDetailsByFlags(c) @@ -270,3 +316,101 @@ func getOsArchBinaryInfo() (osName, arch, binaryName string) { } return } + +// getLatestMcpServerVersion determines the latest available version for the given OS and architecture +func getLatestMcpServerVersion(osName, arch string) (string, error) { + // Build the URL for the latest version (same as download URL but we'll make a HEAD request) + url := fmt.Sprintf("%s/%s/%s-%s/%s", mcpDownloadBaseURL, defaultServerVersion, osName, arch, mcpServerBinaryName) + + // Make a HEAD request to get information without downloading the binary + client := &http.Client{ + Timeout: 10 * time.Second, + } + req, err := http.NewRequest("HEAD", url, nil) + if err != nil { + return "", err + } + + resp, err := client.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("failed to check latest version: received status %s", resp.Status) + } + + // Try to get version from headers + // Server might include version information in headers like X-Version, X-Artifact-Version, etc. + // If not available, we'll try to parse it from the ETag or Last-Modified headers + + // For simplicity, we'll fall back to a generic version check + // In a real implementation, you would parse the version from appropriate headers + + // Check if we can determine version from Content-Disposition header + contentDisposition := resp.Header.Get("Content-Disposition") + if contentDisposition != "" { + // Try to extract version from filename, if present + if versionStr := extractVersionFromHeader(contentDisposition); versionStr != "" { + return versionStr, nil + } + } + + // Check if we can get it from an X-Version or similar header + // This is hypothetical - your actual server may use different headers + if version := resp.Header.Get("X-Version"); version != "" { + return version, nil + } + + // If we can't determine the version from headers, we'll make another request + // to the binary with --version flag after downloading + + // As a fallback, download the binary to a temporary location and check its version + tempDir, err := os.MkdirTemp("", "mcp-version-check") + if err != nil { + return "", err + } + defer os.RemoveAll(tempDir) + + tempBinaryPath := path.Join(tempDir, mcpServerBinaryName) + if osName == "windows" { + tempBinaryPath += ".exe" + } + + // Download to temporary location + _, err = downloadBinary(tempBinaryPath, defaultServerVersion, osName, arch) + if err != nil { + return "", err + } + + // Check the version of the downloaded binary + version, err := getMcpServerVersion(tempBinaryPath) + if err != nil { + return "", err + } + + return version, nil +} + +// extractVersionFromHeader attempts to extract version information from a header value +func extractVersionFromHeader(headerValue string) string { + // This is a simple implementation - you might need to adjust based on your header format + // Example: attachment; filename="cli-mcp-server-0.1.0" + if strings.Contains(headerValue, "filename=") { + parts := strings.Split(headerValue, "filename=") + if len(parts) > 1 { + filename := strings.Trim(parts[1], "\"' ") + // Try to extract version from filename + versionParts := strings.Split(filename, "-") + if len(versionParts) > 0 { + lastPart := versionParts[len(versionParts)-1] + // Check if lastPart looks like a version number + if strings.Contains(lastPart, ".") { + return lastPart + } + } + } + } + return "" +} From 3ffa440d54aed9cbcddb121d1f8eedfd6d20cd57 Mon Sep 17 00:00:00 2001 From: delarea Date: Thu, 22 May 2025 12:46:28 +0300 Subject: [PATCH 09/21] refactor --- general/mcp/cli.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/general/mcp/cli.go b/general/mcp/cli.go index d1a8769cd..7cb30e195 100644 --- a/general/mcp/cli.go +++ b/general/mcp/cli.go @@ -2,7 +2,6 @@ package mcp import ( "fmt" - "github.com/jfrog/jfrog-cli-core/v2/common/commands" "io" "net/http" "os" @@ -12,6 +11,8 @@ import ( "strings" "time" + "github.com/jfrog/jfrog-cli-core/v2/common/commands" + "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" From 0b54dcd429c8e53640792c6b0411d70558618b30 Mon Sep 17 00:00:00 2001 From: delarea Date: Thu, 22 May 2025 13:44:46 +0300 Subject: [PATCH 10/21] fix static check --- general/mcp/cli.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/general/mcp/cli.go b/general/mcp/cli.go index 7cb30e195..c72292ae4 100644 --- a/general/mcp/cli.go +++ b/general/mcp/cli.go @@ -165,12 +165,13 @@ func updateMcpServerExecutable() error { // Check if we already have the latest version if exists && currentVersion != "" { latestVersion, err := getLatestMcpServerVersion(osName, arch) - if err != nil { + switch { + case err != nil: log.Warn("Could not determine latest MCP server version:", err) - } else if currentVersion == latestVersion { + case currentVersion == latestVersion: log.Info("MCP server is already at the latest version:", currentVersion) return nil - } else { + default: log.Info("A newer version is available:", latestVersion) } } @@ -257,7 +258,9 @@ func getLocalBinaryPath(binaryName string) (fullPath string, exists bool, err er } log.Debug("MCP server binary already present at:", fullPath) return fullPath, true, nil - } else if !os.IsNotExist(err) { + } + + if !os.IsNotExist(err) { return "", false, fmt.Errorf("failed to stat '%s': %w", fullPath, err) } @@ -327,7 +330,7 @@ func getLatestMcpServerVersion(osName, arch string) (string, error) { client := &http.Client{ Timeout: 10 * time.Second, } - req, err := http.NewRequest("HEAD", url, nil) + req, err := http.NewRequest(http.MethodHead, url, nil) if err != nil { return "", err } From e0aca79ea5298f1f06ccaa35a19fe2bf02f8654a Mon Sep 17 00:00:00 2001 From: delarea Date: Thu, 22 May 2025 13:53:28 +0300 Subject: [PATCH 11/21] add tests --- general/mcp/cli_test.go | 213 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 general/mcp/cli_test.go diff --git a/general/mcp/cli_test.go b/general/mcp/cli_test.go new file mode 100644 index 000000000..6304a2cee --- /dev/null +++ b/general/mcp/cli_test.go @@ -0,0 +1,213 @@ +package mcp + +import ( + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/jfrog/jfrog-cli/utils/tests" + "github.com/stretchr/testify/assert" +) + +func TestGetMCPServerArgs(t *testing.T) { + testRuns := []struct { + name string + flags []string + envVars map[string]string + expectedToolSets string + expectedToolAccess string + expectedVersion string + }{ + { + name: "FlagsOnly", + flags: []string{"toolsets=test", "tools-access=read", "mcp-server-version=1.0.0"}, + envVars: map[string]string{}, + expectedToolSets: "test", + expectedToolAccess: "read", + expectedVersion: "1.0.0", + }, + { + name: "EnvVarsOnly", + flags: []string{}, + envVars: map[string]string{mcpToolSetsEnvVar: "test-env", mcpToolAccessEnvVar: "read-env"}, + expectedToolSets: "test-env", + expectedToolAccess: "read-env", + expectedVersion: defaultServerVersion, + }, + { + name: "FlagsOverrideEnvVars", + flags: []string{"toolsets=test-flag", "tools-access=read-flag"}, + envVars: map[string]string{mcpToolSetsEnvVar: "test-env", mcpToolAccessEnvVar: "read-env"}, + expectedToolSets: "test-flag", + expectedToolAccess: "read-flag", + expectedVersion: defaultServerVersion, + }, + { + name: "NoFlagsOrEnvVars", + flags: []string{}, + envVars: map[string]string{}, + expectedToolSets: "", + expectedToolAccess: "", + expectedVersion: defaultServerVersion, + }, + } + + for _, test := range testRuns { + t.Run(test.name, func(t *testing.T) { + // Save current environment and restore it after the test + originalEnv := make(map[string]string) + for key := range test.envVars { + originalEnv[key] = os.Getenv(key) + } + defer func() { + for key, value := range originalEnv { + os.Setenv(key, value) + } + }() + + // Set environment variables for the test + for key, value := range test.envVars { + os.Setenv(key, value) + } + + // Create CLI context + context, _ := tests.CreateContext(t, test.flags, []string{}) + + // Test getMCPServerArgs + cmd := NewMcpCommand() + cmd.getMCPServerArgs(context) + + // Assert results + assert.Equal(t, test.expectedToolSets, cmd.toolSets) + assert.Equal(t, test.expectedToolAccess, cmd.toolAccess) + assert.Equal(t, test.expectedVersion, cmd.serverVersion) + }) + } +} + +func TestGetOsArchBinaryInfo(t *testing.T) { + osName, arch, binaryName := getOsArchBinaryInfo() + + // Verify OS and architecture are not empty + assert.NotEmpty(t, osName) + assert.NotEmpty(t, arch) + + // Verify binary name has correct format + if osName == "windows" { + assert.Equal(t, mcpServerBinaryName+".exe", binaryName) + } else { + assert.Equal(t, mcpServerBinaryName, binaryName) + } +} + +func TestExtractVersionFromHeader(t *testing.T) { + testCases := []struct { + name string + headerValue string + expected string + }{ + { + name: "ValidHeader", + headerValue: `attachment; filename="cli-mcp-server-0.1.0"`, + expected: "0.1.0", + }, + { + name: "NoVersion", + headerValue: `attachment; filename="cli-mcp-server"`, + expected: "", + }, + { + name: "NoFilename", + headerValue: `attachment;`, + expected: "", + }, + { + name: "EmptyHeader", + headerValue: "", + expected: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := extractVersionFromHeader(tc.headerValue) + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestCmd(t *testing.T) { + testCases := []struct { + name string + args []string + expectError bool + errorMsg string + }{ + { + name: "NoArgs", + args: []string{}, + expectError: true, + errorMsg: "Unknown subcommand: ", + }, + { + name: "InvalidSubcommand", + args: []string{"invalid"}, + expectError: true, + errorMsg: "Unknown subcommand: invalid", + }, + // Update and start subcommands require more complex mocking to test properly + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + context, _ := tests.CreateContext(t, []string{}, tc.args) + err := Cmd(context) + + if tc.expectError { + assert.Error(t, err) + assert.Contains(t, err.Error(), tc.errorMsg) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestGetLocalBinaryPath(t *testing.T) { + // Skip test since we can't mock coreutils.GetJfrogHomeDir directly + t.Skip("Skipping test as it requires mocking package-level functions") +} + +func TestGetMcpServerVersion(t *testing.T) { + // Create a temporary directory for testing + tempDir, err := os.MkdirTemp("", "mcp-version-test") + assert.NoError(t, err) + defer os.RemoveAll(tempDir) + + // Create a fake binary that outputs a version when called with --version + binaryPath := filepath.Join(tempDir, "fake-binary") + if runtime.GOOS == "windows" { + binaryPath += ".exe" + } + + // Create a simple shell script that echoes a version + var scriptContent string + if runtime.GOOS == "windows" { + scriptContent = "@echo off\r\necho 1.2.3\r\n" + } else { + scriptContent = "#!/bin/sh\necho \"1.2.3\"\n" + } + + err = os.WriteFile(binaryPath, []byte(scriptContent), 0755) + assert.NoError(t, err) + + // Test the getMcpServerVersion function + version, err := getMcpServerVersion(binaryPath) + assert.NoError(t, err) + assert.Equal(t, "1.2.3", version) + + // Test with non-existent binary + _, err = getMcpServerVersion(filepath.Join(tempDir, "non-existent")) + assert.Error(t, err) +} From 0d5a1f027c061a9eb17d2be533a550ed4d0c2929 Mon Sep 17 00:00:00 2001 From: delarea Date: Thu, 22 May 2025 14:01:50 +0300 Subject: [PATCH 12/21] fix static check --- general/mcp/cli.go | 33 +++++++++++++++++--- general/mcp/cli_test.go | 68 ++++++++++++++++++++++------------------- 2 files changed, 64 insertions(+), 37 deletions(-) diff --git a/general/mcp/cli.go b/general/mcp/cli.go index c72292ae4..0edda04ff 100644 --- a/general/mcp/cli.go +++ b/general/mcp/cli.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "net/http" + "net/url" "os" "os/exec" "path" @@ -270,10 +271,21 @@ func getLocalBinaryPath(binaryName string) (fullPath string, exists bool, err er // downloadBinary downloads the binary from the remote server func downloadBinary(fullPath, version, osName, arch string) (string, error) { // Build the download URL - url := fmt.Sprintf("%s/%s/%s-%s/%s", mcpDownloadBaseURL, version, osName, arch, mcpServerBinaryName) - log.Debug("Downloading MCP server from:", url) + urlStr := fmt.Sprintf("%s/%s/%s-%s/%s", mcpDownloadBaseURL, version, osName, arch, mcpServerBinaryName) + log.Debug("Downloading MCP server from:", urlStr) - resp, err := http.Get(url) + // Validate URL + parsedURL, err := url.Parse(urlStr) + if err != nil { + return "", fmt.Errorf("invalid URL: %w", err) + } + + // Ensure URL is using HTTPS + if parsedURL.Scheme != "https" { + return "", fmt.Errorf("URL must use HTTPS scheme") + } + + resp, err := http.Get(parsedURL.String()) if err != nil { return "", fmt.Errorf("failed to download MCP server: %w", err) } @@ -324,13 +336,24 @@ func getOsArchBinaryInfo() (osName, arch, binaryName string) { // getLatestMcpServerVersion determines the latest available version for the given OS and architecture func getLatestMcpServerVersion(osName, arch string) (string, error) { // Build the URL for the latest version (same as download URL but we'll make a HEAD request) - url := fmt.Sprintf("%s/%s/%s-%s/%s", mcpDownloadBaseURL, defaultServerVersion, osName, arch, mcpServerBinaryName) + urlStr := fmt.Sprintf("%s/%s/%s-%s/%s", mcpDownloadBaseURL, defaultServerVersion, osName, arch, mcpServerBinaryName) + + // Validate URL + parsedURL, err := url.Parse(urlStr) + if err != nil { + return "", fmt.Errorf("invalid URL: %w", err) + } + + // Ensure URL is using HTTPS + if parsedURL.Scheme != "https" { + return "", fmt.Errorf("URL must use HTTPS scheme") + } // Make a HEAD request to get information without downloading the binary client := &http.Client{ Timeout: 10 * time.Second, } - req, err := http.NewRequest(http.MethodHead, url, nil) + req, err := http.NewRequest(http.MethodHead, parsedURL.String(), nil) if err != nil { return "", err } diff --git a/general/mcp/cli_test.go b/general/mcp/cli_test.go index 6304a2cee..cad14c7ce 100644 --- a/general/mcp/cli_test.go +++ b/general/mcp/cli_test.go @@ -12,44 +12,44 @@ import ( func TestGetMCPServerArgs(t *testing.T) { testRuns := []struct { - name string - flags []string - envVars map[string]string - expectedToolSets string + name string + flags []string + envVars map[string]string + expectedToolSets string expectedToolAccess string - expectedVersion string + expectedVersion string }{ { - name: "FlagsOnly", - flags: []string{"toolsets=test", "tools-access=read", "mcp-server-version=1.0.0"}, - envVars: map[string]string{}, - expectedToolSets: "test", + name: "FlagsOnly", + flags: []string{"toolsets=test", "tools-access=read", "mcp-server-version=1.0.0"}, + envVars: map[string]string{}, + expectedToolSets: "test", expectedToolAccess: "read", - expectedVersion: "1.0.0", + expectedVersion: "1.0.0", }, { - name: "EnvVarsOnly", - flags: []string{}, - envVars: map[string]string{mcpToolSetsEnvVar: "test-env", mcpToolAccessEnvVar: "read-env"}, - expectedToolSets: "test-env", + name: "EnvVarsOnly", + flags: []string{}, + envVars: map[string]string{mcpToolSetsEnvVar: "test-env", mcpToolAccessEnvVar: "read-env"}, + expectedToolSets: "test-env", expectedToolAccess: "read-env", - expectedVersion: defaultServerVersion, + expectedVersion: defaultServerVersion, }, { - name: "FlagsOverrideEnvVars", - flags: []string{"toolsets=test-flag", "tools-access=read-flag"}, - envVars: map[string]string{mcpToolSetsEnvVar: "test-env", mcpToolAccessEnvVar: "read-env"}, - expectedToolSets: "test-flag", + name: "FlagsOverrideEnvVars", + flags: []string{"toolsets=test-flag", "tools-access=read-flag"}, + envVars: map[string]string{mcpToolSetsEnvVar: "test-env", mcpToolAccessEnvVar: "read-env"}, + expectedToolSets: "test-flag", expectedToolAccess: "read-flag", - expectedVersion: defaultServerVersion, + expectedVersion: defaultServerVersion, }, { - name: "NoFlagsOrEnvVars", - flags: []string{}, - envVars: map[string]string{}, - expectedToolSets: "", + name: "NoFlagsOrEnvVars", + flags: []string{}, + envVars: map[string]string{}, + expectedToolSets: "", expectedToolAccess: "", - expectedVersion: defaultServerVersion, + expectedVersion: defaultServerVersion, }, } @@ -62,22 +62,26 @@ func TestGetMCPServerArgs(t *testing.T) { } defer func() { for key, value := range originalEnv { - os.Setenv(key, value) + if err := os.Setenv(key, value); err != nil { + t.Logf("Failed to restore environment variable %s: %v", key, err) + } } }() // Set environment variables for the test for key, value := range test.envVars { - os.Setenv(key, value) + if err := os.Setenv(key, value); err != nil { + t.Fatalf("Failed to set environment variable %s: %v", key, err) + } } // Create CLI context context, _ := tests.CreateContext(t, test.flags, []string{}) - + // Test getMCPServerArgs cmd := NewMcpCommand() cmd.getMCPServerArgs(context) - + // Assert results assert.Equal(t, test.expectedToolSets, cmd.toolSets) assert.Equal(t, test.expectedToolAccess, cmd.toolAccess) @@ -88,11 +92,11 @@ func TestGetMCPServerArgs(t *testing.T) { func TestGetOsArchBinaryInfo(t *testing.T) { osName, arch, binaryName := getOsArchBinaryInfo() - + // Verify OS and architecture are not empty assert.NotEmpty(t, osName) assert.NotEmpty(t, arch) - + // Verify binary name has correct format if osName == "windows" { assert.Equal(t, mcpServerBinaryName+".exe", binaryName) @@ -163,7 +167,7 @@ func TestCmd(t *testing.T) { t.Run(tc.name, func(t *testing.T) { context, _ := tests.CreateContext(t, []string{}, tc.args) err := Cmd(context) - + if tc.expectError { assert.Error(t, err) assert.Contains(t, err.Error(), tc.errorMsg) From 59b21352cd9f8cdac2c6a1773095ee117c501dd3 Mon Sep 17 00:00:00 2001 From: delarea Date: Thu, 22 May 2025 14:29:43 +0300 Subject: [PATCH 13/21] fix windows test --- general/mcp/cli_test.go | 67 ++++++++++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 17 deletions(-) diff --git a/general/mcp/cli_test.go b/general/mcp/cli_test.go index cad14c7ce..dc3be9737 100644 --- a/general/mcp/cli_test.go +++ b/general/mcp/cli_test.go @@ -2,6 +2,7 @@ package mcp import ( "os" + "os/exec" "path/filepath" "runtime" "testing" @@ -189,27 +190,59 @@ func TestGetMcpServerVersion(t *testing.T) { assert.NoError(t, err) defer os.RemoveAll(tempDir) - // Create a fake binary that outputs a version when called with --version - binaryPath := filepath.Join(tempDir, "fake-binary") + // For Windows, we need a different approach as batch scripts don't work the same way if runtime.GOOS == "windows" { - binaryPath += ".exe" - } - - // Create a simple shell script that echoes a version - var scriptContent string - if runtime.GOOS == "windows" { - scriptContent = "@echo off\r\necho 1.2.3\r\n" + // On Windows, we'll create a small Go program and compile it + tempSrcDir := filepath.Join(tempDir, "src") + err = os.Mkdir(tempSrcDir, 0755) + assert.NoError(t, err) + + // Create a simple Go program that prints the version + srcFile := filepath.Join(tempSrcDir, "main.go") + srcContent := `package main + +import "fmt" + +func main() { + fmt.Println("1.2.3") +} +` + err = os.WriteFile(srcFile, []byte(srcContent), 0644) + assert.NoError(t, err) + + // Compile the program + binaryPath := filepath.Join(tempDir, "fake-binary.exe") + cmd := exec.Command("go", "build", "-o", binaryPath, srcFile) + err = cmd.Run() + + // If compilation fails, skip the test + if err != nil { + t.Skip("Skipping test, unable to compile test binary:", err) + return + } + + // Test with the compiled binary + version, err := getMcpServerVersion(binaryPath) + assert.NoError(t, err) + assert.Equal(t, "1.2.3", version) } else { - scriptContent = "#!/bin/sh\necho \"1.2.3\"\n" + // On Unix systems, we can use a shell script + binaryPath := filepath.Join(tempDir, "fake-binary") + scriptContent := "#!/bin/sh\necho \"1.2.3\"\n" + + err = os.WriteFile(binaryPath, []byte(scriptContent), 0755) + assert.NoError(t, err) + + // Test the getMcpServerVersion function + version, err := getMcpServerVersion(binaryPath) + assert.NoError(t, err) + assert.Equal(t, "1.2.3", version) } - err = os.WriteFile(binaryPath, []byte(scriptContent), 0755) - assert.NoError(t, err) - - // Test the getMcpServerVersion function - version, err := getMcpServerVersion(binaryPath) - assert.NoError(t, err) - assert.Equal(t, "1.2.3", version) + // Test with non-existent binary + _, err = getMcpServerVersion(filepath.Join(tempDir, "non-existent")) + assert.Error(t, err) +} // Test with non-existent binary _, err = getMcpServerVersion(filepath.Join(tempDir, "non-existent")) From 9aad0f02e30d38544e191942ad0fc4cadde118f9 Mon Sep 17 00:00:00 2001 From: delarea Date: Thu, 22 May 2025 14:52:40 +0300 Subject: [PATCH 14/21] fix --- general/mcp/cli_test.go | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/general/mcp/cli_test.go b/general/mcp/cli_test.go index dc3be9737..123e3e360 100644 --- a/general/mcp/cli_test.go +++ b/general/mcp/cli_test.go @@ -196,7 +196,7 @@ func TestGetMcpServerVersion(t *testing.T) { tempSrcDir := filepath.Join(tempDir, "src") err = os.Mkdir(tempSrcDir, 0755) assert.NoError(t, err) - + // Create a simple Go program that prints the version srcFile := filepath.Join(tempSrcDir, "main.go") srcContent := `package main @@ -209,18 +209,18 @@ func main() { ` err = os.WriteFile(srcFile, []byte(srcContent), 0644) assert.NoError(t, err) - + // Compile the program binaryPath := filepath.Join(tempDir, "fake-binary.exe") cmd := exec.Command("go", "build", "-o", binaryPath, srcFile) err = cmd.Run() - + // If compilation fails, skip the test if err != nil { t.Skip("Skipping test, unable to compile test binary:", err) return } - + // Test with the compiled binary version, err := getMcpServerVersion(binaryPath) assert.NoError(t, err) @@ -229,10 +229,10 @@ func main() { // On Unix systems, we can use a shell script binaryPath := filepath.Join(tempDir, "fake-binary") scriptContent := "#!/bin/sh\necho \"1.2.3\"\n" - + err = os.WriteFile(binaryPath, []byte(scriptContent), 0755) assert.NoError(t, err) - + // Test the getMcpServerVersion function version, err := getMcpServerVersion(binaryPath) assert.NoError(t, err) @@ -243,8 +243,3 @@ func main() { _, err = getMcpServerVersion(filepath.Join(tempDir, "non-existent")) assert.Error(t, err) } - - // Test with non-existent binary - _, err = getMcpServerVersion(filepath.Join(tempDir, "non-existent")) - assert.Error(t, err) -} From 6a0659fea605514d35e25e729bbb749137dbc7d1 Mon Sep 17 00:00:00 2001 From: delarea Date: Sun, 25 May 2025 16:25:37 +0300 Subject: [PATCH 15/21] remove update --- general/mcp/cli.go | 206 +++----------------------------- utils/cliutils/commandsflags.go | 4 +- 2 files changed, 18 insertions(+), 192 deletions(-) diff --git a/general/mcp/cli.go b/general/mcp/cli.go index 0edda04ff..866a4dba1 100644 --- a/general/mcp/cli.go +++ b/general/mcp/cli.go @@ -1,6 +1,7 @@ package mcp import ( + "errors" "fmt" "io" "net/http" @@ -10,7 +11,6 @@ import ( "path" "runtime" "strings" - "time" "github.com/jfrog/jfrog-cli-core/v2/common/commands" @@ -60,9 +60,10 @@ func (mcp *Command) CommandName() string { return "jf_mcp_start" } -// getMCPServerArgs extracts and sets command arguments from CLI flags or environment variables +// McpToolset - specifies the toolsets to use (e.g artifactory, distribution, etc.) +// toolAccess - specifies the tool access level (e.g read, write, etc.) +// McpServerVersion - specifies the version of the MCP server to run func (mcp *Command) getMCPServerArgs(c *cli.Context) { - // Accept --toolset and --tool-access from flags or env vars (flags win) mcp.toolSets = c.String(cliutils.McpToolsets) if mcp.toolSets == "" { mcp.toolSets = os.Getenv(mcpToolSetsEnvVar) @@ -71,7 +72,6 @@ func (mcp *Command) getMCPServerArgs(c *cli.Context) { if mcp.toolAccess == "" { mcp.toolAccess = os.Getenv(mcpToolAccessEnvVar) } - // Add a flag to allow specifying a specific version of the MCP server mcp.serverVersion = c.String(cliutils.McpServerVersion) if mcp.serverVersion == "" { mcp.serverVersion = defaultServerVersion @@ -120,8 +120,7 @@ func logStartupInfo(toolSets, toolAccess string) { if displayToolsAccess == "" { displayToolsAccess = defaultToolAccess } - - log.Debug("Starting MCP server with toolset:", displayToolset, "and tools access:", displayToolsAccess) + log.Info("Starting MCP server with toolset:", displayToolset, "and tools access:", displayToolsAccess) } // Cmd handles the CLI command execution and argument parsing @@ -130,69 +129,9 @@ func Cmd(c *cli.Context) error { if show, err := cliutils.ShowCmdHelpIfNeeded(c, c.Args()); show || err != nil { return err } - // Validate arguments - cmdArg := c.Args().Get(0) - switch cmdArg { - case "update": - return updateMcpServerExecutable() - case "start": - cmd := createAndConfigureCommand(c) - return commands.Exec(cmd) - default: - return cliutils.PrintHelpAndReturnError(fmt.Sprintf("Unknown subcommand: %s", cmdArg), c) - } -} - -// updateMcpServerExecutable forces an update of the MCP server binary -func updateMcpServerExecutable() error { - log.Info("Updating MCP server binary...") - osName, arch, binaryName := getOsArchBinaryInfo() - fullPath, exists, err := getLocalBinaryPath(binaryName) - if err != nil { - return err - } - - var currentVersion string - // Check current version if binary exists - if exists { - currentVersion, err = getMcpServerVersion(fullPath) - if err != nil { - log.Warn("Could not determine current MCP server version:", err) - } else { - log.Info("Current MCP server version:", currentVersion) - } - } - - // Check if we already have the latest version - if exists && currentVersion != "" { - latestVersion, err := getLatestMcpServerVersion(osName, arch) - switch { - case err != nil: - log.Warn("Could not determine latest MCP server version:", err) - case currentVersion == latestVersion: - log.Info("MCP server is already at the latest version:", currentVersion) - return nil - default: - log.Info("A newer version is available:", latestVersion) - } - } - - // Download the latest version - _, err = downloadBinary(fullPath, defaultServerVersion, osName, arch) - if err != nil { - return err - } + cmd := createAndConfigureCommand(c) + return commands.Exec(cmd) - // Check new version after update - newVersion, err := getMcpServerVersion(fullPath) - if err != nil { - log.Warn("Could not determine new MCP server version:", err) - } else { - log.Info("Updated MCP server to version:", newVersion) - } - - log.Info("MCP server binary updated successfully.") - return nil } // getMcpServerVersion runs the MCP server binary with --version flag to get its version @@ -224,17 +163,16 @@ func createAndConfigureCommand(c *cli.Context) *Command { // downloadServerExecutable downloads the MCP server binary if it doesn't exist locally func downloadServerExecutable(version string) (string, error) { osName, arch, binaryName := getOsArchBinaryInfo() - - fullPath, exists, err := getLocalBinaryPath(binaryName) + targetPath, exists, err := getLocalBinaryPath(binaryName) if err != nil { return "", err } if exists { - return fullPath, nil + return targetPath, nil } - return downloadBinary(fullPath, version, osName, arch) + return downloadBinary(targetPath, version, osName, arch) } // getLocalBinaryPath determines the path to the binary and checks if it exists @@ -245,7 +183,7 @@ func getLocalBinaryPath(binaryName string) (fullPath string, exists bool, err er } targetDir := path.Join(jfrogHomeDir, cliMcpDirName) - if err := os.MkdirAll(targetDir, 0777); err != nil { + if err = os.MkdirAll(targetDir, 0777); err != nil { return "", false, fmt.Errorf("failed to create directory '%s': %w", targetDir, err) } @@ -269,7 +207,7 @@ func getLocalBinaryPath(binaryName string) (fullPath string, exists bool, err er } // downloadBinary downloads the binary from the remote server -func downloadBinary(fullPath, version, osName, arch string) (string, error) { +func downloadBinary(targetPath, version, osName, arch string) (string, error) { // Build the download URL urlStr := fmt.Sprintf("%s/%s/%s-%s/%s", mcpDownloadBaseURL, version, osName, arch, mcpServerBinaryName) log.Debug("Downloading MCP server from:", urlStr) @@ -280,24 +218,19 @@ func downloadBinary(fullPath, version, osName, arch string) (string, error) { return "", fmt.Errorf("invalid URL: %w", err) } - // Ensure URL is using HTTPS - if parsedURL.Scheme != "https" { - return "", fmt.Errorf("URL must use HTTPS scheme") - } - resp, err := http.Get(parsedURL.String()) if err != nil { return "", fmt.Errorf("failed to download MCP server: %w", err) } defer func() { - err = resp.Body.Close() + err = errors.Join(err, resp.Body.Close()) }() if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("failed to download MCP server: received status %s", resp.Status) } - return saveAndMakeExecutable(fullPath, resp.Body) + return saveAndMakeExecutable(targetPath, resp.Body) } // saveAndMakeExecutable saves the binary to disk and makes it executable @@ -307,7 +240,7 @@ func saveAndMakeExecutable(fullPath string, content io.Reader) (string, error) { return "", fmt.Errorf("failed to create file '%s': %w", fullPath, err) } defer func() { - err = out.Close() + err = errors.Join(err, out.Close()) }() if _, err = io.Copy(out, content); err != nil { @@ -332,112 +265,3 @@ func getOsArchBinaryInfo() (osName, arch, binaryName string) { } return } - -// getLatestMcpServerVersion determines the latest available version for the given OS and architecture -func getLatestMcpServerVersion(osName, arch string) (string, error) { - // Build the URL for the latest version (same as download URL but we'll make a HEAD request) - urlStr := fmt.Sprintf("%s/%s/%s-%s/%s", mcpDownloadBaseURL, defaultServerVersion, osName, arch, mcpServerBinaryName) - - // Validate URL - parsedURL, err := url.Parse(urlStr) - if err != nil { - return "", fmt.Errorf("invalid URL: %w", err) - } - - // Ensure URL is using HTTPS - if parsedURL.Scheme != "https" { - return "", fmt.Errorf("URL must use HTTPS scheme") - } - - // Make a HEAD request to get information without downloading the binary - client := &http.Client{ - Timeout: 10 * time.Second, - } - req, err := http.NewRequest(http.MethodHead, parsedURL.String(), nil) - if err != nil { - return "", err - } - - resp, err := client.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("failed to check latest version: received status %s", resp.Status) - } - - // Try to get version from headers - // Server might include version information in headers like X-Version, X-Artifact-Version, etc. - // If not available, we'll try to parse it from the ETag or Last-Modified headers - - // For simplicity, we'll fall back to a generic version check - // In a real implementation, you would parse the version from appropriate headers - - // Check if we can determine version from Content-Disposition header - contentDisposition := resp.Header.Get("Content-Disposition") - if contentDisposition != "" { - // Try to extract version from filename, if present - if versionStr := extractVersionFromHeader(contentDisposition); versionStr != "" { - return versionStr, nil - } - } - - // Check if we can get it from an X-Version or similar header - // This is hypothetical - your actual server may use different headers - if version := resp.Header.Get("X-Version"); version != "" { - return version, nil - } - - // If we can't determine the version from headers, we'll make another request - // to the binary with --version flag after downloading - - // As a fallback, download the binary to a temporary location and check its version - tempDir, err := os.MkdirTemp("", "mcp-version-check") - if err != nil { - return "", err - } - defer os.RemoveAll(tempDir) - - tempBinaryPath := path.Join(tempDir, mcpServerBinaryName) - if osName == "windows" { - tempBinaryPath += ".exe" - } - - // Download to temporary location - _, err = downloadBinary(tempBinaryPath, defaultServerVersion, osName, arch) - if err != nil { - return "", err - } - - // Check the version of the downloaded binary - version, err := getMcpServerVersion(tempBinaryPath) - if err != nil { - return "", err - } - - return version, nil -} - -// extractVersionFromHeader attempts to extract version information from a header value -func extractVersionFromHeader(headerValue string) string { - // This is a simple implementation - you might need to adjust based on your header format - // Example: attachment; filename="cli-mcp-server-0.1.0" - if strings.Contains(headerValue, "filename=") { - parts := strings.Split(headerValue, "filename=") - if len(parts) > 1 { - filename := strings.Trim(parts[1], "\"' ") - // Try to extract version from filename - versionParts := strings.Split(filename, "-") - if len(versionParts) > 0 { - lastPart := versionParts[len(versionParts)-1] - // Check if lastPart looks like a version number - if strings.Contains(lastPart, ".") { - return lastPart - } - } - } - } - return "" -} diff --git a/utils/cliutils/commandsflags.go b/utils/cliutils/commandsflags.go index e378d2d99..433fcc7e2 100644 --- a/utils/cliutils/commandsflags.go +++ b/utils/cliutils/commandsflags.go @@ -2060,7 +2060,9 @@ var commandFlags = map[string][]string{ Setup: { serverId, url, user, password, accessToken, sshPassphrase, sshKeyPath, ClientCertPath, ClientCertKeyPath, Project, setupRepo, }, - Mcp: {McpToolsets, McpToolAccess, McpServerVersion}, + Mcp: { + McpToolsets, McpToolAccess, McpServerVersion, + }, } func GetCommandFlags(cmd string) []cli.Flag { From 9b60dbc83a3af27b404b9c5d681f22852e333f7d Mon Sep 17 00:00:00 2001 From: delarea Date: Sun, 25 May 2025 16:28:16 +0300 Subject: [PATCH 16/21] remove update cmd --- docs/general/mcp/help.go | 4 ++-- general/mcp/cli_test.go | 36 ------------------------------------ 2 files changed, 2 insertions(+), 38 deletions(-) diff --git a/docs/general/mcp/help.go b/docs/general/mcp/help.go index ed1ca0bd8..a68bdc7a5 100644 --- a/docs/general/mcp/help.go +++ b/docs/general/mcp/help.go @@ -1,7 +1,7 @@ package mcp -var Usage = []string{"mcp start", "mcp update"} +var Usage = []string{"mcp start"} func GetDescription() string { - return "Start or update the JFrog MCP server and begin using it with your MCP client of your choice." + return "Start the JFrog MCP server and begin using it with your MCP client of your choice." } diff --git a/general/mcp/cli_test.go b/general/mcp/cli_test.go index 123e3e360..b6bd66e78 100644 --- a/general/mcp/cli_test.go +++ b/general/mcp/cli_test.go @@ -106,42 +106,6 @@ func TestGetOsArchBinaryInfo(t *testing.T) { } } -func TestExtractVersionFromHeader(t *testing.T) { - testCases := []struct { - name string - headerValue string - expected string - }{ - { - name: "ValidHeader", - headerValue: `attachment; filename="cli-mcp-server-0.1.0"`, - expected: "0.1.0", - }, - { - name: "NoVersion", - headerValue: `attachment; filename="cli-mcp-server"`, - expected: "", - }, - { - name: "NoFilename", - headerValue: `attachment;`, - expected: "", - }, - { - name: "EmptyHeader", - headerValue: "", - expected: "", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - result := extractVersionFromHeader(tc.headerValue) - assert.Equal(t, tc.expected, result) - }) - } -} - func TestCmd(t *testing.T) { testCases := []struct { name string From 9dacc863a5405fed63f27cb03cb2d4fa64ee9633 Mon Sep 17 00:00:00 2001 From: delarea Date: Sun, 25 May 2025 16:39:17 +0300 Subject: [PATCH 17/21] always download binary --- general/mcp/cli.go | 34 +++++++--------------------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/general/mcp/cli.go b/general/mcp/cli.go index 866a4dba1..15891385d 100644 --- a/general/mcp/cli.go +++ b/general/mcp/cli.go @@ -120,7 +120,7 @@ func logStartupInfo(toolSets, toolAccess string) { if displayToolsAccess == "" { displayToolsAccess = defaultToolAccess } - log.Info("Starting MCP server with toolset:", displayToolset, "and tools access:", displayToolsAccess) + log.Info(fmt.Sprintf("Starting MCP server | toolset: %s | tools access: %s", displayToolset, displayToolsAccess)) } // Cmd handles the CLI command execution and argument parsing @@ -163,54 +163,34 @@ func createAndConfigureCommand(c *cli.Context) *Command { // downloadServerExecutable downloads the MCP server binary if it doesn't exist locally func downloadServerExecutable(version string) (string, error) { osName, arch, binaryName := getOsArchBinaryInfo() - targetPath, exists, err := getLocalBinaryPath(binaryName) + targetPath, err := getLocalBinaryPath(binaryName) if err != nil { return "", err } - - if exists { - return targetPath, nil - } - return downloadBinary(targetPath, version, osName, arch) } // getLocalBinaryPath determines the path to the binary and checks if it exists -func getLocalBinaryPath(binaryName string) (fullPath string, exists bool, err error) { +func getLocalBinaryPath(binaryName string) (fullPath string, err error) { jfrogHomeDir, err := coreutils.GetJfrogHomeDir() if err != nil { - return "", false, fmt.Errorf("failed to get JFrog home directory: %w", err) + return "", fmt.Errorf("failed to get JFrog home directory: %w", err) } targetDir := path.Join(jfrogHomeDir, cliMcpDirName) if err = os.MkdirAll(targetDir, 0777); err != nil { - return "", false, fmt.Errorf("failed to create directory '%s': %w", targetDir, err) + return "", fmt.Errorf("failed to create directory '%s': %w", targetDir, err) } fullPath = path.Join(targetDir, binaryName) - fileInfo, err := os.Stat(fullPath) - if err == nil { - // On Unix, check if the file is executable - if runtime.GOOS != "windows" && fileInfo.Mode()&0111 == 0 { - log.Debug("File exists but is not executable, will re-download:", fullPath) - return fullPath, false, nil - } - log.Debug("MCP server binary already present at:", fullPath) - return fullPath, true, nil - } - - if !os.IsNotExist(err) { - return "", false, fmt.Errorf("failed to stat '%s': %w", fullPath, err) - } - - return fullPath, false, nil + return fullPath, nil } // downloadBinary downloads the binary from the remote server func downloadBinary(targetPath, version, osName, arch string) (string, error) { // Build the download URL urlStr := fmt.Sprintf("%s/%s/%s-%s/%s", mcpDownloadBaseURL, version, osName, arch, mcpServerBinaryName) - log.Debug("Downloading MCP server from:", urlStr) + log.Info("Downloading MCP server from:", urlStr) // Validate URL parsedURL, err := url.Parse(urlStr) From 7fd84160c33968c7aec124d4be4d140455b25272 Mon Sep 17 00:00:00 2001 From: delarea Date: Sun, 25 May 2025 16:49:27 +0300 Subject: [PATCH 18/21] use fileutil to download --- general/mcp/cli.go | 35 +++++------------------------------ 1 file changed, 5 insertions(+), 30 deletions(-) diff --git a/general/mcp/cli.go b/general/mcp/cli.go index 15891385d..5440f745a 100644 --- a/general/mcp/cli.go +++ b/general/mcp/cli.go @@ -3,9 +3,8 @@ package mcp import ( "errors" "fmt" + "github.com/jfrog/build-info-go/utils" "io" - "net/http" - "net/url" "os" "os/exec" "path" @@ -167,7 +166,10 @@ func downloadServerExecutable(version string) (string, error) { if err != nil { return "", err } - return downloadBinary(targetPath, version, osName, arch) + urlStr := fmt.Sprintf("%s/%s/%s-%s/%s", mcpDownloadBaseURL, version, osName, arch, mcpServerBinaryName) + log.Info("Downloading MCP server from:", urlStr) + return targetPath, utils.DownloadFile(targetPath, urlStr) + } // getLocalBinaryPath determines the path to the binary and checks if it exists @@ -186,33 +188,6 @@ func getLocalBinaryPath(binaryName string) (fullPath string, err error) { return fullPath, nil } -// downloadBinary downloads the binary from the remote server -func downloadBinary(targetPath, version, osName, arch string) (string, error) { - // Build the download URL - urlStr := fmt.Sprintf("%s/%s/%s-%s/%s", mcpDownloadBaseURL, version, osName, arch, mcpServerBinaryName) - log.Info("Downloading MCP server from:", urlStr) - - // Validate URL - parsedURL, err := url.Parse(urlStr) - if err != nil { - return "", fmt.Errorf("invalid URL: %w", err) - } - - resp, err := http.Get(parsedURL.String()) - if err != nil { - return "", fmt.Errorf("failed to download MCP server: %w", err) - } - defer func() { - err = errors.Join(err, resp.Body.Close()) - }() - - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("failed to download MCP server: received status %s", resp.Status) - } - - return saveAndMakeExecutable(targetPath, resp.Body) -} - // saveAndMakeExecutable saves the binary to disk and makes it executable func saveAndMakeExecutable(fullPath string, content io.Reader) (string, error) { out, err := os.Create(fullPath) From 0543115f092619385990f7c69b12dee74beb93af Mon Sep 17 00:00:00 2001 From: delarea Date: Sun, 25 May 2025 16:51:42 +0300 Subject: [PATCH 19/21] fix static check --- general/mcp/cli.go | 27 ++------------------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/general/mcp/cli.go b/general/mcp/cli.go index 5440f745a..60705a062 100644 --- a/general/mcp/cli.go +++ b/general/mcp/cli.go @@ -1,16 +1,15 @@ package mcp import ( - "errors" "fmt" - "github.com/jfrog/build-info-go/utils" - "io" "os" "os/exec" "path" "runtime" "strings" + "github.com/jfrog/build-info-go/utils" + "github.com/jfrog/jfrog-cli-core/v2/common/commands" "github.com/jfrog/jfrog-cli-core/v2/utils/config" @@ -188,28 +187,6 @@ func getLocalBinaryPath(binaryName string) (fullPath string, err error) { return fullPath, nil } -// saveAndMakeExecutable saves the binary to disk and makes it executable -func saveAndMakeExecutable(fullPath string, content io.Reader) (string, error) { - out, err := os.Create(fullPath) - if err != nil { - return "", fmt.Errorf("failed to create file '%s': %w", fullPath, err) - } - defer func() { - err = errors.Join(err, out.Close()) - }() - - if _, err = io.Copy(out, content); err != nil { - return "", fmt.Errorf("failed to write binary: %w", err) - } - - if err = os.Chmod(fullPath, 0755); err != nil && !strings.HasSuffix(fullPath, ".exe") { - return "", fmt.Errorf("failed to make binary executable: %w", err) - } - - log.Debug("MCP server binary downloaded to:", fullPath) - return fullPath, nil -} - // getOsArchBinaryInfo returns the current OS, architecture, and appropriate binary name func getOsArchBinaryInfo() (osName, arch, binaryName string) { osName = runtime.GOOS From 0494c99b490b6b22de61269349b4981d8701adaf Mon Sep 17 00:00:00 2001 From: delarea Date: Sun, 25 May 2025 17:19:08 +0300 Subject: [PATCH 20/21] fix --- general/mcp/cli.go | 46 +++++++++++++++++++---------------------- general/mcp/cli_test.go | 2 +- 2 files changed, 22 insertions(+), 26 deletions(-) diff --git a/general/mcp/cli.go b/general/mcp/cli.go index 60705a062..e22c6e644 100644 --- a/general/mcp/cli.go +++ b/general/mcp/cli.go @@ -58,18 +58,31 @@ func (mcp *Command) CommandName() string { return "jf_mcp_start" } -// McpToolset - specifies the toolsets to use (e.g artifactory, distribution, etc.) -// toolAccess - specifies the tool access level (e.g read, write, etc.) -// McpServerVersion - specifies the version of the MCP server to run -func (mcp *Command) getMCPServerArgs(c *cli.Context) { +// resolveMCPServerArgs resolves the MCP server arguments (toolSets, toolAccess, serverVersion) +// in the following order for each value: +// 1. CLI flag +// 2. Environment variable +// 3. Default constant +func (mcp *Command) resolveMCPServerArgs(c *cli.Context) { + // Resolve toolSets: CLI flag -> Env var -> Default mcp.toolSets = c.String(cliutils.McpToolsets) if mcp.toolSets == "" { mcp.toolSets = os.Getenv(mcpToolSetsEnvVar) } + if mcp.toolSets == "" { + mcp.toolSets = defaultToolsets + } + + // Resolve toolAccess: CLI flag -> Env var -> Default mcp.toolAccess = c.String(cliutils.McpToolAccess) if mcp.toolAccess == "" { mcp.toolAccess = os.Getenv(mcpToolAccessEnvVar) } + if mcp.toolAccess == "" { + mcp.toolAccess = defaultToolAccess + } + + // Resolve serverVersion: CLI flag -> Default mcp.serverVersion = c.String(cliutils.McpServerVersion) if mcp.serverVersion == "" { mcp.serverVersion = defaultServerVersion @@ -82,13 +95,10 @@ func (mcp *Command) Run() error { if err != nil { return err } - // Create command to execute the MCP server cmd := createMcpServerCommand(executablePath, mcp.toolSets, mcp.toolAccess) - // Log startup information - logStartupInfo(mcp.toolSets, mcp.toolAccess) - + log.Info(fmt.Sprintf("Starting MCP server | toolset: %s | tools access: %s", mcp.toolSets, mcp.toolAccess)) // Execute the command return cmd.Run() } @@ -97,8 +107,8 @@ func (mcp *Command) Run() error { func createMcpServerCommand(executablePath, toolSets, toolAccess string) *exec.Cmd { cmd := exec.Command( executablePath, - cliutils.McpToolsets+toolSets, - cliutils.McpToolAccess+toolAccess, + fmt.Sprintf("--%s=%s", cliutils.McpToolsets, toolSets), + fmt.Sprintf("--%s=%s", cliutils.McpToolAccess, toolAccess), ) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout @@ -107,20 +117,6 @@ func createMcpServerCommand(executablePath, toolSets, toolAccess string) *exec.C return cmd } -// logStartupInfo logs the MCP server startup parameters -func logStartupInfo(toolSets, toolAccess string) { - displayToolset := toolSets - if displayToolset == "" { - displayToolset = defaultToolsets - } - - displayToolsAccess := toolAccess - if displayToolsAccess == "" { - displayToolsAccess = defaultToolAccess - } - log.Info(fmt.Sprintf("Starting MCP server | toolset: %s | tools access: %s", displayToolset, displayToolsAccess)) -} - // Cmd handles the CLI command execution and argument parsing func Cmd(c *cli.Context) error { // Show help if needed @@ -153,7 +149,7 @@ func createAndConfigureCommand(c *cli.Context) *Command { cmd := NewMcpCommand() cmd.SetServerDetails(serverDetails) - cmd.getMCPServerArgs(c) + cmd.resolveMCPServerArgs(c) return cmd } diff --git a/general/mcp/cli_test.go b/general/mcp/cli_test.go index b6bd66e78..d59e42fb5 100644 --- a/general/mcp/cli_test.go +++ b/general/mcp/cli_test.go @@ -81,7 +81,7 @@ func TestGetMCPServerArgs(t *testing.T) { // Test getMCPServerArgs cmd := NewMcpCommand() - cmd.getMCPServerArgs(context) + cmd.resolveMCPServerArgs(context) // Assert results assert.Equal(t, test.expectedToolSets, cmd.toolSets) From 786e91496b1bacb07fb439f9437450bec782d5d9 Mon Sep 17 00:00:00 2001 From: delarea Date: Sun, 25 May 2025 17:48:54 +0300 Subject: [PATCH 21/21] fix --- general/mcp/cli.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/general/mcp/cli.go b/general/mcp/cli.go index e22c6e644..ba14c5a27 100644 --- a/general/mcp/cli.go +++ b/general/mcp/cli.go @@ -26,9 +26,10 @@ const ( mcpServerBinaryName = "cli-mcp-server" defaultServerVersion = "[RELEASE]" cliMcpDirName = "cli-mcp" - defaultToolsets = "read" - defaultToolAccess = "all-toolsets" - mcpDownloadBaseURL = "https://releases.jfrog.io/artifactory/cli-mcp-server/v0" + // Empty tool sets enabled all available tools + defaultToolsets = "" + defaultToolAccess = "read" + mcpDownloadBaseURL = "https://releases.jfrog.io/artifactory/cli-mcp-server/v0" ) type Command struct {