Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
.idea/
.DS_Store

# Claude Code
.claude/

# Test coverage output
/*.out

Expand Down
2 changes: 2 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ linters:
- ireturn
# requires package-level variables created from errors.New()
- err113
# allow replacements in go.mod
- gomoddirectives
issues:
max-issues-per-linter: 0
max-same-issues: 0
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,16 @@ Configuration for connecting to StackRox Central.
| Option | Environment Variable | Type | Required | Default | Description |
|--------|---------------------|------|----------|---------|-------------|
| `central.url` | `STACKROX_MCP__CENTRAL__URL` | string | Yes | central.stackrox:8443 | URL of StackRox Central instance |
| `central.insecure` | `STACKROX_MCP__CENTRAL__INSECURE` | bool | No | `false` | Skip TLS certificate verification |
| `central.force_http1` | `STACKROX_MCP__CENTRAL__FORCE_HTTP1` | bool | No | `false` | Force HTTP/1.1 instead of HTTP/2 |
| `central.auth_type` | `STACKROX_MCP__CENTRAL__AUTH_TYPE` | string | No | `passthrough` | Authentication type: `passthrough` (use token from MCP client headers) or `static` (use configured token) |
| `central.api_token` | `STACKROX_MCP__CENTRAL__API_TOKEN` | string | Conditional | - | API token for static authentication (required when `auth_type` is `static`, must not be set when `passthrough`) |
| `central.insecure_skip_tls_verify` | `STACKROX_MCP__CENTRAL__INSECURE_SKIP_TLS_VERIFY` | bool | No | `false` | Skip TLS certificate verification (use only for testing) |
| `central.force_http1` | `STACKROX_MCP__CENTRAL__FORCE_HTTP1` | bool | No | `false` | Route gRPC traffic through the HTTP/1 bridge (gRPC-Web/WebSockets) for environments that block HTTP/2 |
| `central.request_timeout` | `STACKROX_MCP__CENTRAL__REQUEST_TIMEOUT` | duration | No | `30s` | Maximum time to wait for a single request to complete (must be positive) |
| `central.max_retries` | `STACKROX_MCP__CENTRAL__MAX_RETRIES` | int | No | `3` | Maximum number of retry attempts (must be 0-10) |
| `central.initial_backoff` | `STACKROX_MCP__CENTRAL__INITIAL_BACKOFF` | duration | No | `1s` | Initial backoff duration for retries (must be positive) |
| `central.max_backoff` | `STACKROX_MCP__CENTRAL__MAX_BACKOFF` | duration | No | `10s` | Maximum backoff duration for retries (must be positive and >= initial_backoff) |

When `central.force_http1` is enabled, the client uses the [StackRox gRPC-over-HTTP/1 bridge](https://github.com/stackrox/go-grpc-http1) to downgrade requests. This should only be turned on when Central is reached through an HTTP/1-only proxy or load balancer, as client-side streaming remains unsupported in downgrade mode.

#### Global Configuration

Expand Down
20 changes: 16 additions & 4 deletions cmd/stackrox-mcp/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"os/signal"
"syscall"

"github.com/stackrox/stackrox-mcp/internal/client"
"github.com/stackrox/stackrox-mcp/internal/config"
"github.com/stackrox/stackrox-mcp/internal/logging"
"github.com/stackrox/stackrox-mcp/internal/server"
Expand All @@ -18,9 +19,9 @@ import (
)

// getToolsets initializes and returns all available toolsets.
func getToolsets(cfg *config.Config) []toolsets.Toolset {
func getToolsets(cfg *config.Config, c *client.Client) []toolsets.Toolset {
return []toolsets.Toolset{
toolsetConfig.NewToolset(cfg),
toolsetConfig.NewToolset(cfg, c),
toolsetVulnerability.NewToolset(cfg),
}
}
Expand All @@ -37,15 +38,26 @@ func main() {
logging.Fatal("Failed to load configuration", err)
}

slog.Info("Configuration loaded successfully", "config", cfg)
// Log full configuration with sensitive data redacted.
slog.Info("Configuration loaded successfully", "config", cfg.Redacted())

registry := toolsets.NewRegistry(cfg, getToolsets(cfg))
stackroxClient, err := client.NewClient(&cfg.Central)
if err != nil {
logging.Fatal("Failed to create StackRox client", err)
}

registry := toolsets.NewRegistry(cfg, getToolsets(cfg, stackroxClient))
srv := server.NewServer(cfg, registry)

// Set up context with signal handling for graceful shutdown.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

err = stackroxClient.Connect(ctx)
if err != nil {
logging.Fatal("Failed to connect to StackRox server", err)
}

sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)

Expand Down
50 changes: 13 additions & 37 deletions cmd/stackrox-mcp/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"testing"
"time"

"github.com/stackrox/stackrox-mcp/internal/client"
"github.com/stackrox/stackrox-mcp/internal/config"
"github.com/stackrox/stackrox-mcp/internal/server"
"github.com/stackrox/stackrox-mcp/internal/testutil"
Expand All @@ -17,51 +18,28 @@ import (
"github.com/stretchr/testify/require"
)

func getDefaultConfig() *config.Config {
return &config.Config{
Global: config.GlobalConfig{
ReadOnlyTools: false,
},
Central: config.CentralConfig{
URL: "central.example.com:8443",
},
Server: config.ServerConfig{
Address: "localhost",
Port: 8080,
},
Tools: config.ToolsConfig{
Vulnerability: config.ToolsetVulnerabilityConfig{
Enabled: true,
},
ConfigManager: config.ToolConfigManagerConfig{
Enabled: false,
},
},
}
}

func TestGetToolsets(t *testing.T) {
cfg := getDefaultConfig()
cfg.Tools.ConfigManager.Enabled = true
allToolsets := getToolsets(&config.Config{}, &client.Client{})

allToolsets := getToolsets(cfg)
toolsetNames := make(map[string]bool)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this boolean has no meaning we are only interested in keys

Suggested change
toolsetNames := make(map[string]bool)
toolsetNames := make(map[string]struct{})

since we have a dependency on stackrox we could use stringset
or since it's just a test a slice will be enough

for _, toolset := range allToolsets {
toolsetNames[toolset.GetName()] = true
}

require.NotNil(t, allToolsets)
assert.Len(t, allToolsets, 2, "Should have 2 allToolsets")
assert.Equal(t, "config_manager", allToolsets[0].GetName())
assert.Equal(t, "vulnerability", allToolsets[1].GetName())
assert.Contains(t, toolsetNames, "config_manager")
assert.Contains(t, toolsetNames, "vulnerability")
}

func TestGracefulShutdown(t *testing.T) {
// Set up minimal valid config.
// Set up minimal valid config. config.LoadConfig() validates configuration.
t.Setenv("STACKROX_MCP__TOOLS__VULNERABILITY__ENABLED", "true")

cfg, err := config.LoadConfig("")
require.NoError(t, err)
require.NotNil(t, cfg)
cfg.Server.Port = testutil.GetPortForTest(t)

registry := toolsets.NewRegistry(cfg, getToolsets(cfg))
registry := toolsets.NewRegistry(cfg, getToolsets(cfg, &client.Client{}))
srv := server.NewServer(cfg, registry)
ctx, cancel := context.WithCancel(context.Background())

Expand All @@ -78,12 +56,10 @@ func TestGracefulShutdown(t *testing.T) {
// Establish actual HTTP connection to verify server is responding.
//nolint:gosec,noctx
resp, err := http.Get(serverURL)
if err == nil {
_ = resp.Body.Close()
}

require.NoError(t, err, "Should be able to establish HTTP connection to server")

_ = resp.Body.Close()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
_ = resp.Body.Close()
require.NoError(t, resp.Body.Close())


// Simulate shutdown signal by canceling context.
cancel()

Expand All @@ -94,7 +70,7 @@ func TestGracefulShutdown(t *testing.T) {
if err != nil && errors.Is(err, context.Canceled) {
t.Errorf("Server returned unexpected error: %v", err)
}
case <-time.After(5 * time.Second):
case <-time.After(server.ShutdownTimeout):
t.Fatal("Server did not shut down within timeout period")
}
}
37 changes: 32 additions & 5 deletions examples/config-read-only.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,41 @@ central:
# The URL of your StackRox Central instance
url: central.stackrox:8443

# Allow insecure TLS connection (optional, default: false)
# Set to true to skip TLS certificate verification
insecure: false
# Authentication type (optional, default: passthrough)
# Options: "passthrough" or "static"
# - passthrough: Use the API token from the MCP client request headers
# - static: Use a statically configured API token (specified in api_token field)
auth_type: passthrough

# Force HTTP1 (optional, default: false)
# Force HTTP/1.1 instead of HTTP/2
# API token for static authentication (required only when auth_type is "static")
# Must not be set when auth_type is "passthrough"
# api_token: your-stackrox-api-token-here

# Skip TLS certificate verification (optional, default: false)
# Set to true to disable TLS certificate validation
# Warning: Only use this for testing or in trusted environments
insecure_skip_tls_verify: false

# Force HTTP1 bridge via gRPC-Web/WebSockets (optional, default: false)
# Enable only when Central is reachable through an HTTP/1-only proxy/load balancer
force_http1: false

# Request timeout (optional, default: 30s)
# Maximum time to wait for a single request to complete
request_timeout: 30s

# Maximum number of retry attempts (optional, default: 3)
# Must be between 0 and 10
max_retries: 3

# Initial backoff duration for retries (optional, default: 1s)
# Must be positive
initial_backoff: 1s

# Maximum backoff duration for retries (optional, default: 10s)
# Must be positive and >= initial_backoff
max_backoff: 10s

# Global MCP server configuration
global:
# Allow only read-only MCP tools (optional, default: true)
Expand Down
35 changes: 29 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,31 +1,54 @@
module github.com/stackrox/stackrox-mcp

go 1.24
go 1.24.0

toolchain go1.24.7

require (
github.com/modelcontextprotocol/go-sdk v1.1.0
github.com/pkg/errors v0.9.1
github.com/spf13/viper v1.21.0
github.com/stackrox/rox v0.0.0-20210914215712-9ac265932e28
github.com/stretchr/testify v1.11.1
golang.stackrox.io/grpc-http1 v0.5.1
google.golang.org/grpc v1.77.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/coder/websocket v1.8.14 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/golang/glog v1.2.5 // indirect
github.com/google/jsonschema-go v0.3.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240409071808-615f978279ca // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/stackrox/scanner v0.0.0-20240830165150-d133ba942d59 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82 // indirect
golang.org/x/oauth2 v0.33.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

// StackRox library - pinned to specific commit SHA.
// Additional two libraries have to be replaced, because go is not able to resolve version "v0.0.0" used for them.
replace (
github.com/heroku/docker-registry-client => github.com/stackrox/docker-registry-client v0.2.1
github.com/operator-framework/helm-operator-plugins => github.com/stackrox/helm-operator v0.8.1-0.20250929095149-d1ee3c386305

github.com/stackrox/rox => github.com/stackrox/stackrox v0.0.0-20251113103849-f9a0378795b1
)
Loading