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
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,10 @@ Run the Vault MCP server:

```bash
docker run --network=mcp -p 8080:8080 -e VAULT_ADDR='http://vault-dev:8200' -e VAULT_TOKEN='<your-token-from-last-step>' -e TRANSPORT_MODE='http' vault-mcp-server:dev

# Filter tools (optional)
docker run -i --rm vault-mcp-server:dev --toolsets=sys,kv
docker run -i --rm vault-mcp-server:dev --tools=read_secret,list_mounts
Comment on lines +239 to +241

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

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

The new Docker examples for --toolsets / --tools omit the required Vault connection env vars (e.g., VAULT_ADDR, VAULT_TOKEN) and transport settings used in the preceding command, so as written they won’t successfully start a functional server. Consider showing these flags as additions to the existing docker run ... -e VAULT_ADDR=... -e VAULT_TOKEN=... example (or explicitly noting that the same env/network flags are still required).

Suggested change
# Filter tools (optional)
docker run -i --rm vault-mcp-server:dev --toolsets=sys,kv
docker run -i --rm vault-mcp-server:dev --tools=read_secret,list_mounts
# Filter tools (optional; same network/env settings as above)
docker run --network=mcp -e VAULT_ADDR='http://vault-dev:8200' -e VAULT_TOKEN='<your-token-from-last-step>' -e TRANSPORT_MODE='http' -i --rm vault-mcp-server:dev --toolsets=sys,kv
docker run --network=mcp -e VAULT_ADDR='http://vault-dev:8200' -e VAULT_TOKEN='<your-token-from-last-step>' -e TRANSPORT_MODE='http' -i --rm vault-mcp-server:dev --tools=read_secret,list_mounts

Copilot uses AI. Check for mistakes.
```

## Available Tools
Expand Down Expand Up @@ -332,6 +336,20 @@ Issues a new certificate using a PKI role.
- `ipSans`: (Optional) IP SANs for the certificate
- `ttl`: (Optional) Time-to-live for the certificate

### Tool Filtering

Control which tools are available using `--toolsets` (groups) or `--tools` (individual):

```bash
# Enable tool groups (default: all)
./vault-mcp-server --toolsets=sys,kv

# Enable specific tools only
./vault-mcp-server --tools=read_secret,list_mounts,enable_pki
```

Available toolsets: `sys`, `kv`, `pki`, `all`, `default`. See `pkg/toolsets/mapping.go` for individual tool names. Cannot use both flags together.

## Command Line Usage

