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
97 changes: 97 additions & 0 deletions pkg/tools/sys/list_leases.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// 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"
)

// ListLeases creates a tool for listing leases in Vault
func ListLeases(logger *log.Logger) server.ServerTool {
return server.ServerTool{
Tool: mcp.NewTool("list_leases",
mcp.WithDescription("List leases in Vault at a specific prefix path. Returns keys/paths containing leases. Omit prefix to list top-level lease paths. Use this to discover what lease paths exist before reading specific lease details. Useful for exploring lease hierarchy and finding active leases."),
mcp.WithString("prefix",
mcp.Description("Lease path prefix to list under (e.g., 'database/creds', 'pki/issue'). Omit to list top-level lease paths. The prefix determines which lease subtree to explore."),
),
mcp.WithToolAnnotation(
mcp.ToolAnnotation{
IdempotentHint: utils.ToBoolPtr(true),
ReadOnlyHint: utils.ToBoolPtr(true),
},
),
),
Handler: func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
return listLeasesHandler(ctx, req, logger)
},
}
}

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

// Extract parameters
args, ok := req.Params.Arguments.(map[string]interface{})
if !ok {
args = make(map[string]interface{})
}

// Extract optional prefix parameter
prefix := ""
if prefixVal, ok := args["prefix"].(string); ok {
prefix = prefixVal
}

logger.WithFields(log.Fields{
"prefix": prefix,
}).Debug("Listing Vault leases")

// 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
}

// Build the path
path := "sys/leases/lookup"
if prefix != "" {
path = fmt.Sprintf("sys/leases/lookup/%s", prefix)
}

// List leases at the specified path
secret, err := vault.Logical().List(path)
if err != nil {
logger.WithError(err).Error("Failed to list leases")
return mcp.NewToolResultError(fmt.Sprintf("Failed to list leases: %v", err)), nil
}

if secret == nil || secret.Data == nil {
// No leases at this path
return mcp.NewToolResultText("No leases found at this path"), nil
Comment on lines +79 to +80

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.

This "no leases" branch returns a human string, while other list tools in this repo return JSON (e.g., list_secrets returns []). Returning non-JSON here makes the output harder to consume programmatically and inconsistent with existing tool behavior. Consider returning a JSON empty list (or { "keys": [] }) instead.

Suggested change
// No leases at this path
return mcp.NewToolResultText("No leases found at this path"), nil
// No leases at this path; return an empty JSON array for consistency with other list tools
return mcp.NewToolResultText("[]"), nil

Copilot uses AI. Check for mistakes.
}

// Format the response
jsonData, err := json.MarshalIndent(secret.Data, "", " ")
Comment on lines +83 to +84

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 list response is marshaled as the full secret.Data map. Other list tools typically extract and return just the key list (e.g., secret.Data["keys"]) as JSON. Consider extracting the keys field and returning a JSON array for consistency with list_secrets/list_pki_roles patterns.

Suggested change
// Format the response
jsonData, err := json.MarshalIndent(secret.Data, "", " ")
// Format the response: extract and return only the "keys" field for consistency
keysVal, ok := secret.Data["keys"]
if !ok {
// No "keys" field; return an empty list to keep the response shape consistent
keysVal = []string{}
}
// Normalize keysVal into something JSON-marshalable, preferring []string
var (
marshalTarget interface{}
keysSlice []string
)
switch v := keysVal.(type) {
case []string:
marshalTarget = v
case []interface{}:
keysSlice = make([]string, 0, len(v))
for _, item := range v {
keysSlice = append(keysSlice, fmt.Sprint(item))
}
marshalTarget = keysSlice
default:
// Fallback: wrap single value or unexpected type into a one-element string slice
if v != nil {
marshalTarget = []string{fmt.Sprint(v)}
} else {
marshalTarget = []string{}
}
}
jsonData, err := json.MarshalIndent(marshalTarget, "", " ")

Copilot uses AI. Check for mistakes.
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("Failed to format lease list: %v", err)), nil
}

resultText := string(jsonData)

logger.WithFields(log.Fields{
"prefix": prefix,
"data_length": len(resultText),
}).Debug("Successfully listed leases")

return mcp.NewToolResultText(resultText), nil
}
Loading
Loading