Skip to content

Commit 5964ffd

Browse files
authored
Support new command (source-mcp) (jfrog#448)
1 parent 5c96139 commit 5964ffd

File tree

4 files changed

+238
-9
lines changed

4 files changed

+238
-9
lines changed

cli/docs/mcp/help.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package source_mcp
2+
3+
import (
4+
"github.com/jfrog/jfrog-cli-core/v2/plugins/components"
5+
)
6+
7+
func GetDescription() string {
8+
return "Runs a local source code analysis as a local MCP server, allowing access to tools which reflect source code analysis"
9+
}
10+
11+
func GetArguments() []components.Argument {
12+
return []components.Argument{{Name: "Source path", Description: `Specifies the local file system path of source code to analyze.`}}
13+
}

cli/scancommands.go

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ package cli
33
import (
44
"errors"
55
"fmt"
6+
"os"
7+
"strings"
8+
69
buildInfoUtils "github.com/jfrog/build-info-go/utils"
710
"github.com/jfrog/gofrog/datastructures"
811
"github.com/jfrog/jfrog-cli-core/v2/common/cliutils"
@@ -14,26 +17,28 @@ import (
1417
"github.com/jfrog/jfrog-cli-core/v2/plugins/components"
1518
coreConfig "github.com/jfrog/jfrog-cli-core/v2/utils/config"
1619
"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
17-
enrichDocs "github.com/jfrog/jfrog-cli-security/cli/docs/enrich"
18-
"github.com/jfrog/jfrog-cli-security/commands/enrich"
19-
"github.com/jfrog/jfrog-cli-security/utils/xray"
20-
"github.com/jfrog/jfrog-client-go/utils/io/fileutils"
21-
"github.com/jfrog/jfrog-client-go/utils/log"
22-
"github.com/urfave/cli"
23-
"os"
24-
"strings"
25-
2620
flags "github.com/jfrog/jfrog-cli-security/cli/docs"
2721
auditSpecificDocs "github.com/jfrog/jfrog-cli-security/cli/docs/auditspecific"
22+
enrichDocs "github.com/jfrog/jfrog-cli-security/cli/docs/enrich"
23+
mcpDocs "github.com/jfrog/jfrog-cli-security/cli/docs/mcp"
2824
auditDocs "github.com/jfrog/jfrog-cli-security/cli/docs/scan/audit"
2925
buildScanDocs "github.com/jfrog/jfrog-cli-security/cli/docs/scan/buildscan"
3026
curationDocs "github.com/jfrog/jfrog-cli-security/cli/docs/scan/curation"
3127
dockerScanDocs "github.com/jfrog/jfrog-cli-security/cli/docs/scan/dockerscan"
3228
scanDocs "github.com/jfrog/jfrog-cli-security/cli/docs/scan/scan"
29+
"github.com/jfrog/jfrog-cli-security/commands/enrich"
30+
"github.com/jfrog/jfrog-cli-security/commands/source_mcp"
31+
"github.com/jfrog/jfrog-cli-security/jas"
32+
33+
"github.com/jfrog/jfrog-cli-security/utils/xray"
34+
"github.com/jfrog/jfrog-client-go/utils/io/fileutils"
35+
"github.com/jfrog/jfrog-client-go/utils/log"
36+
"github.com/urfave/cli"
3337

3438
"github.com/jfrog/jfrog-cli-security/commands/audit"
3539
"github.com/jfrog/jfrog-cli-security/commands/curation"
3640
"github.com/jfrog/jfrog-cli-security/commands/scan"
41+
3742
"github.com/jfrog/jfrog-cli-security/utils/severityutils"
3843
"github.com/jfrog/jfrog-cli-security/utils/techutils"
3944
"github.com/jfrog/jfrog-cli-security/utils/xsc"
@@ -101,6 +106,13 @@ func getAuditAndScansCommands() []components.Command {
101106
Action: CurationCmd,
102107
},
103108

109+
{
110+
Name: "source-mcp",
111+
Description: mcpDocs.GetDescription(),
112+
Action: SourceMcpCmd,
113+
Hidden: true,
114+
},
115+
104116
// TODO: Deprecated commands (remove at next CLI major version)
105117
{
106118
Name: "audit-mvn",
@@ -165,6 +177,28 @@ func getAuditAndScansCommands() []components.Command {
165177
}
166178
}
167179

180+
func SourceMcpCmd(c *components.Context) error {
181+
182+
serverDetails, err := createServerDetailsWithConfigOffer(c)
183+
if err != nil {
184+
return err
185+
}
186+
187+
am_env, err := jas.GetAnalyzerManagerEnvVariables(serverDetails)
188+
if err != nil {
189+
return err
190+
}
191+
192+
mcp_cmd := source_mcp.McpCommand{
193+
Env: am_env,
194+
Arguments: c.Arguments,
195+
InputPipe: os.Stdin,
196+
OutputPipe: os.Stdout,
197+
ErrorPipe: os.Stderr,
198+
}
199+
return mcp_cmd.Run()
200+
}
201+
168202
func EnrichCmd(c *components.Context) error {
169203
if len(c.Arguments) == 0 {
170204
return pluginsCommon.PrintHelpAndReturnError("providing a file path argument is mandatory", c)

commands/source_mcp/source_mcp.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package source_mcp
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"os/exec"
7+
"time"
8+
9+
"github.com/jfrog/jfrog-cli-security/jas"
10+
"github.com/jfrog/jfrog-cli-security/utils"
11+
"github.com/jfrog/jfrog-client-go/utils/log"
12+
)
13+
14+
type McpCommand struct {
15+
Env map[string]string
16+
Arguments []string
17+
InputPipe io.Reader
18+
OutputPipe io.Writer
19+
ErrorPipe io.Writer
20+
}
21+
22+
func establishPipeToFile(dst io.WriteCloser, src io.Reader) {
23+
defer dst.Close()
24+
_, err := io.Copy(dst, src)
25+
if err != nil {
26+
log.Error("Error establishing pipe")
27+
}
28+
}
29+
30+
func establishPipeFromFile(dst io.Writer, src io.ReadCloser) {
31+
defer src.Close()
32+
_, err := io.Copy(dst, src)
33+
if err != nil {
34+
log.Error("Error establishing pipe")
35+
}
36+
}
37+
38+
func RunAmMcpWithPipes(env map[string]string, cmd string, input_pipe io.Reader, output_pipe io.Writer, error_pipe io.Writer, timeout int, args ...string) error {
39+
am_path, err := jas.GetAnalyzerManagerExecutable()
40+
if err != nil {
41+
return err
42+
}
43+
44+
allArgs := append([]string{cmd}, args...)
45+
log.Info(fmt.Sprintf("Launching: %s; command %s; arguments %v", am_path, cmd, args))
46+
command := exec.Command(am_path, allArgs...)
47+
command.Env = utils.ToCommandEnvVars(env)
48+
49+
defer func() {
50+
if command != nil && !command.ProcessState.Exited() {
51+
if _error := command.Process.Kill(); _error != nil {
52+
log.Error(fmt.Sprintf("failed to kill process: %s", _error.Error()))
53+
}
54+
}
55+
}()
56+
57+
stdin, _error := command.StdinPipe()
58+
if _error != nil {
59+
log.Error(fmt.Sprintf("Error creating MCPService stdin pipe: %v", _error))
60+
return _error
61+
}
62+
defer stdin.Close()
63+
64+
stdout, _error := command.StdoutPipe()
65+
if _error != nil {
66+
log.Error(fmt.Sprintf("Error creating MCPService stdout pipe: %v", _error))
67+
return _error
68+
}
69+
defer stdout.Close()
70+
71+
stderr, _error := command.StderrPipe()
72+
if _error != nil {
73+
log.Error(fmt.Sprintf("Error creating MCPService stderr pipe: %v", _error))
74+
return _error
75+
}
76+
defer stderr.Close()
77+
78+
go establishPipeToFile(stdin, input_pipe)
79+
go establishPipeFromFile(error_pipe, stderr)
80+
go establishPipeFromFile(output_pipe, stdout)
81+
82+
if _error := command.Start(); _error != nil {
83+
log.Error(fmt.Sprintf("Error starting MCPService subprocess: %v", _error))
84+
return _error
85+
}
86+
87+
if timeout > 0 {
88+
go func() {
89+
time.Sleep(time.Duration(timeout) * time.Second)
90+
// closing the pipe required prior to killing the process
91+
// according to MCP documentation
92+
// https://modelcontextprotocol.io/specification/2025-03-26/basic/lifecycle
93+
err := stdin.Close()
94+
if err != nil {
95+
log.Error(fmt.Sprintf("Error closing MCPService stdin pipe: %v", err))
96+
}
97+
98+
err = command.Process.Kill()
99+
if err != nil {
100+
log.Error(fmt.Sprintf("Error killing MCPService subprocess: %v", err))
101+
}
102+
}()
103+
}
104+
105+
if _error := command.Wait(); _error != nil {
106+
log.Error(fmt.Sprintf("Error waiting for MCPService subprocess: %v", _error))
107+
return _error
108+
}
109+
return nil
110+
}
111+
112+
func (mcpCmd *McpCommand) runWithTimeout(timeout int, cmd string) (err error) {
113+
err_ := jas.DownloadAnalyzerManagerIfNeeded(0)
114+
if err_ != nil {
115+
log.Error(fmt.Sprintf("Failed to download Analyzer Manager: %v", err))
116+
}
117+
118+
return RunAmMcpWithPipes(mcpCmd.Env, cmd, mcpCmd.InputPipe, mcpCmd.OutputPipe, mcpCmd.ErrorPipe, timeout, mcpCmd.Arguments...)
119+
}
120+
121+
func (mcpCmd *McpCommand) Run() (err error) {
122+
return mcpCmd.runWithTimeout(0, "mcp-sast")
123+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package source_mcp
2+
3+
import (
4+
"bytes"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/jfrog/jfrog-cli-security/jas"
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestRunSourceMcpHappyFlow(t *testing.T) {
13+
scanner, cleanUp := jas.InitJasTest(t)
14+
defer cleanUp()
15+
scanned_path := filepath.Join("..", "..", "tests", "testdata", "projects", "jas", "jas")
16+
query := "{\"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"initialize\", \"params\": {\"protocolVersion\": \"2024-11-05\", \"capabilities\": {}, \"clientInfo\": {\"name\": \"ExampleClient\", \"version\": \"1.0.0\" }}}"
17+
input_buffer := *bytes.NewBufferString(query)
18+
output_buffer := *bytes.NewBuffer(make([]byte, 0, 500))
19+
error_buffer := *bytes.NewBuffer(make([]byte, 0, 500))
20+
am_env, _ := jas.GetAnalyzerManagerEnvVariables(scanner.ServerDetails)
21+
22+
mcp_cmd := McpCommand{
23+
Env: am_env,
24+
Arguments: []string{scanned_path},
25+
InputPipe: &input_buffer,
26+
OutputPipe: &output_buffer,
27+
ErrorPipe: &error_buffer,
28+
}
29+
30+
err := mcp_cmd.runWithTimeout(5, "mcp-sast")
31+
assert.Error(t, err) // returns error because it was terminated upon timeout
32+
if !assert.Contains(t, error_buffer.String(), "Generated IR") {
33+
t.Error(error_buffer.String())
34+
}
35+
36+
if !assert.Contains(t, output_buffer.String(), "\"serverInfo\":{\"name\":\"jfrog_sast\"") {
37+
t.Error(output_buffer.String())
38+
}
39+
}
40+
41+
func TestRunSourceMcpScannerError(t *testing.T) {
42+
scanner, cleanUp := jas.InitJasTest(t)
43+
defer cleanUp()
44+
// no such path
45+
scanned_path := ""
46+
input_buffer := *bytes.NewBufferString("")
47+
output_buffer := *bytes.NewBuffer(make([]byte, 0, 500))
48+
error_buffer := *bytes.NewBuffer(make([]byte, 0, 500))
49+
am_env, _ := jas.GetAnalyzerManagerEnvVariables(scanner.ServerDetails)
50+
mcp_cmd := McpCommand{
51+
Env: am_env,
52+
Arguments: []string{scanned_path},
53+
InputPipe: &input_buffer,
54+
OutputPipe: &output_buffer,
55+
ErrorPipe: &error_buffer,
56+
}
57+
err := mcp_cmd.runWithTimeout(0, "mcp-sast1") // no such command
58+
assert.ErrorContains(t, err, "exit status 99")
59+
}

0 commit comments

Comments
 (0)