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 .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ linters:
- paralleltest
- testpackage
- noinlineerr
- ireturn
# requires package-level variables created from errors.New()
- err113
issues:
max-issues-per-linter: 0
max-same-issues: 0
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@
# Binary name
BINARY_NAME=stackrox-mcp

# Version (can be overridden with VERSION=x.y.z make build)
VERSION?=0.1.0
Copy link

Choose a reason for hiding this comment

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

maybe when not provided we should use git describe --tags --abbrev=10 --dirty --long instead? So development builds will have commit information

Copy link
Collaborator Author

@mtodor mtodor Nov 21, 2025

Choose a reason for hiding this comment

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

@janisz This does not work for me:

❯ git describe --tags --abbrev=10 --dirty --long
fatal: No names found, cannot describe anything

For local dev, we could use: git rev-parse --short HEAD - but on tag builds, we could get tag info from CI environment. Any better ideas?


# Go parameters
GOCMD=go
GOBUILD=$(GOCMD) build
GOTEST=$(GOCMD) test
GOFMT=$(GOCMD) fmt
GOCLEAN=$(GOCMD) clean

# Build flags
LDFLAGS=-ldflags "-X github.com/stackrox/stackrox-mcp/internal/server.version=$(VERSION)"

# Coverage files
COVERAGE_OUT=coverage.out

Expand All @@ -24,7 +30,7 @@ help: ## Display this help message

.PHONY: build
build: ## Build the binary
$(GOBUILD) -o $(BINARY_NAME) ./cmd/stackrox-mcp
$(GOBUILD) $(LDFLAGS) -o $(BINARY_NAME) ./cmd/stackrox-mcp

.PHONY: test
test: ## Run unit tests with coverage
Expand Down
68 changes: 68 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export STACKROX_MCP__TOOLS__VULNERABILITY__ENABLED=true
./stackrox-mcp
```

The server will start on `http://localhost:8080` by default. See the [Testing the MCP Server](#testing-the-mcp-server) section for instructions on connecting with Claude Code.

## Configuration

The StackRox MCP server supports configuration through both YAML files and environment variables. Environment variables take precedence over YAML configuration.
Expand Down Expand Up @@ -80,6 +82,15 @@ Global MCP server settings.
|--------|---------------------|------|----------|---------|-------------|
| `global.read_only_tools` | `STACKROX_MCP__GLOBAL__READ_ONLY_TOOLS` | bool | No | `true` | Only allow read-only tools |

#### Server Configuration

HTTP server settings for the MCP server.

| Option | Environment Variable | Type | Required | Default | Description |
|--------|---------------------|------|----------|---------|-------------|
| `server.address` | `STACKROX_MCP__SERVER__ADDRESS` | string | No | `localhost` | HTTP server listen address |
| `server.port` | `STACKROX_MCP__SERVER__PORT` | int | No | `8080` | HTTP server listen port (must be 1-65535) |

#### Tools Configuration

