Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
ad082a6
fix(upload): stop retrying 4xx errors and add tests for 6 packages
appleboy Mar 24, 2026
4bc0155
feat: add MCP server mode, command fallback, traffic capture, and E2E…
appleboy Mar 24, 2026
65d67a7
fix: address Copilot review comments for PR #6
appleboy Mar 25, 2026
b5a43f9
fix: address Copilot review round 2 for PR #6
appleboy Mar 25, 2026
338c048
fix: address Copilot review round 3 for PR #6
appleboy Mar 25, 2026
66a5229
test(mcpserver): add concurrent Set/Get to TestScanState_Concurrency
appleboy Mar 25, 2026
e76c923
refactor(mcpserver): accept parent context in RunServer for graceful …
appleboy Mar 25, 2026
5eb097e
fix: skip E2E tests in short mode and handle empty config files
appleboy Mar 25, 2026
4b64650
fix(capture): use sync.Once to prevent multiple Receive() goroutine l…
appleboy Mar 25, 2026
0e8ad76
fix(e2e): use SKIP_E2E env var instead of testing.Short in TestMain
appleboy Mar 25, 2026
f2a76bd
fix: JSONC config support, duplicate error codes, and atomic test flag
appleboy Mar 25, 2026
a15ef1f
build(e2e): gate E2E tests behind //go:build e2e tag
appleboy Mar 25, 2026
1d02129
merge: resolve conflicts with main branch
appleboy Mar 26, 2026
511cf73
Merge remote-tracking branch 'origin/main' into phase3-4
appleboy Mar 28, 2026
1c99c5b
fix(mcpserver): redact sensitive data and validate scan interval
appleboy Mar 28, 2026
dab46cc
refactor: deduplicate test server loop and add direction constants
appleboy Mar 28, 2026
4dc06d5
docs(readme): add MCP server mode and install command usage
appleboy Mar 28, 2026
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
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Inspired by [snyk/agent-scan](https://github.com/snyk/agent-scan), reimplemented
- **13 security rules** detecting prompt injections, tool shadowing, hardcoded secrets, malicious code, toxic flows, and more
- **Skill scanning** for agent skill directories containing `SKILL.md`
- **Direct scanning** from package managers (`npm:`, `pypi:`, `oci://`) and URLs (`sse://`, `streamable-http://`)
- **MCP server mode** — run agent-scanner itself as an MCP server with background periodic scanning
- **Cross-platform** support (macOS, Linux, Windows)
- **Single binary** with zero runtime dependencies

Expand Down Expand Up @@ -94,6 +95,33 @@ List tools, prompts, and resources without security analysis:
agent-scanner inspect
```

### MCP Server Mode

Run agent-scanner as an MCP server, exposing `scan` and `get_scan_results` tools:

```bash
agent-scanner mcp-server
```

Run in tool-only mode (no background scanning):

```bash
agent-scanner mcp-server --tool
```

Customize the background scan interval:

```bash
agent-scanner mcp-server --scan-interval 60
```

Install agent-scanner into Claude Desktop configuration:

```bash
agent-scanner install-mcp-server
agent-scanner install-mcp-server ~/.config/claude/claude_desktop_config.json
```

### Options

```text
Expand Down
7 changes: 7 additions & 0 deletions cmd/testserver-math/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package main

import "github.com/go-authgate/agent-scanner/internal/testserver"

func main() {
testserver.RunMathServer()
}
7 changes: 7 additions & 0 deletions cmd/testserver-weather/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package main

import "github.com/go-authgate/agent-scanner/internal/testserver"

func main() {
testserver.RunWeatherServer()
}
7 changes: 7 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,19 @@ module github.com/go-authgate/agent-scanner
go 1.25.8

require (
github.com/modelcontextprotocol/go-sdk v1.4.1
github.com/spf13/cobra v1.10.2
github.com/tidwall/jsonc v0.3.3
golang.org/x/sync v0.20.0
)

require (
github.com/google/jsonschema-go v0.4.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/segmentio/asm v1.1.3 // indirect
github.com/segmentio/encoding v0.5.4 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sys v0.40.0 // indirect
)
20 changes: 20 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,14 +1,34 @@
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/modelcontextprotocol/go-sdk v1.4.1 h1:M4x9GyIPj+HoIlHNGpK2hq5o3BFhC+78PkEaldQRphc=
github.com/modelcontextprotocol/go-sdk v1.4.1/go.mod h1:Bo/mS87hPQqHSRkMv4dQq1XCu6zv4INdXnFZabkNU6s=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0=
github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/tidwall/jsonc v0.3.3 h1:RVQqL3xFfDkKKXIDsrBiVQiEpBtxoKbmMXONb2H/y2w=
github.com/tidwall/jsonc v0.3.3/go.mod h1:dw+3CIxqHi+t8eFSpzzMlcVYxKp08UP5CD8/uSFCyJE=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
1 change: 0 additions & 1 deletion internal/cli/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ type MCPServerFlags struct {
Tool bool
Background bool
ScanInterval int
ClientName string
}

var (
Expand Down
26 changes: 23 additions & 3 deletions internal/cli/install.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package cli

import (
"fmt"

"github.com/go-authgate/agent-scanner/internal/mcpserver"
"github.com/spf13/cobra"
)

Expand All @@ -15,8 +18,25 @@ func newInstallCmd() *cobra.Command {
return cmd
}

func runInstall(cmd *cobra.Command, _ []string) error {
// TODO: Implement MCP server installation in Phase 8
cmd.Println("MCP server installation not yet implemented")
func runInstall(cmd *cobra.Command, args []string) error {
var configPath string
if len(args) > 0 {
configPath = args[0]
}

if configPath == "" {
defaultPath, err := mcpserver.DefaultConfigPath()
if err != nil {
return err
}
configPath = defaultPath
cmd.Printf("No config file specified, using default: %s\n", configPath)
}

if err := mcpserver.InstallServer(configPath); err != nil {
return fmt.Errorf("installation failed: %w", err)
}

cmd.Printf("Successfully installed agent-scanner as MCP server in %s\n", configPath)
return nil
}
47 changes: 42 additions & 5 deletions internal/cli/mcpserver.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
package cli

import (
"context"
"time"

"github.com/go-authgate/agent-scanner/internal/analysis"
"github.com/go-authgate/agent-scanner/internal/discovery"
"github.com/go-authgate/agent-scanner/internal/inspect"
"github.com/go-authgate/agent-scanner/internal/mcpclient"
"github.com/go-authgate/agent-scanner/internal/mcpserver"
"github.com/go-authgate/agent-scanner/internal/models"
"github.com/go-authgate/agent-scanner/internal/pipeline"
"github.com/go-authgate/agent-scanner/internal/rules"
"github.com/spf13/cobra"
)

Expand All @@ -18,13 +29,39 @@ func newMCPServerCmd() *cobra.Command {
BoolVar(&mcpServerFlags.Background, "background", true, "Enable background periodic scanning")
cmd.Flags().
IntVar(&mcpServerFlags.ScanInterval, "scan-interval", 30, "Background scan interval in minutes")
cmd.Flags().
StringVar(&mcpServerFlags.ClientName, "client-name", "", "Client name for identification")
return cmd
}

func runMCPServer(cmd *cobra.Command, _ []string) error {
// TODO: Implement MCP server mode in Phase 8
cmd.Println("MCP server mode not yet implemented")
return nil
setupLogging()

// Build pipeline components
discoverer := discovery.NewDiscoverer()
client := mcpclient.NewClient(commonFlags.SkipSSLVerify)
inspector := inspect.NewInspector(client, commonFlags.ServerTimeout)
ruleEngine := rules.NewDefaultEngine()
analyzer := analysis.NewAnalyzer(commonFlags.AnalysisURL, commonFlags.SkipSSLVerify)

// Create the scan function closure
scanFn := func(ctx context.Context, paths []string, skills bool) ([]models.ScanPathResult, error) {
p := pipeline.New(pipeline.Config{
Discoverer: discoverer,
Inspector: inspector,
RuleEngine: ruleEngine,
Analyzer: analyzer,
Paths: paths,
ScanSkills: skills,
ScanAllUsers: commonFlags.ScanAllUsers,
Verbose: commonFlags.Verbose,
})
return p.Run(ctx)
}

background := mcpServerFlags.Background && !mcpServerFlags.Tool

return mcpserver.RunServer(cmd.Context(), mcpserver.ServerConfig{
ScanFn: scanFn,
Background: background,
ScanInterval: time.Duration(mcpServerFlags.ScanInterval) * time.Minute,
})
}
Loading
Loading