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
112 changes: 112 additions & 0 deletions pkg/tools/sys/list_auth_methods.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Copyright IBM Corp. 2025
// SPDX-License-Identifier: MPL-2.0

package sys

import (
"context"
"encoding/json"
"fmt"

"github.com/hashicorp/vault-mcp-server/pkg/client"
"github.com/hashicorp/vault-mcp-server/pkg/utils"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
log "github.com/sirupsen/logrus"
)

type AuthMethod struct {
Path string `json:"path"` // Path where the auth method is mounted
Type string `json:"type"` // Type of the auth method (e.g., userpass, approle, oidc)
Description string `json:"description"` // Description of the auth method
Accessor string `json:"accessor"` // Unique accessor for the auth method
Local bool `json:"local"` // Whether the auth method is local to the namespace
SealWrap bool `json:"seal_wrap"` // Whether seal wrapping is enabled
ExternalEntropy bool `json:"external_entropy"` // Whether external entropy is used
DefaultLeaseTTL int `json:"default_lease_ttl"` // Default lease TTL
MaxLeaseTTL int `json:"max_lease_ttl"` // Max lease TTL
}

// ListAuthMethods creates a tool for listing Vault auth methods
func ListAuthMethods(logger *log.Logger) server.ServerTool {
return server.ServerTool{
Tool: mcp.NewTool("list_auth_methods",
mcp.WithDescription("List all enabled authentication methods in Vault. Returns information about each auth method including type, path, and configuration."),
mcp.WithToolAnnotation(
mcp.ToolAnnotation{
IdempotentHint: utils.ToBoolPtr(true),
ReadOnlyHint: utils.ToBoolPtr(true),
},
),
mcp.WithString("namespace",
mcp.DefaultString(""),
mcp.Description("Namespace path to list auth methods from (e.g., 'admin/' or empty for root). Defaults to current namespace.")),
),
Handler: func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
return listAuthMethodsHandler(ctx, req, logger)
},
}
}

func listAuthMethodsHandler(ctx context.Context, req mcp.CallToolRequest, logger *log.Logger) (*mcp.CallToolResult, error) {
logger.Debug("Handling list_auth_methods request")

// Extract parameters
args, ok := req.Params.Arguments.(map[string]interface{})
if !ok {
return mcp.NewToolResultError("Missing or invalid arguments format"), nil
}

namespace, _ := args["namespace"].(string)

Comment on lines +54 to +61

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.

list_auth_methods declares only an optional namespace, but the handler unconditionally type-asserts req.Params.Arguments to a map and errors if it’s nil. A CallToolRequest can omit Arguments entirely (nil), so this tool will fail when invoked with no parameters. Consider treating nil arguments as an empty map and only parsing namespace when arguments are provided (similar to other sys tools that allow optional params).

Suggested change
// Extract parameters
args, ok := req.Params.Arguments.(map[string]interface{})
if !ok {
return mcp.NewToolResultError("Missing or invalid arguments format"), nil
}
namespace, _ := args["namespace"].(string)
// Extract parameters (namespace is optional)
var namespace string
if req.Params.Arguments != nil {
args, ok := req.Params.Arguments.(map[string]interface{})
if !ok {
return mcp.NewToolResultError("Invalid arguments format"), nil
}
if ns, ok := args["namespace"].(string); ok {
namespace = ns
}
}

Copilot uses AI. Check for mistakes.
logger.WithFields(log.Fields{
"namespace": namespace,
}).Debug("Listing auth methods")

// Get Vault client from context
vault, err := client.GetVaultClientFromContext(ctx, logger)
if err != nil {
logger.WithError(err).Error("Failed to get Vault client")
return mcp.NewToolResultError(fmt.Sprintf("Failed to get Vault client: %v", err)), nil
}

// Create a new client instance with the specified namespace if provided
nsClient := vault
if namespace != "" {
nsClient = vault.WithNamespace(namespace)
logger.WithField("namespace", namespace).Debug("Using specified namespace")
}

// List auth methods from Vault
auths, err := nsClient.Sys().ListAuth()
if err != nil {
logger.WithError(err).Error("Failed to list auth methods")
return mcp.NewToolResultError(fmt.Sprintf("Failed to list auth methods: %v", err)), nil
}

var results []*AuthMethod
for path, auth := range auths {
method := &AuthMethod{
Path: path,
Type: auth.Type,
Description: auth.Description,
Accessor: auth.Accessor,
Local: auth.Local,
SealWrap: auth.SealWrap,
ExternalEntropy: auth.ExternalEntropyAccess,
DefaultLeaseTTL: auth.Config.DefaultLeaseTTL,
MaxLeaseTTL: auth.Config.MaxLeaseTTL,
}
results = append(results, method)
}

// Marshal the struct to JSON
jsonData, err := json.Marshal(results)
if err != nil {
logger.WithError(err).Error("Failed to marshal auth methods to JSON")
return mcp.NewToolResultError(fmt.Sprintf("Error marshaling JSON: %v", err)), nil
}

logger.WithField("auth_method_count", len(results)).Debug("Successfully listed auth methods")
return mcp.NewToolResultText(string(jsonData)), nil
}
152 changes: 152 additions & 0 deletions pkg/tools/sys/read_auth_method.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// Copyright IBM Corp. 2025
// SPDX-License-Identifier: MPL-2.0

