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

package auth

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"
)

// IntrospectSelf creates a tool that returns caller token details and self entity data together.
func IntrospectSelf(logger *log.Logger) server.ServerTool {
return server.ServerTool{
Tool: mcp.NewTool("introspect_self",
mcp.WithDescription("Introspect current Vault identity by combining auth/token/lookup-self and identity/entity/id/:entity_id (when present). Useful for policy/template-aware access analysis."),
mcp.WithToolAnnotation(
mcp.ToolAnnotation{
IdempotentHint: utils.ToBoolPtr(true),
ReadOnlyHint: utils.ToBoolPtr(true),
},
),
mcp.WithString("namespace",
mcp.DefaultString(""),
mcp.Description("Namespace path (for example 'admin/'). Defaults to current namespace.")),
),
Handler: func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
return introspectSelfHandler(ctx, req, logger)
},
}
}

func introspectSelfHandler(ctx context.Context, req mcp.CallToolRequest, logger *log.Logger) (*mcp.CallToolResult, error) {
args, ok := req.Params.Arguments.(map[string]interface{})
if !ok {
return mcp.NewToolResultError("Missing or invalid arguments format"), nil
Comment on lines +40 to +42

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.

These tool params are optional (only namespace with a default), but the handler hard-requires req.Params.Arguments to be a map[string]interface{}. If the client omits the arguments field entirely, this will always return "Missing or invalid arguments format" and the tool can’t be called with defaults. Consider treating a nil/absent Arguments as an empty map and only erroring when Arguments is present but of the wrong type.

Suggested change
args, ok := req.Params.Arguments.(map[string]interface{})
if !ok {
return mcp.NewToolResultError("Missing or invalid arguments format"), nil
var args map[string]interface{}
if req.Params.Arguments == nil {
args = map[string]interface{}{}
} else {
var ok bool
args, ok = req.Params.Arguments.(map[string]interface{})
if !ok {
return mcp.NewToolResultError("Missing or invalid arguments format"), nil
}

Copilot uses AI. Check for mistakes.
}

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

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

nsClient := vault
if namespace != "" {
nsClient = vault.WithNamespace(namespace)
}

lookupSecret, err := nsClient.Logical().Read("auth/token/lookup-self")
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("Failed to lookup self token: %v", err)), nil
}
if lookupSecret == nil || lookupSecret.Data == nil {
return mcp.NewToolResultError("No token lookup data returned"), nil
}

entityID, _ := lookupSecret.Data["entity_id"].(string)
result := map[string]interface{}{
"namespace": namespace,
"token": lookupSecret.Data,
"entity_id": entityID,
}

if entityID != "" {
entityPath := fmt.Sprintf("identity/entity/id/%s", entityID)
entitySecret, entityErr := nsClient.Logical().Read(entityPath)
if entityErr != nil {
result["entity_error"] = entityErr.Error()
} else if entitySecret == nil || entitySecret.Data == nil {
result["entity_error"] = "entity not found"
} else {
result["entity"] = entitySecret.Data
}
}

jsonResult, err := json.MarshalIndent(result, "", " ")

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.

Repository tools generally marshal JSON responses with json.Marshal(...) (compact). Using json.MarshalIndent(...) here makes this tool’s output inconsistent and increases payload size without adding structured typing. Consider switching to json.Marshal for consistency with other tools.

Suggested change
jsonResult, err := json.MarshalIndent(result, "", " ")
jsonResult, err := json.Marshal(result)

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

return mcp.NewToolResultText(string(jsonResult)), nil
}
76 changes: 76 additions & 0 deletions pkg/tools/auth/lookup_self.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright IBM Corp. 2025
// SPDX-License-Identifier: MPL-2.0

package auth

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"
)

// LookupSelf creates a tool for reading details about the caller token.
func LookupSelf(logger *log.Logger) server.ServerTool {
return server.ServerTool{
Tool: mcp.NewTool("lookup_self",
mcp.WithDescription("Look up details about the current Vault token (auth/token/lookup-self), including policies, entity_id, display_name, TTL, and metadata."),
mcp.WithToolAnnotation(
mcp.ToolAnnotation{
IdempotentHint: utils.ToBoolPtr(true),
ReadOnlyHint: utils.ToBoolPtr(true),
},
),
mcp.WithString("namespace",
mcp.DefaultString(""),
mcp.Description("Namespace path (for example 'admin/'). Defaults to current namespace.")),
),
Handler: func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
return lookupSelfHandler(ctx, req, logger)
},
}
}

func lookupSelfHandler(ctx context.Context, req mcp.CallToolRequest, logger *log.Logger) (*mcp.CallToolResult, error) {
args, ok := req.Params.Arguments.(map[string]interface{})
if !ok {
return mcp.NewToolResultError("Missing or invalid arguments format"), nil
Comment on lines +40 to +42

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.

These tool params are optional (only namespace with a default), but the handler hard-requires req.Params.Arguments to be a map[string]interface{}. If the client omits the arguments field entirely, this will always return "Missing or invalid arguments format" and the tool can’t be called with defaults. Consider treating a nil/absent Arguments as an empty map and only erroring when Arguments is present but of the wrong type.

Suggested change
args, ok := req.Params.Arguments.(map[string]interface{})
if !ok {
return mcp.NewToolResultError("Missing or invalid arguments format"), nil
var args map[string]interface{}
if req.Params.Arguments == nil {
args = map[string]interface{}{}
} else {
var ok bool
args, ok = req.Params.Arguments.(map[string]interface{})
if !ok {
return mcp.NewToolResultError("Missing or invalid arguments format"), nil
}

Copilot uses AI. Check for mistakes.
}

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

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

nsClient := vault
if namespace != "" {
nsClient = vault.WithNamespace(namespace)
}

secret, err := nsClient.Logical().Read("auth/token/lookup-self")
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("Failed to lookup self token: %v", err)), nil
}
if secret == nil || secret.Data == nil {
return mcp.NewToolResultError("No token lookup data returned"), nil
}

result := map[string]interface{}{
"namespace": namespace,
"data": secret.Data,
}

jsonResult, err := json.MarshalIndent(result, "", " ")

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.

Repository tools generally marshal JSON responses with json.Marshal(...) (compact). Using json.MarshalIndent(...) here makes this tool’s output inconsistent and increases payload size without adding structured typing. Consider switching to json.Marshal for consistency with other tools.

Suggested change
jsonResult, err := json.MarshalIndent(result, "", " ")
jsonResult, err := json.Marshal(result)

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

return mcp.NewToolResultText(string(jsonResult)), nil
}
91 changes: 91 additions & 0 deletions pkg/tools/identity/read_entity_self.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright IBM Corp. 2025
// SPDX-License-Identifier: MPL-2.0

