Add MCP entry password tool
ci / lint-test (pull_request) Successful in 1m46s
ci / build (pull_request) Successful in 2m40s

This commit is contained in:
Joe Julian
2026-05-14 09:06:59 -07:00
parent d9a4bc6b14
commit 13eeb3fe4a
2 changed files with 126 additions and 1 deletions
+80
View File
@@ -55,6 +55,11 @@ func New(client vaultClient, cfg Config) *mcp.Server {
Title: "Find Browser Logins",
Description: "Find KeePassGO browser-login matches for a page URL without returning passwords.",
}, handlers.findBrowserLogins)
mcp.AddTool(server, &mcp.Tool{
Name: "get_entry_password",
Title: "Get Entry Password",
Description: "Return the password for one uniquely matched KeePassGO entry using path, query, and optional metadata filters.",
}, handlers.getEntryPassword)
mcp.AddTool(server, &mcp.Tool{
Name: "get_browser_credential",
Title: "Get Browser Credential",
@@ -198,6 +203,46 @@ func (h *handlers) getBrowserCredential(ctx context.Context, _ *mcp.CallToolRequ
}, nil
}
type getEntryPasswordInput struct {
Path []string `json:"path,omitempty" jsonschema:"optional KeePassGO group path to search within"`
Query string `json:"query,omitempty" jsonschema:"text query used to find candidate entries"`
Title string `json:"title,omitempty" jsonschema:"optional exact entry title filter"`
Username string `json:"username,omitempty" jsonschema:"optional exact entry username filter"`
URL string `json:"url,omitempty" jsonschema:"optional exact entry URL filter"`
}
type getEntryPasswordOutput struct {
ID string `json:"id,omitempty" jsonschema:"entry id, when present"`
Title string `json:"title" jsonschema:"entry title"`
Username string `json:"username,omitempty" jsonschema:"entry username"`
URL string `json:"url,omitempty" jsonschema:"entry URL"`
Path []string `json:"path,omitempty" jsonschema:"entry group path"`
Password string `json:"password" jsonschema:"entry password"`
}
func (h *handlers) getEntryPassword(ctx context.Context, _ *mcp.CallToolRequest, input getEntryPasswordInput) (*mcp.CallToolResult, getEntryPasswordOutput, error) {
entries, err := h.client.ListEntries(ctx, cleanPath(input.Path), strings.TrimSpace(input.Query))
if err != nil {
return nil, getEntryPasswordOutput{}, fmt.Errorf("find KeePassGO entry password candidates: %w", err)
}
entry, err := uniquePasswordEntry(entries, input)
if err != nil {
return nil, getEntryPasswordOutput{}, err
}
password := entry.GetPassword()
if password == "" {
return nil, getEntryPasswordOutput{}, fmt.Errorf("matched KeePassGO entry %q has an empty password", entry.GetTitle())
}
return nil, getEntryPasswordOutput{
ID: entry.GetId(),
Title: entry.GetTitle(),
Username: entry.GetUsername(),
URL: entry.GetUrl(),
Path: append([]string(nil), entry.GetPath()...),
Password: password,
}, nil
}
func summarizeEntry(entry *keepassgov1.Entry) entrySummary {
if entry == nil {
return entrySummary{}
@@ -220,6 +265,41 @@ func summarizeEntry(entry *keepassgov1.Entry) entrySummary {
}
}
func uniquePasswordEntry(entries []*keepassgov1.Entry, input getEntryPasswordInput) (*keepassgov1.Entry, error) {
var matches []*keepassgov1.Entry
for _, entry := range entries {
if entry == nil {
continue
}
if !optionalEqual(input.Title, entry.GetTitle()) {
continue
}
if !optionalEqual(input.Username, entry.GetUsername()) {
continue
}
if !optionalEqual(input.URL, entry.GetUrl()) {
continue
}
matches = append(matches, entry)
}
switch len(matches) {
case 0:
return nil, fmt.Errorf("no KeePassGO entry matched the password request")
case 1:
return matches[0], nil
default:
return nil, fmt.Errorf("password request matched %d KeePassGO entries; add title, username, URL, path, or query filters", len(matches))
}
}
func optionalEqual(want, got string) bool {
want = strings.TrimSpace(want)
if want == "" {
return true
}
return strings.EqualFold(want, strings.TrimSpace(got))
}
func cleanPath(path []string) []string {
out := make([]string, 0, len(path))
for _, part := range path {