-
Notifications
You must be signed in to change notification settings - Fork 22
feat: add new KV tools and improve existing ones #80
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
This file was deleted.
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,148 @@ | ||||||
| // Copyright IBM Corp. 2025 | ||||||
| // SPDX-License-Identifier: MPL-2.0 | ||||||
|
|
||||||
| package kv | ||||||
|
|
||||||
| import ( | ||||||
| "context" | ||||||
| "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" | ||||||
| ) | ||||||
|
|
||||||
| // DeleteSecretVersions creates a tool for soft-deleting specific versions of a secret in a Vault KV v2 mount | ||||||
| func DeleteSecretVersions(logger *log.Logger) server.ServerTool { | ||||||
| return server.ServerTool{ | ||||||
| Tool: mcp.NewTool("delete_secret_versions", | ||||||
| mcp.WithToolAnnotation( | ||||||
| mcp.ToolAnnotation{ | ||||||
| DestructiveHint: utils.ToBoolPtr(true), | ||||||
| IdempotentHint: utils.ToBoolPtr(true), | ||||||
| }, | ||||||
| ), | ||||||
| mcp.WithDescription("Soft-delete specific versions of a secret in a KV v2 mount in Vault. The secret data is marked as deleted but can be recovered using undelete_secret. Only supported on KV v2 mounts."), | ||||||
| mcp.WithString("mount", | ||||||
|
Comment on lines
+28
to
+29
|
||||||
| mcp.Required(), | ||||||
| mcp.Description("The mount path of the secret engine."), | ||||||
| ), | ||||||
| mcp.WithString("path", | ||||||
| mcp.Required(), | ||||||
| mcp.Description("The full path to the secret without the mount prefix."), | ||||||
| ), | ||||||
| mcp.WithArray("versions", | ||||||
| mcp.Description("An array of version numbers to soft-delete. For example: [1, 3, 5]. If not specified, the latest version is soft-deleted. Soft-deleted versions can be recovered with undelete_secret."), | ||||||
| ), | ||||||
|
Comment on lines
+37
to
+39
|
||||||
| ), | ||||||
| Handler: func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { | ||||||
| return deleteSecretVersionsHandler(ctx, req, logger) | ||||||
| }, | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| func deleteSecretVersionsHandler(ctx context.Context, req mcp.CallToolRequest, logger *log.Logger) (*mcp.CallToolResult, error) { | ||||||
| logger.Debug("Handling delete_secret_versions request") | ||||||
|
|
||||||
| // Extract parameters | ||||||
| args, ok := req.Params.Arguments.(map[string]interface{}) | ||||||
| if !ok { | ||||||
| return mcp.NewToolResultError("Missing or invalid arguments format"), nil | ||||||
| } | ||||||
|
|
||||||
| mount, err := utils.ExtractMountPath(args) | ||||||
| if err != nil { | ||||||
| return mcp.NewToolResultError(err.Error()), nil | ||||||
| } | ||||||
|
|
||||||
| path, ok := args["path"].(string) | ||||||
| if !ok || path == "" { | ||||||
| return mcp.NewToolResultError("Missing or invalid 'path' parameter"), nil | ||||||
| } | ||||||
|
|
||||||
| // 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 | ||||||
| } | ||||||
|
|
||||||
| isV2, err := getMountInfo(vault, mount) | ||||||
| if err != nil { | ||||||
| return mcp.NewToolResultError(err.Error()), nil | ||||||
| } | ||||||
|
|
||||||
| if !isV2 { | ||||||
| return mcp.NewToolResultError("delete_secret_versions is only supported on KV v2 mounts"), nil | ||||||
| } | ||||||
|
|
||||||
| versionsRaw, hasVersions := args["versions"].([]interface{}) | ||||||
|
|
||||||
| if hasVersions && len(versionsRaw) > 0 { | ||||||
| // Convert float64 values from JSON to int | ||||||
| versions := make([]int, 0, len(versionsRaw)) | ||||||
| for _, v := range versionsRaw { | ||||||
| vFloat, ok := v.(float64) | ||||||
| if !ok { | ||||||
| return mcp.NewToolResultError("Invalid version number in 'versions' array — each element must be a number"), nil | ||||||
| } | ||||||
| versions = append(versions, int(vFloat)) | ||||||
| } | ||||||
|
|
||||||
| logger.WithFields(log.Fields{ | ||||||
| "mount": mount, | ||||||
| "path": path, | ||||||
| "versions": versions, | ||||||
| }).Debug("Soft-deleting secret versions") | ||||||
|
|
||||||
| // Soft-delete specific versions at mount/delete/path | ||||||
| fullPath := fmt.Sprintf("%s/delete/%s", mount, strings.TrimPrefix(path, "/")) | ||||||
| _, err = vault.Logical().Write(fullPath, map[string]interface{}{ | ||||||
| "versions": versions, | ||||||
| }) | ||||||
| if err != nil { | ||||||
| logger.WithError(err).WithFields(log.Fields{ | ||||||
| "mount": mount, | ||||||
| "path": path, | ||||||
| "full_path": fullPath, | ||||||
| "versions": versions, | ||||||
| }).Error("Failed to soft-delete secret versions") | ||||||
| return mcp.NewToolResultError(fmt.Sprintf("Failed to soft-delete secret versions: %v", err)), nil | ||||||
| } | ||||||
|
|
||||||
| logger.WithFields(log.Fields{ | ||||||
| "mount": mount, | ||||||
| "path": path, | ||||||
| "versions": versions, | ||||||
| }).Info("Successfully soft-deleted secret versions") | ||||||
|
|
||||||
| return mcp.NewToolResultText(fmt.Sprintf("Successfully soft-deleted versions %v of secret at path '%s' in mount '%s'. Use undelete_secret to recover them.", versions, path, mount)), nil | ||||||
| } | ||||||
|
Comment on lines
+122
to
+123
|
||||||
|
|
||||||
| // No versions specified: DELETE on the data path soft-deletes the latest version | ||||||
| logger.WithFields(log.Fields{ | ||||||
| "mount": mount, | ||||||
| "path": path, | ||||||
| }).Debug("Soft-deleting latest secret version") | ||||||
|
|
||||||
| dataPath := fmt.Sprintf("%s/data/%s", mount, strings.TrimPrefix(path, "/")) | ||||||
| _, err = vault.Logical().Delete(dataPath) | ||||||
| if err != nil { | ||||||
| logger.WithError(err).WithFields(log.Fields{ | ||||||
| "mount": mount, | ||||||
| "path": path, | ||||||
| "full_path": dataPath, | ||||||
| }).Error("Failed to soft-delete latest secret version") | ||||||
| return mcp.NewToolResultError(fmt.Sprintf("Failed to soft-delete latest secret version: %v", err)), nil | ||||||
| } | ||||||
|
|
||||||
| logger.WithFields(log.Fields{ | ||||||
| "mount": mount, | ||||||
| "path": path, | ||||||
| }).Info("Successfully soft-deleted latest secret version") | ||||||
|
|
||||||
| return mcp.NewToolResultText(fmt.Sprintf("Successfully soft-deleted the latest version of secret at path '%s' in mount '%s'. Use undelete_secret to recover it.", path, mount)), nil | ||||||
|
||||||
| return mcp.NewToolResultText(fmt.Sprintf("Successfully soft-deleted the latest version of secret at path '%s' in mount '%s'. Use undelete_secret to recover it.", path, mount)), nil | |
| return mcp.NewToolResultText(fmt.Sprintf("Successfully soft-deleted the latest version of secret at path '%s' in mount '%s'. Use undelete_secret_versions to recover it.", path, mount)), nil |
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 README section is now inaccurate after the KV tool changes in this PR:
write_secretno longer takeskey/valueand instead requires adataobject,read_secretgained an optionalversion(KV v2 only), and several new KV tools were added/renamed. Please update the “Available Tools” docs to match the current tool schemas/names.