package identity

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"
)

// ReadEntitySelf creates a tool for reading the caller's identity entity.
func ReadEntitySelf(logger *log.Logger) server.ServerTool {
return server.ServerTool{
Tool: mcp.NewTool("read_entity_self",
mcp.WithDescription("Read the Vault identity entity associated with the current token by resolving entity_id from auth/token/lookup-self. Includes entity metadata and aliases."),
mcp.WithToolAnnotation(
mcp.ToolAnnotation{
IdempotentHint: utils.ToBoolPtr(true),
ReadOnlyHint: utils.ToBoolPtr(true),
},
),
mcp.WithString("namespace",
mcp.DefaultString(""),
mcp.Description("Namespace path (for example 'admin/'). Defaults to current namespace.")),
),
Handler: func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
return readEntitySelfHandler(ctx, req, logger)
},
}
}

func readEntitySelfHandler(ctx context.Context, req mcp.CallToolRequest, logger *log.Logger) (*mcp.CallToolResult, error) {
args, ok := req.Params.Arguments.(map[string]interface{})
if !ok {
return mcp.NewToolResultError("Missing or invalid arguments format"), nil
Comment on lines +40 to +42

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.

These tool params are optional (only namespace with a default), but the handler hard-requires req.Params.Arguments to be a map[string]interface{}. If the client omits the arguments field entirely, this will always return "Missing or invalid arguments format" and the tool can’t be called with defaults. Consider treating a nil/absent Arguments as an empty map and only erroring when Arguments is present but of the wrong type.

Suggested change
args, ok := req.Params.Arguments.(map[string]interface{})
if !ok {
return mcp.NewToolResultError("Missing or invalid arguments format"), nil
var args map[string]interface{}
if req.Params.Arguments == nil {
args = map[string]interface{}{}
} else {
var ok bool
args, ok = req.Params.Arguments.(map[string]interface{})
if !ok {
return mcp.NewToolResultError("Missing or invalid arguments format"), nil
}

Copilot uses AI. Check for mistakes.
}

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

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

nsClient := vault
if namespace != "" {
nsClient = vault.WithNamespace(namespace)
}

lookupSecret, err := nsClient.Logical().Read("auth/token/lookup-self")
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("Failed to lookup self token: %v", err)), nil
}
if lookupSecret == nil || lookupSecret.Data == nil {
return mcp.NewToolResultError("No token lookup data returned"), nil
}

entityID, _ := lookupSecret.Data["entity_id"].(string)
if entityID == "" {
return mcp.NewToolResultError("Current token has no entity_id"), nil
}

entityPath := fmt.Sprintf("identity/entity/id/%s", entityID)
entitySecret, err := nsClient.Logical().Read(entityPath)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("Failed to read entity for self token: %v", err)), nil
}
if entitySecret == nil || entitySecret.Data == nil {
return mcp.NewToolResultError("Self entity not found"), nil
}

result := map[string]interface{}{
"namespace": namespace,
"entity_id": entityID,
"entity": entitySecret.Data,
}

jsonResult, err := json.MarshalIndent(result, "", " ")

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.

Repository tools generally marshal JSON responses with json.Marshal(...) (compact). Using json.MarshalIndent(...) here makes this tool’s output inconsistent and increases payload size without adding structured typing. Consider switching to json.Marshal for consistency with other tools.

Suggested change
jsonResult, err := json.MarshalIndent(result, "", " ")
jsonResult, err := json.Marshal(result)

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

return mcp.NewToolResultText(string(jsonResult)), nil
}
11 changes: 11 additions & 0 deletions pkg/tools/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
package tools

import (
"github.com/hashicorp/vault-mcp-server/pkg/tools/auth"
"github.com/hashicorp/vault-mcp-server/pkg/tools/identity"
"github.com/hashicorp/vault-mcp-server/pkg/tools/kv"
"github.com/hashicorp/vault-mcp-server/pkg/tools/pki"
"github.com/hashicorp/vault-mcp-server/pkg/tools/sys"
Expand All @@ -23,6 +25,15 @@ func InitTools(hcServer *server.MCPServer, logger *log.Logger) {
deleteMountTool := sys.DeleteMount(logger)
hcServer.AddTool(deleteMountTool.Tool, deleteMountTool.Handler)

lookupSelfTool := auth.LookupSelf(logger)
hcServer.AddTool(lookupSelfTool.Tool, lookupSelfTool.Handler)

introspectSelfTool := auth.IntrospectSelf(logger)
hcServer.AddTool(introspectSelfTool.Tool, introspectSelfTool.Handler)

readEntitySelfTool := identity.ReadEntitySelf(logger)
hcServer.AddTool(readEntitySelfTool.Tool, readEntitySelfTool.Handler)

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