Skip to content

Commit d98a103

Browse files
hTrapclaude
andcommitted
fix: resolve usernames to bech32 addresses in assignees
create_pull_request and update_issue (add/remove assignees) now accept usernames (e.g. "htrap") and resolve them to bech32 addresses via the chain's User query. Addresses starting with "gitopia1" are passed through. Fixes Gitopia/gitopia-mcp-server#3 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 262370c commit d98a103

2 files changed

Lines changed: 57 additions & 3 deletions

File tree

internal/gitopia/client.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,25 @@ func (c *Client) GetUserByAddress(ctx context.Context, address string) (*User, e
170170
}, nil
171171
}
172172

173+
// GetUserByUsername queries a user by username and returns their address.
174+
func (c *Client) GetUserByUsername(ctx context.Context, username string) (*User, error) {
175+
q := gitopiatypes.NewQueryClient(c.conn)
176+
resp, err := q.User(ctx, &gitopiatypes.QueryGetUserRequest{
177+
Id: username,
178+
})
179+
if err != nil {
180+
return nil, fmt.Errorf("failed to query user '%s': %w", username, err)
181+
}
182+
if resp.User == nil {
183+
return nil, fmt.Errorf("user not found: %s", username)
184+
}
185+
return &User{
186+
ID: strconv.FormatUint(resp.User.Id, 10),
187+
Username: resp.User.Username,
188+
Address: resp.User.Creator,
189+
}, nil
190+
}
191+
173192
// GetUserDAOs queries DAOs that a user is a member of
174193
func (c *Client) GetUserDAOs(ctx context.Context, username string) ([]DAO, error) {
175194
q := gitopiatypes.NewQueryClient(c.conn)

internal/handler/gitopia_handlers.go

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,25 @@ import (
1313
"github.com/modelcontextprotocol/go-sdk/mcp"
1414
)
1515

16+
// resolveAssignees converts a list of usernames or addresses to bech32 addresses.
17+
// If a value is already a bech32 address (starts with "gitopia1"), it's kept as-is.
18+
// Otherwise it's treated as a username and resolved via the chain.
19+
func (h *ToolHandler) resolveAssignees(ctx context.Context, assignees []string) ([]string, error) {
20+
resolved := make([]string, 0, len(assignees))
21+
for _, a := range assignees {
22+
if strings.HasPrefix(a, "gitopia1") {
23+
resolved = append(resolved, a)
24+
continue
25+
}
26+
user, err := h.GClient.GetUserByUsername(ctx, a)
27+
if err != nil {
28+
return nil, fmt.Errorf("failed to resolve assignee '%s': %s", a, err)
29+
}
30+
resolved = append(resolved, user.Address)
31+
}
32+
return resolved, nil
33+
}
34+
1635
// ---- Gitopia API Handlers ----
1736

1837
type ListReposParams struct {
@@ -366,12 +385,20 @@ func (h *ToolHandler) UpdateIssue(
366385
}
367386

368387
if len(p.AddAssignees) > 0 {
369-
msgs = append(msgs, gitopiatypes.NewMsgAddIssueAssignees(w.Address(), repo.Id, p.IssueIid, p.AddAssignees))
388+
resolved, resolveErr := h.resolveAssignees(ctx, p.AddAssignees)
389+
if resolveErr != nil {
390+
return toolErrorf(ErrValidation, "%s", resolveErr)
391+
}
392+
msgs = append(msgs, gitopiatypes.NewMsgAddIssueAssignees(w.Address(), repo.Id, p.IssueIid, resolved))
370393
actions = append(actions, fmt.Sprintf("added %d assignees", len(p.AddAssignees)))
371394
}
372395

373396
if len(p.RemoveAssignees) > 0 {
374-
msgs = append(msgs, gitopiatypes.NewMsgRemoveIssueAssignees(w.Address(), repo.Id, p.IssueIid, p.RemoveAssignees))
397+
resolved, resolveErr := h.resolveAssignees(ctx, p.RemoveAssignees)
398+
if resolveErr != nil {
399+
return toolErrorf(ErrValidation, "%s", resolveErr)
400+
}
401+
msgs = append(msgs, gitopiatypes.NewMsgRemoveIssueAssignees(w.Address(), repo.Id, p.IssueIid, resolved))
375402
actions = append(actions, fmt.Sprintf("removed %d assignees", len(p.RemoveAssignees)))
376403
}
377404

@@ -651,9 +678,17 @@ func (h *ToolHandler) CreatePullRequest(
651678
if err != nil {
652679
return toolErrorf(ErrAuthFailed, "Authentication failed: %s", err)
653680
}
681+
// Resolve usernames to bech32 addresses if needed
682+
assignees := p.Assignees
683+
if len(assignees) > 0 {
684+
assignees, err = h.resolveAssignees(ctx, assignees)
685+
if err != nil {
686+
return toolErrorf(ErrValidation, "%s", err)
687+
}
688+
}
654689
start := time.Now()
655690
prNumber, err := retryOnSequenceMismatch(func() (int, error) {
656-
return h.GClient.CreatePullRequest(ctx, w, p.Owner, p.Name, p.Title, p.Description, p.HeadBranch, p.BaseBranch, p.Assignees, p.Labels, p.IssueIids)
691+
return h.GClient.CreatePullRequest(ctx, w, p.Owner, p.Name, p.Title, p.Description, p.HeadBranch, p.BaseBranch, assignees, p.Labels, p.IssueIids)
657692
})
658693
AuditLog("create_pull_request", w.Address(), err == nil, time.Since(start), "")
659694
if err != nil {

0 commit comments

Comments
 (0)