-
Notifications
You must be signed in to change notification settings - Fork 22
feat(auth/identity): add self introspection tools for caller token and entity context #84
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,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 | ||||||
| } | ||||||
|
|
||||||
| 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, "", " ") | ||||||
|
||||||
| jsonResult, err := json.MarshalIndent(result, "", " ") | |
| jsonResult, err := json.Marshal(result) |
| 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
|
||||||||||||||||||||||||||
| 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
AI
Mar 9, 2026
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.
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.
| jsonResult, err := json.MarshalIndent(result, "", " ") | |
| jsonResult, err := json.Marshal(result) |
| 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
|
||||||||||||||||||||||||||
| 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
AI
Mar 9, 2026
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.
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.
| jsonResult, err := json.MarshalIndent(result, "", " ") | |
| jsonResult, err := json.Marshal(result) |
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.
These tool params are optional (only
namespacewith a default), but the handler hard-requiresreq.Params.Argumentsto be amap[string]interface{}. If the client omits theargumentsfield entirely, this will always return "Missing or invalid arguments format" and the tool can’t be called with defaults. Consider treating a nil/absentArgumentsas an empty map and only erroring whenArgumentsis present but of the wrong type.