```bash
Expand All @@ -340,10 +358,10 @@ Issues a new certificate using a PKI role.

# Run in stdio mode (default)
./vault-mcp-server
./vault-mcp-server stdio
./vault-mcp-server stdio [--log-file /path/to/log] [--toolsets <toolsets>] [--tools <tools>]

# Run in HTTP mode
./vault-mcp-server http --transport-port 8080 --transport-host 127.0.0.1
./vault-mcp-server http --transport-port 8080 --transport-host 127.0.0.1 [--toolsets <toolsets>] [--tools <tools>]

# Show version
./vault-mcp-server --version
Expand Down Expand Up @@ -414,6 +432,9 @@ vault-mcp-server/
│ │ ├── pki/ # PKI certificate tools
│ │ ├── sys/ # System management tools
│ │ └── tools.go # Tool registration
│ ├── toolsets/ # Toolset definitions and filtering
│ │ ├── toolsets.go # Toolset groups and helpers
│ │ └── mapping.go # Tool-to-toolset mapping
│ └── utils/ # Utility functions
├── scripts/ # Build and utility scripts
├── version/ # Version information
Expand Down
4 changes: 4 additions & 0 deletions cmd/vault-mcp-server/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
stdlog "log"
"os"

"github.com/hashicorp/vault-mcp-server/pkg/toolsets"
"github.com/mark3labs/mcp-go/server"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
Expand All @@ -31,6 +32,9 @@ func init() {
httpCmdAlias.Flags().StringP("transport-port", "p", DefaultBindPort, "Port to listen on")
httpCmdAlias.Flags().String("mcp-endpoint", DefaultEndPointPath, "Path for streamable HTTP endpoint")

rootCmd.PersistentFlags().String("toolsets", "all", toolsets.GenerateToolsetsHelp())
rootCmd.PersistentFlags().String("tools", "", toolsets.GenerateToolsHelp())

rootCmd.AddCommand(stdioCmd)
rootCmd.AddCommand(streamableHTTPCmd)
rootCmd.AddCommand(httpCmdAlias) // Add the alias for backward compatibility
Expand Down
104 changes: 90 additions & 14 deletions cmd/vault-mcp-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (

"github.com/hashicorp/vault-mcp-server/pkg/client"
"github.com/hashicorp/vault-mcp-server/pkg/tools"
"github.com/hashicorp/vault-mcp-server/pkg/toolsets"

"github.com/hashicorp/vault-mcp-server/version"

Expand Down Expand Up @@ -45,7 +46,7 @@ var (
Use: "stdio",
Short: "Start stdio server",
Long: `Start a server that communicates via standard input/output streams using JSON-RPC messages.`,
Run: func(_ *cobra.Command, _ []string) {
Run: func(cmd *cobra.Command, _ []string) {
logFile, err := rootCmd.PersistentFlags().GetString("log-file")
if err != nil {
stdlog.Fatal("Failed to get log file:", err)
Expand All @@ -55,7 +56,9 @@ var (
stdlog.Fatal("Failed to initialize logger:", err)
}

if err := runStdioServer(logger); err != nil {
enabledToolsets := getToolsetsFromCmd(cmd.Root(), logger)

if err := runStdioServer(logger, enabledToolsets); err != nil {
stdlog.Fatal("failed to run stdio server:", err)
}
},
Expand Down Expand Up @@ -91,7 +94,9 @@ You can specify the host, port, and endpoint path to customize where the server
stdlog.Fatal("Failed to get endpoint path:", err)
}

if err := runHTTPServer(logger, host, port, endpointPath); err != nil {
enabledToolsets := getToolsetsFromCmd(cmd.Root(), logger)

if err := runHTTPServer(logger, host, port, endpointPath, enabledToolsets); err != nil {
stdlog.Fatal("failed to run streamableHTTP server:", err)
}
},
Expand All @@ -110,12 +115,12 @@ You can specify the host, port, and endpoint path to customize where the server
}
)

func runHTTPServer(logger *log.Logger, host string, port string, endpointPath string) error {
func runHTTPServer(logger *log.Logger, host string, port string, endpointPath string, enabledToolsets []string) error {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

hcServer := NewServer(version.Version, logger)
tools.InitTools(hcServer, logger)
tools.RegisterTools(hcServer, logger, enabledToolsets)

return httpServerInit(ctx, hcServer, logger, host, port, endpointPath)
}
Expand Down Expand Up @@ -227,12 +232,12 @@ func httpServerInit(ctx context.Context, hcServer *server.MCPServer, logger *log
return nil
}

func runStdioServer(logger *log.Logger) error {
func runStdioServer(logger *log.Logger, enabledToolsets []string) error {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

hcServer := NewServer(version.Version, logger)
tools.InitTools(hcServer, logger)
tools.RegisterTools(hcServer, logger, enabledToolsets)

return serverInit(ctx, hcServer, logger)
}
Expand Down Expand Up @@ -283,14 +288,16 @@ func runDefaultCommand(cmd *cobra.Command, _ []string) {
stdlog.Fatal("Failed to initialize logger:", err)
}

if err := runStdioServer(logger); err != nil {
enabledToolsets := getToolsetsFromCmd(cmd, logger)

if err := runStdioServer(logger, enabledToolsets); err != nil {
stdlog.Fatal("failed to run stdio server:", err)
}
}

func main() {
// Check environment variables first - they override command line args
if shouldUseHTTPMode() {
if shouldUseStreamableHTTPMode() {
port := getHTTPPort()
host := getHTTPHost()
endpointPath := getEndpointPath(nil)
Expand All @@ -301,8 +308,10 @@ func main() {
stdlog.Fatal("Failed to initialize logger:", err)
}

if err := runHTTPServer(logger, host, port, endpointPath); err != nil {
stdlog.Fatal("failed to run HTTP server:", err)
enabledToolsets := getToolsetsFromCmd(rootCmd, logger)

if err := runHTTPServer(logger, host, port, endpointPath, enabledToolsets); err != nil {
stdlog.Fatal("failed to run StreamableHTTP server:", err)
}
return
}
Expand All @@ -314,8 +323,8 @@ func main() {
}
}

// shouldUseHTTPMode checks if environment variables indicate HTTP mode
func shouldUseHTTPMode() bool {
// shouldUseStreamableHTTPMode checks if environment variables indicate HTTP mode
func shouldUseStreamableHTTPMode() bool {
transportMode := os.Getenv("TRANSPORT_MODE")
return transportMode == "http" || transportMode == "streamable-http" ||
os.Getenv("TRANSPORT_PORT") != "" ||
Expand All @@ -339,7 +348,74 @@ func getHTTPHost() string {
return DefaultBindAddress
}

// Add function to get endpoint path from environment or flag
// parseToolsets parses and validates the toolsets flag value
func parseToolsets(toolsetsFlag string, logger *log.Logger) []string {
rawToolsets := strings.Split(toolsetsFlag, ",")

cleaned, invalid := toolsets.CleanToolsets(rawToolsets)
if len(invalid) > 0 {
logger.Warnf("Invalid toolsets ignored: %v", invalid)
}

expanded := toolsets.ExpandDefaultToolset(cleaned)

logger.Infof("Enabled toolsets: %v", expanded)
return expanded
}

// parseIndividualTools parses and validates the tools flag value
func parseIndividualTools(toolsFlag string, logger *log.Logger) []string {
rawTools := strings.Split(toolsFlag, ",")

validTools, invalidTools := toolsets.ParseIndividualTools(rawTools)
if len(invalidTools) > 0 {
logger.Warnf("Invalid tool names ignored: %v", invalidTools)
}

if len(validTools) == 0 {
logger.Warn("No valid tools specified, falling back to default toolsets")
return parseToolsets("default", logger)
}

// Use the public API to enable individual tools mode
result := toolsets.EnableIndividualTools(validTools)
logger.Infof("Enabled individual tools: %v", validTools)
return result
}

func getToolsetsFromCmd(cmd *cobra.Command, logger *log.Logger) []string {
// Check if --tools flag is set (individual tool mode)
toolsFlag, err := cmd.Flags().GetString("tools")
if err != nil {
// Try root persistent flags
toolsFlag, err = cmd.Root().PersistentFlags().GetString("tools")
}

if err == nil && toolsFlag != "" {
// Ensure --toolsets is not also set
toolsetsFlag, _ := cmd.Flags().GetString("toolsets")
if toolsetsFlag == "" {
toolsetsFlag, _ = cmd.Root().PersistentFlags().GetString("toolsets")
}
if toolsetsFlag != "" && toolsetsFlag != "default" {
Comment on lines +395 to +400

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

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

The mutual-exclusion check for --tools vs --toolsets will always trigger because toolsets is a persistent flag with default value "all" (so toolsetsFlag is never empty even when the user did not set it). This makes --tools=... unusable unless the user also overrides --toolsets to default, which contradicts the intent.

Consider checking whether the toolsets flag was explicitly set (e.g., Flags().Changed("toolsets") / PersistentFlags().Changed("toolsets") or inspecting the specific FlagSet where it was defined) instead of comparing the resolved value against empty/default strings.

Suggested change
// Ensure --toolsets is not also set
toolsetsFlag, _ := cmd.Flags().GetString("toolsets")
if toolsetsFlag == "" {
toolsetsFlag, _ = cmd.Root().PersistentFlags().GetString("toolsets")
}
if toolsetsFlag != "" && toolsetsFlag != "default" {
// Ensure --toolsets is not also explicitly set
toolsetsSet := cmd.Flags().Changed("toolsets") || cmd.Root().PersistentFlags().Changed("toolsets")
if toolsetsSet {

Copilot uses AI. Check for mistakes.
logger.Fatal("Cannot use both --tools and --toolsets flags together")
}
return parseIndividualTools(toolsFlag, logger)
}

// Fall back to toolsets mode
toolsetsFlag, err := cmd.Flags().GetString("toolsets")
if err != nil {
toolsetsFlag, err = cmd.Root().PersistentFlags().GetString("toolsets")
if err != nil {
logger.Warnf("Failed to get toolsets flag, using default: %v", err)
toolsetsFlag = "default"
}
}
return parseToolsets(toolsetsFlag, logger)
}

// getEndpointPath returns the endpoint path from environment or flag.
func getEndpointPath(cmd *cobra.Command) string {
// First check environment variable
if envPath := os.Getenv("MCP_ENDPOINT"); envPath != "" {
Expand Down
Loading
Loading