-
Notifications
You must be signed in to change notification settings - Fork 296
Add jf mcp server commands
#2980
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
EyalDelarea
wants to merge
22
commits into
jfrog:dev-deprecated-please-target-master-directly
from
EyalDelarea:add_mcp_server
Closed
Changes from 18 commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
ff8da1c
stash
EyalDelarea d7e0978
Merge branch 'dev' of https://github.com/jfrog/jfrog-cli into add_mcp…
EyalDelarea a7815f5
refactor
EyalDelarea 14c282d
refactor
EyalDelarea acdebb5
export env vars
EyalDelarea 20efe8e
Move from script to go
EyalDelarea 4a5a844
Add download from releases
EyalDelarea 7746cd0
Add download from releases
EyalDelarea b08c50b
Add download from releases
EyalDelarea 3ffa440
refactor
EyalDelarea 0b54dcd
fix static check
EyalDelarea e0aca79
add tests
EyalDelarea 0d5a1f0
fix static check
EyalDelarea 59b2135
fix windows test
EyalDelarea 9aad0f0
fix
EyalDelarea 6a0659f
remove update
EyalDelarea 9b60dbc
remove update cmd
EyalDelarea 9dacc86
always download binary
EyalDelarea 7fd8416
use fileutil to download
EyalDelarea 0543115
fix static check
EyalDelarea 0494c99
fix
EyalDelarea 786e914
fix
EyalDelarea File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| package ai | ||
| package how | ||
|
|
||
| var Usage = []string{"how"} | ||
|
|
||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package mcp | ||
|
|
||
| var Usage = []string{"mcp start"} | ||
|
|
||
| func GetDescription() string { | ||
| return "Start the JFrog MCP server and begin using it with your MCP client of your choice." | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| package ai | ||
| package how | ||
|
|
||
| import ( | ||
| "bufio" | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,247 @@ | ||
| package mcp | ||
|
|
||
| import ( | ||
| "errors" | ||
| "fmt" | ||
| "io" | ||
| "net/http" | ||
| "net/url" | ||
| "os" | ||
| "os/exec" | ||
| "path" | ||
| "runtime" | ||
| "strings" | ||
|
|
||
| "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" | ||
| "github.com/jfrog/jfrog-cli/utils/cliutils" | ||
| "github.com/jfrog/jfrog-client-go/utils/log" | ||
| "github.com/urfave/cli" | ||
| ) | ||
|
|
||
| const ( | ||
| 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" | ||
| ) | ||
|
|
||
| type Command struct { | ||
| serverDetails *config.ServerDetails | ||
| toolSets string | ||
| toolAccess string | ||
| serverVersion string | ||
| } | ||
|
|
||
| // 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" | ||
| } | ||
|
|
||
| // 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) { | ||
| mcp.toolSets = c.String(cliutils.McpToolsets) | ||
| if mcp.toolSets == "" { | ||
| mcp.toolSets = os.Getenv(mcpToolSetsEnvVar) | ||
| } | ||
| mcp.toolAccess = c.String(cliutils.McpToolAccess) | ||
| if mcp.toolAccess == "" { | ||
| mcp.toolAccess = os.Getenv(mcpToolAccessEnvVar) | ||
| } | ||
| mcp.serverVersion = c.String(cliutils.McpServerVersion) | ||
| if mcp.serverVersion == "" { | ||
| mcp.serverVersion = defaultServerVersion | ||
| } | ||
| } | ||
|
|
||
| // 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 | ||
| } | ||
|
|
||
| // 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() | ||
| return cmd | ||
| } | ||
|
|
||
| // logStartupInfo logs the MCP server startup parameters | ||
| func logStartupInfo(toolSets, toolAccess string) { | ||
| displayToolset := toolSets | ||
| if displayToolset == "" { | ||
| displayToolset = defaultToolsets | ||
|
EyalDelarea marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| 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 | ||
| if show, err := cliutils.ShowCmdHelpIfNeeded(c, c.Args()); show || err != nil { | ||
| return err | ||
| } | ||
| cmd := createAndConfigureCommand(c) | ||
| return commands.Exec(cmd) | ||
|
|
||
| } | ||
|
|
||
| // 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) | ||
| 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 := getOsArchBinaryInfo() | ||
| targetPath, err := getLocalBinaryPath(binaryName) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| return downloadBinary(targetPath, version, osName, arch) | ||
| } | ||
|
|
||
| // getLocalBinaryPath determines the path to the binary and checks if it exists | ||
| func getLocalBinaryPath(binaryName string) (fullPath string, err error) { | ||
| jfrogHomeDir, err := coreutils.GetJfrogHomeDir() | ||
| if err != nil { | ||
| 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 "", fmt.Errorf("failed to create directory '%s': %w", targetDir, err) | ||
| } | ||
|
|
||
| fullPath = path.Join(targetDir, binaryName) | ||
| 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()) | ||
|
EyalDelarea marked this conversation as resolved.
Outdated
|
||
| 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) | ||
| 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 | ||
| arch = runtime.GOARCH | ||
| binaryName = mcpServerBinaryName | ||
| if osName == "windows" { | ||
| binaryName += ".exe" | ||
| } | ||
| return | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.