package sys

import (
"context"
"encoding/json"
"fmt"
"strings"

"github.com/hashicorp/vault-mcp-server/pkg/client"
"github.com/hashicorp/vault-mcp-server/pkg/utils"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
log "github.com/sirupsen/logrus"
)

type AuthMethodDetail struct {
Path string `json:"path"` // Path where the auth method is mounted
Type string `json:"type"` // Type of the auth method
Description string `json:"description"` // Description of the auth method
Accessor string `json:"accessor"` // Unique accessor for the auth method
Local bool `json:"local"` // Whether the auth method is local
SealWrap bool `json:"seal_wrap"` // Whether seal wrapping is enabled
ExternalEntropy bool `json:"external_entropy"` // Whether external entropy is used
Config map[string]any `json:"config,omitempty"` // Full configuration details
Options map[string]any `json:"options,omitempty"` // Auth method options
}

// ReadAuthMethod creates a tool for reading details of a specific auth method
func ReadAuthMethod(logger *log.Logger) server.ServerTool {
return server.ServerTool{
Tool: mcp.NewTool("read_auth_method",
mcp.WithDescription("Read detailed configuration and information about a specific authentication method in Vault. Returns full details including config, options, and metadata."),
mcp.WithToolAnnotation(
mcp.ToolAnnotation{
IdempotentHint: utils.ToBoolPtr(true),
ReadOnlyHint: utils.ToBoolPtr(true),
},
),
mcp.WithString("path",
mcp.Required(),
mcp.Description("The mount path of the auth method to read (e.g., 'approle/', 'userpass/', 'oidc/'). Include trailing slash.")),

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 path parameter description says “Include trailing slash”, but the handler normalizes the value by adding a trailing slash when missing. To avoid confusing callers, consider updating the description to indicate the trailing slash is optional (it will be added automatically).

Suggested change
mcp.Description("The mount path of the auth method to read (e.g., 'approle/', 'userpass/', 'oidc/'). Include trailing slash.")),
mcp.Description("The mount path of the auth method to read (e.g., 'approle/', 'userpass/', 'oidc/'). Trailing slash is optional; it will be added automatically if missing.")),

Copilot uses AI. Check for mistakes.
mcp.WithString("namespace",
mcp.DefaultString(""),
mcp.Description("Namespace path where the auth method exists (e.g., 'admin/' or empty for root). Defaults to current namespace.")),
),
Handler: func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
return readAuthMethodHandler(ctx, req, logger)
},
}
}

