Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/general/ai/help.go → docs/general/how/help.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package ai
package how

var Usage = []string{"how"}

Expand Down
7 changes: 7 additions & 0 deletions docs/general/mcp/help.go
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."
}
2 changes: 1 addition & 1 deletion general/ai/cli.go → general/how/cli.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package ai
package how

import (
"bufio"
Expand Down
196 changes: 196 additions & 0 deletions general/mcp/cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package mcp

import (
"fmt"
"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"

"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"
// Empty tool sets enabled all available tools
defaultToolsets = ""
defaultToolAccess = "read"
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"
}

// 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
}
}

// 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.Info(fmt.Sprintf("Starting MCP server | toolset: %s | tools access: %s", 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,
fmt.Sprintf("--%s=%s", cliutils.McpToolsets, toolSets),
fmt.Sprintf("--%s=%s", cliutils.McpToolAccess, toolAccess),
)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = os.Environ()
return cmd
}

// 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.resolveMCPServerArgs(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
}
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
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
}

// 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
}
Loading
Loading