-
Notifications
You must be signed in to change notification settings - Fork 22
feat(sys): add operational observability tools (leases, metrics, replication, cluster health, host info) #86
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Format the response | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| jsonData, err := json.MarshalIndent(secret.Data, "", " ") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+83
to
+84
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 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, "", " ") |
There was a problem hiding this comment.
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_secretsreturns[]). 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.