diff --git a/README.md b/README.md index a9afcdb..d15e26d 100644 --- a/README.md +++ b/README.md @@ -96,8 +96,9 @@ HTTP server settings for the MCP server. | Option | Environment Variable | Type | Required | Default | Description | |--------|---------------------|------|----------|---------|-------------| -| `server.address` | `STACKROX_MCP__SERVER__ADDRESS` | string | No | `0.0.0.0` | HTTP server listen address | -| `server.port` | `STACKROX_MCP__SERVER__PORT` | int | No | `8080` | HTTP server listen port (must be 1-65535) | +| `server.type` | `STACKROX_MCP__SERVER__TYPE` | string | No | `streamable-http` | Server transport type: `streamable-http` (HTTP server) or `stdio` (stdio transport). **Note**: stdio transport requires `central.auth_type` to be set to `static` | +| `server.address` | `STACKROX_MCP__SERVER__ADDRESS` | string | No | `0.0.0.0` | HTTP server listen address (only applies when `server.type` is `http`) | +| `server.port` | `STACKROX_MCP__SERVER__PORT` | int | No | `8080` | HTTP server listen port (must be 1-65535, only applies when `server.type` is `http`) | #### Tools Configuration @@ -138,7 +139,9 @@ The server will start on `http://0.0.0.0:8080` by default (configurable via `ser ### Connecting with Claude Code CLI -Add the MCP server to Claude Code using command-line options: +#### HTTP Transport + +Add the MCP server to Claude Code using HTTP transport: ```bash claude mcp add stackrox \ @@ -147,6 +150,22 @@ claude mcp add stackrox \ --url http://localhost:8080 ``` +#### Stdio Transport + +Add the MCP server to Claude Code using stdio transport with static authentication: + +```bash +claude mcp add --transport stdio stackrox \ + --env STACKROX_MCP__SERVER__TYPE=stdio \ + --env STACKROX_MCP__CENTRAL__AUTH_TYPE=static \ + --env STACKROX_MCP__CENTRAL__API_TOKEN="${ROX_TOKEN}" \ + --env STACKROX_MCP__CENTRAL__URL=central.stackrox:443 \ + --env STACKROX_MCP__TOOLS__CONFIG_MANAGER__ENABLED=true \ + -- /path/to/stackrox-mcp +``` + +**Important**: Stdio transport requires static authentication (`central.auth_type=static`). Passthrough authentication is not supported with stdio transport. + ### Verifying Connection List configured MCP servers: diff --git a/internal/config/config.go b/internal/config/config.go index cee0ce8..316590d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -29,6 +29,7 @@ type Config struct { } type authType string +type serverType string const ( // AuthTypePassthrough defines auth flow where API token, used to communicate with MCP server, @@ -38,6 +39,11 @@ const ( // AuthTypeStatic defines auth flow where API token is statically configured and // defined in configuration or environment variable. AuthTypeStatic authType = "static" + + // ServerTypeStdio indicates server runs over stdio. + ServerTypeStdio serverType = "stdio" + // ServerTypeStreamableHTTP indicates server runs over streamable-http. + ServerTypeStreamableHTTP serverType = "streamable-http" ) // CentralConfig contains StackRox Central connection configuration. @@ -62,8 +68,9 @@ type GlobalConfig struct { // ServerConfig contains HTTP server configuration. type ServerConfig struct { - Address string `mapstructure:"address"` - Port int `mapstructure:"port"` + Type serverType `mapstructure:"type"` + Address string `mapstructure:"address"` + Port int `mapstructure:"port"` } // ToolsConfig contains configuration for individual MCP tools. @@ -138,6 +145,7 @@ func setDefaults(viper *viper.Viper) { viper.SetDefault("server.address", "0.0.0.0") viper.SetDefault("server.port", defaultPort) + viper.SetDefault("server.type", ServerTypeStreamableHTTP) viper.SetDefault("tools.vulnerability.enabled", false) viper.SetDefault("tools.config_manager.enabled", false) @@ -207,6 +215,14 @@ func (cc *CentralConfig) validate() error { } func (sc *ServerConfig) validate() error { + if sc.Type != ServerTypeStreamableHTTP && sc.Type != ServerTypeStdio { + return errors.New("server.type must be either streamable-http or stdio") + } + + if sc.Type == ServerTypeStdio { + return nil + } + if sc.Address == "" { return errors.New("server.address is required") } @@ -232,6 +248,10 @@ func (c *Config) Validate() error { return errors.New("at least one tool has to be enabled") } + if c.Server.Type == ServerTypeStdio && c.Central.AuthType != AuthTypeStatic { + return errors.New("stdio server does require static auth type") + } + return nil } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 277889d..be46ed5 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -28,6 +28,7 @@ func getDefaultConfig() *Config { ReadOnlyTools: false, }, Server: ServerConfig{ + Type: ServerTypeStreamableHTTP, Address: "localhost", Port: 8080, }, @@ -201,6 +202,26 @@ tools: assert.Contains(t, err.Error(), "central.url is required") } +func TestLoadConfig_ServerTypeValidationPass(t *testing.T) { + validYAMLConfig := ` +central: + url: "localhost:8080" + auth_type: static + api_token: "test-token" +server: + type: stdio + address: "" + port: 0 +tools: + vulnerability: + enabled: true +` + + configPath := testutil.WriteYAMLFile(t, validYAMLConfig) + _, err := LoadConfig(configPath) + require.NoError(t, err) +} + func TestValidate_MissingURL(t *testing.T) { cfg := getDefaultConfig() cfg.Central.URL = "" @@ -299,6 +320,26 @@ func TestValidate_AuthTypePassthrough_ForbidsAPIToken(t *testing.T) { assert.Contains(t, err.Error(), "passthrough") } +func TestValidate_AuthTypePassthrough_ForbidsStdio(t *testing.T) { + cfg := getDefaultConfig() + cfg.Central.AuthType = AuthTypePassthrough + cfg.Central.APIToken = "" + cfg.Server.Type = ServerTypeStdio + + err := cfg.Validate() + require.Error(t, err) + assert.Contains(t, err.Error(), "stdio server does require static auth type") +} + +func TestValidate_ServerType_InvalidType(t *testing.T) { + cfg := getDefaultConfig() + cfg.Server.Type = "invalid-type" + + err := cfg.Validate() + require.Error(t, err) + assert.Contains(t, err.Error(), "server.type must be either streamable-http or stdio") +} + func TestValidate_AuthTypePassthrough_Success(t *testing.T) { cfg := getDefaultConfig() cfg.Central.AuthType = AuthTypePassthrough diff --git a/internal/logging/logging.go b/internal/logging/logging.go index 0ecb7b4..936b867 100644 --- a/internal/logging/logging.go +++ b/internal/logging/logging.go @@ -31,7 +31,7 @@ func SetupLogging() { } // Initialize slog with JSON handler. - logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{ Level: logLevel, })) diff --git a/internal/server/server.go b/internal/server/server.go index 4eca1db..7286968 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -53,6 +53,10 @@ func NewServer(cfg *config.Config, registry *toolsets.Registry) *Server { func (s *Server) Start(ctx context.Context) error { s.registerTools() + if s.cfg.Server.Type == config.ServerTypeStdio { + return errors.Wrap(s.mcp.Run(ctx, &mcp.StdioTransport{}), "running mcp over stdio") + } + // Create a new ServeMux for routing. mux := http.NewServeMux() s.registerRouteHealth(mux)