func readAuthMethodHandler(ctx context.Context, req mcp.CallToolRequest, logger *log.Logger) (*mcp.CallToolResult, error) {
logger.Debug("Handling read_auth_method request")

// Extract parameters
args, ok := req.Params.Arguments.(map[string]interface{})
if !ok {
return mcp.NewToolResultError("Missing or invalid arguments format"), nil
}

path, ok := args["path"].(string)
if !ok || path == "" {
return mcp.NewToolResultError("Missing or invalid 'path' parameter"), nil
}

// Ensure path has trailing slash for consistency with Vault's API
if !strings.HasSuffix(path, "/") {
path = path + "/"
}

namespace, _ := args["namespace"].(string)

logger.WithFields(log.Fields{
"path": path,
"namespace": namespace,
}).Debug("Reading auth method")

// Get Vault client from context
vault, err := client.GetVaultClientFromContext(ctx, logger)
if err != nil {
logger.WithError(err).Error("Failed to get Vault client")
return mcp.NewToolResultError(fmt.Sprintf("Failed to get Vault client: %v", err)), nil
}

// Create a new client instance with the specified namespace if provided
nsClient := vault
if namespace != "" {
nsClient = vault.WithNamespace(namespace)
logger.WithField("namespace", namespace).Debug("Using specified namespace")
}

// Read auth method from Vault
auth, err := nsClient.Sys().ListAuth()
if err != nil {
logger.WithError(err).Error("Failed to list auth methods")
return mcp.NewToolResultError(fmt.Sprintf("Failed to read auth methods: %v", err)), nil
}

// Find the specific auth method by path
authMethod, found := auth[path]
if !found {
logger.WithField("path", path).Warn("Auth method not found")
return mcp.NewToolResultError(fmt.Sprintf("Auth method at path '%s' not found", path)), nil
}

// Build detailed response
result := &AuthMethodDetail{
Path: path,
Type: authMethod.Type,
Description: authMethod.Description,
Accessor: authMethod.Accessor,
Local: authMethod.Local,
SealWrap: authMethod.SealWrap,
ExternalEntropy: authMethod.ExternalEntropyAccess,
}

// Convert Options from map[string]string to map[string]any
if authMethod.Options != nil {
result.Options = make(map[string]any)
for k, v := range authMethod.Options {
result.Options[k] = v
}
}

// Include full config details
if authMethod.Config.DefaultLeaseTTL > 0 || authMethod.Config.MaxLeaseTTL > 0 {
result.Config = map[string]any{
"default_lease_ttl": authMethod.Config.DefaultLeaseTTL,
"max_lease_ttl": authMethod.Config.MaxLeaseTTL,
"force_no_cache": authMethod.Config.ForceNoCache,
"token_type": authMethod.Config.TokenType,
"audit_non_hmac_request_keys": authMethod.Config.AuditNonHMACRequestKeys,
"audit_non_hmac_response_keys": authMethod.Config.AuditNonHMACResponseKeys,
"listing_visibility": authMethod.Config.ListingVisibility,
"passthrough_request_headers": authMethod.Config.PassthroughRequestHeaders,
"allowed_response_headers": authMethod.Config.AllowedResponseHeaders,
}
Comment on lines +128 to +140

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 config block is only included when DefaultLeaseTTL or MaxLeaseTTL is > 0, but other config fields (e.g., force_no_cache, headers, token_type, etc.) can be set even when TTLs are 0. This means read_auth_method can return an empty config despite the tool description claiming it returns full config details. Consider populating result.Config unconditionally (or gating on the presence of any non-zero/non-empty config field), and keep the tool description aligned with the actual output.

Suggested change
// Include full config details
if authMethod.Config.DefaultLeaseTTL > 0 || authMethod.Config.MaxLeaseTTL > 0 {
result.Config = map[string]any{
"default_lease_ttl": authMethod.Config.DefaultLeaseTTL,
"max_lease_ttl": authMethod.Config.MaxLeaseTTL,
"force_no_cache": authMethod.Config.ForceNoCache,
"token_type": authMethod.Config.TokenType,
"audit_non_hmac_request_keys": authMethod.Config.AuditNonHMACRequestKeys,
"audit_non_hmac_response_keys": authMethod.Config.AuditNonHMACResponseKeys,
"listing_visibility": authMethod.Config.ListingVisibility,
"passthrough_request_headers": authMethod.Config.PassthroughRequestHeaders,
"allowed_response_headers": authMethod.Config.AllowedResponseHeaders,
}
// Include full config details (independent of TTL values)
result.Config = map[string]any{
"default_lease_ttl": authMethod.Config.DefaultLeaseTTL,
"max_lease_ttl": authMethod.Config.MaxLeaseTTL,
"force_no_cache": authMethod.Config.ForceNoCache,
"token_type": authMethod.Config.TokenType,
"audit_non_hmac_request_keys": authMethod.Config.AuditNonHMACRequestKeys,
"audit_non_hmac_response_keys": authMethod.Config.AuditNonHMACResponseKeys,
"listing_visibility": authMethod.Config.ListingVisibility,
"passthrough_request_headers": authMethod.Config.PassthroughRequestHeaders,
"allowed_response_headers": authMethod.Config.AllowedResponseHeaders,

Copilot uses AI. Check for mistakes.
}

// Marshal the struct to JSON
jsonData, err := json.Marshal(result)
if err != nil {
logger.WithError(err).Error("Failed to marshal auth method to JSON")
return mcp.NewToolResultError(fmt.Sprintf("Error marshaling JSON: %v", err)), nil
}

logger.WithField("path", path).Debug("Successfully read auth method")
return mcp.NewToolResultText(string(jsonData)), nil
}
7 changes: 7 additions & 0 deletions pkg/tools/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ func InitTools(hcServer *server.MCPServer, logger *log.Logger) {
deleteMountTool := sys.DeleteMount(logger)
hcServer.AddTool(deleteMountTool.Tool, deleteMountTool.Handler)

// Tools for Vault auth method management
listAuthMethodsTool := sys.ListAuthMethods(logger)
hcServer.AddTool(listAuthMethodsTool.Tool, listAuthMethodsTool.Handler)

readAuthMethodTool := sys.ReadAuthMethod(logger)
hcServer.AddTool(readAuthMethodTool.Tool, readAuthMethodTool.Handler)

// Tools for KV secrets management
listSecretsTool := kv.ListSecrets(logger)
hcServer.AddTool(listSecretsTool.Tool, listSecretsTool.Handler)
Expand Down
Loading