Enable or disable individual MCP tools. At least one tool has to be enabled.
Expand All @@ -97,6 +108,63 @@ Configuration values are loaded in the following order (later sources override e
2. YAML configuration file (if provided via `--config`)
3. Environment variables (highest precedence)

## Testing the MCP Server

### Starting the Server

Start the server with a configuration file:

```bash
./stackrox-mcp --config examples/config-read-only.yaml
```

Or using environment variables:

```bash
export STACKROX_MCP__CENTRAL__URL="central.example.com:8443"
export STACKROX_MCP__TOOLS__VULNERABILITY__ENABLED="true"
./stackrox-mcp
```

The server will start on `http://localhost:8080` by default (configurable via `server.address` and `server.port`).

### Connecting with Claude Code CLI

Add the MCP server to Claude Code using command-line options:

```bash
claude mcp add stackrox \
--name "StackRox MCP Server" \
--transport http \
--url http://localhost:8080
```

### Verifying Connection

List configured MCP servers:

```bash
claude mcp list
```

Get details for a specific server:

```bash
claude mcp get stackrox
```

Within a Claude Code session, use the `/mcp` command to view available tools from connected servers.

### Example Usage

Once connected, interact with the tools using natural language:

**List all clusters:**
```
You: "Can you list all the clusters from StackRox?"
Claude: [Uses list_clusters tool to retrieve cluster information]
```

## Development

For detailed development guidelines, testing standards, and contribution workflows, see [CONTRIBUTING.md](.github/CONTRIBUTING.md).
Expand Down
38 changes: 36 additions & 2 deletions cmd/stackrox-mcp/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,29 @@
package main

import (
"context"
"flag"
"log/slog"
"os"
"os/signal"
"syscall"

"github.com/stackrox/stackrox-mcp/internal/config"
"github.com/stackrox/stackrox-mcp/internal/logging"
"github.com/stackrox/stackrox-mcp/internal/server"
"github.com/stackrox/stackrox-mcp/internal/toolsets"
toolsetConfig "github.com/stackrox/stackrox-mcp/internal/toolsets/config"
toolsetVulnerability "github.com/stackrox/stackrox-mcp/internal/toolsets/vulnerability"
)

// getToolsets initializes and returns all available toolsets.
func getToolsets(cfg *config.Config) []toolsets.Toolset {
return []toolsets.Toolset{
toolsetConfig.NewToolset(cfg),
toolsetVulnerability.NewToolset(cfg),
}
}

func main() {
logging.SetupLogging()

Expand All @@ -19,11 +34,30 @@ func main() {

cfg, err := config.LoadConfig(*configPath)
if err != nil {
slog.Error("Failed to load configuration", "error", err)
os.Exit(1)
logging.Fatal("Failed to load configuration", err)
}

slog.Info("Configuration loaded successfully", "config", cfg)

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

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

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

go func() {
<-sigChan
slog.Info("Received shutdown signal")
cancel()
}()

slog.Info("Starting Stackrox MCP server")

if err := srv.Start(ctx); err != nil {
logging.Fatal("Server error", err)
}
}
100 changes: 100 additions & 0 deletions cmd/stackrox-mcp/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package main

import (
"context"
"errors"
"net"
"net/http"
"strconv"
"testing"
"time"

"github.com/stackrox/stackrox-mcp/internal/config"
"github.com/stackrox/stackrox-mcp/internal/server"
"github.com/stackrox/stackrox-mcp/internal/testutil"
"github.com/stackrox/stackrox-mcp/internal/toolsets"
"github.com/stretchr/testify/assert"
"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(cfg)

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())
}

func TestGracefulShutdown(t *testing.T) {
// Set up minimal valid config.
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))
srv := server.NewServer(cfg, registry)
ctx, cancel := context.WithCancel(context.Background())

errChan := make(chan error, 1)

go func() {
errChan <- srv.Start(ctx)
}()

serverURL := "http://" + net.JoinHostPort(cfg.Server.Address, strconv.Itoa(cfg.Server.Port))
err = testutil.WaitForServerReady(serverURL, 3*time.Second)
require.NoError(t, err, "Server should start within timeout")

// 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")

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

// Wait for server to shut down.
select {
case err := <-errChan:
// Server should shut down cleanly (either nil or context.Canceled).
if err != nil && errors.Is(err, context.Canceled) {
t.Errorf("Server returned unexpected error: %v", err)
}
case <-time.After(5 * time.Second):
t.Fatal("Server did not shut down within timeout period")
}
}
11 changes: 11 additions & 0 deletions examples/config-read-only.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ global:
# When false, both read and write tools may be available (if implemented)
read_only_tools: true

# HTTP server configuration
server:
# Server listen address (optional, default: localhost)
# The address on which the MCP HTTP server will listen
address: localhost

# Server listen port (optional, default: 8080)
# The port on which the MCP HTTP server will listen
# Must be between 1 and 65535
port: 8080

# Configuration of MCP tools
# Each tool has an enable/disable flag. At least one tool has to be enabled.
tools:
Expand Down
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/stackrox/stackrox-mcp
go 1.24

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/stretchr/testify v1.11.1
Expand All @@ -12,6 +13,7 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/google/jsonschema-go v0.3.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
Expand All @@ -20,7 +22,9 @@ require (
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // 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
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
14 changes: 12 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q=
github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/modelcontextprotocol/go-sdk v1.1.0 h1:Qjayg53dnKC4UZ+792W21e4BpwEZBzwgRW6LrjLWSwA=
github.com/modelcontextprotocol/go-sdk v1.1.0/go.mod h1:6fM3LCm3yV7pAs8isnKLn07oKtB0MP9LHd3DfAcKw10=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
Expand All @@ -36,12 +40,18 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
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 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
Loading