273 lines
9.2 KiB
Go
273 lines
9.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/url"
|
|
"strconv"
|
|
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
)
|
|
|
|
func registerPullTools(server *mcp.Server, api *giteaAPI) {
|
|
mcp.AddTool(server, &mcp.Tool{
|
|
Name: "list_pulls",
|
|
Description: "List pull requests for a repository. Filters: state (open|closed|all), sort, milestone, labels.",
|
|
}, listPullsHandler(api))
|
|
|
|
mcp.AddTool(server, &mcp.Tool{
|
|
Name: "get_pull",
|
|
Description: "Get full details of a pull request by index. Includes head/base refs, mergeable status, and requested reviewers.",
|
|
}, getPullHandler(api))
|
|
|
|
mcp.AddTool(server, &mcp.Tool{
|
|
Name: "list_pull_files",
|
|
Description: "List the files changed by a pull request with per-file additions/deletions counts (no diffs).",
|
|
}, listPullFilesHandler(api))
|
|
|
|
mcp.AddTool(server, &mcp.Tool{
|
|
Name: "list_pull_reviews",
|
|
Description: "List reviews on a pull request (state: APPROVED | CHANGES_REQUESTED | COMMENTED | …).",
|
|
}, listPullReviewsHandler(api))
|
|
}
|
|
|
|
type pullRef struct {
|
|
Label string `json:"label"`
|
|
Ref string `json:"ref"`
|
|
SHA string `json:"sha"`
|
|
}
|
|
type pullSummary struct {
|
|
ID int64 `json:"id"`
|
|
Index int64 `json:"index"`
|
|
Title string `json:"title"`
|
|
State string `json:"state"`
|
|
Draft bool `json:"draft,omitempty"`
|
|
Merged bool `json:"merged,omitempty"`
|
|
Mergeable bool `json:"mergeable,omitempty"`
|
|
Author string `json:"author,omitempty"`
|
|
Labels []string `json:"labels,omitempty"`
|
|
Reviewers []string `json:"requested_reviewers,omitempty"`
|
|
Head pullRef `json:"head"`
|
|
Base pullRef `json:"base"`
|
|
CreatedAt string `json:"created_at,omitempty"`
|
|
UpdatedAt string `json:"updated_at,omitempty"`
|
|
MergedAt string `json:"merged_at,omitempty"`
|
|
URL string `json:"url,omitempty"`
|
|
}
|
|
type pullFull struct {
|
|
pullSummary
|
|
Body string `json:"body,omitempty"`
|
|
Additions int64 `json:"additions,omitempty"`
|
|
Deletions int64 `json:"deletions,omitempty"`
|
|
Changed int64 `json:"changed_files,omitempty"`
|
|
}
|
|
type pullFile struct {
|
|
Filename string `json:"filename"`
|
|
Status string `json:"status"`
|
|
Additions int64 `json:"additions"`
|
|
Deletions int64 `json:"deletions"`
|
|
Changes int64 `json:"changes"`
|
|
}
|
|
type pullReview struct {
|
|
ID int64 `json:"id"`
|
|
State string `json:"state"`
|
|
Body string `json:"body,omitempty"`
|
|
Reviewer string `json:"reviewer,omitempty"`
|
|
SubmittedAt string `json:"submitted_at,omitempty"`
|
|
CommitID string `json:"commit_id,omitempty"`
|
|
}
|
|
|
|
type rawPull struct {
|
|
ID int64 `json:"id"`
|
|
Number int64 `json:"number"`
|
|
Title string `json:"title"`
|
|
Body string `json:"body"`
|
|
State string `json:"state"`
|
|
Draft bool `json:"draft"`
|
|
Merged bool `json:"merged"`
|
|
Mergeable bool `json:"mergeable"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
MergedAt string `json:"merged_at"`
|
|
HTMLURL string `json:"html_url"`
|
|
Additions int64 `json:"additions"`
|
|
Deletions int64 `json:"deletions"`
|
|
Changed int64 `json:"changed_files"`
|
|
User struct {
|
|
Login string `json:"login"`
|
|
} `json:"user"`
|
|
Labels []struct {
|
|
Name string `json:"name"`
|
|
} `json:"labels"`
|
|
RequestedReviewers []struct {
|
|
Login string `json:"login"`
|
|
} `json:"requested_reviewers"`
|
|
Head struct {
|
|
Label string `json:"label"`
|
|
Ref string `json:"ref"`
|
|
SHA string `json:"sha"`
|
|
} `json:"head"`
|
|
Base struct {
|
|
Label string `json:"label"`
|
|
Ref string `json:"ref"`
|
|
SHA string `json:"sha"`
|
|
} `json:"base"`
|
|
}
|
|
|
|
func summarizePull(r rawPull) pullSummary {
|
|
labels := make([]string, 0, len(r.Labels))
|
|
for _, l := range r.Labels {
|
|
labels = append(labels, l.Name)
|
|
}
|
|
reviewers := make([]string, 0, len(r.RequestedReviewers))
|
|
for _, u := range r.RequestedReviewers {
|
|
reviewers = append(reviewers, u.Login)
|
|
}
|
|
return pullSummary{
|
|
ID: r.ID, Index: r.Number, Title: r.Title, State: r.State,
|
|
Draft: r.Draft, Merged: r.Merged, Mergeable: r.Mergeable,
|
|
Author: r.User.Login, Labels: labels, Reviewers: reviewers,
|
|
Head: pullRef{Label: r.Head.Label, Ref: r.Head.Ref, SHA: r.Head.SHA},
|
|
Base: pullRef{Label: r.Base.Label, Ref: r.Base.Ref, SHA: r.Base.SHA},
|
|
CreatedAt: r.CreatedAt, UpdatedAt: r.UpdatedAt, MergedAt: r.MergedAt, URL: r.HTMLURL,
|
|
}
|
|
}
|
|
|
|
type listPullsInput struct {
|
|
Owner string `json:"owner"`
|
|
Repo string `json:"repo"`
|
|
State string `json:"state,omitempty" jsonschema:"open | closed | all (default: open)"`
|
|
Sort string `json:"sort,omitempty" jsonschema:"oldest | recentupdate | leastupdate | mostcomment | leastcomment | priority"`
|
|
Labels string `json:"labels,omitempty" jsonschema:"comma-separated label names"`
|
|
Milestone int64 `json:"milestone,omitempty"`
|
|
Limit int `json:"limit,omitempty"`
|
|
}
|
|
type listPullsOutput struct {
|
|
Pulls []pullSummary `json:"pulls"`
|
|
}
|
|
|
|
func listPullsHandler(api *giteaAPI) mcp.ToolHandlerFor[listPullsInput, listPullsOutput] {
|
|
return func(ctx context.Context, _ *mcp.CallToolRequest, in listPullsInput) (*mcp.CallToolResult, listPullsOutput, error) {
|
|
if in.Owner == "" || in.Repo == "" {
|
|
return nil, listPullsOutput{}, fmt.Errorf("owner and repo are required")
|
|
}
|
|
limit := clampInt(in.Limit, 1, 50, 20)
|
|
q := url.Values{"limit": []string{strconv.Itoa(limit)}}
|
|
setIf(q, "state", in.State)
|
|
setIf(q, "sort", in.Sort)
|
|
setIf(q, "labels", in.Labels)
|
|
if in.Milestone > 0 {
|
|
q.Set("milestone", strconv.FormatInt(in.Milestone, 10))
|
|
}
|
|
path := fmt.Sprintf("/api/v1/repos/%s/%s/pulls",
|
|
url.PathEscape(in.Owner), url.PathEscape(in.Repo))
|
|
var raw []rawPull
|
|
if err := api.get(ctx, path, q, &raw); err != nil {
|
|
return nil, listPullsOutput{}, err
|
|
}
|
|
out := listPullsOutput{Pulls: make([]pullSummary, 0, len(raw))}
|
|
for _, r := range raw {
|
|
out.Pulls = append(out.Pulls, summarizePull(r))
|
|
}
|
|
return nil, out, nil
|
|
}
|
|
}
|
|
|
|
type getPullInput struct {
|
|
Owner string `json:"owner"`
|
|
Repo string `json:"repo"`
|
|
Index int64 `json:"index"`
|
|
}
|
|
type getPullOutput struct {
|
|
Pull pullFull `json:"pull"`
|
|
}
|
|
|
|
func getPullHandler(api *giteaAPI) mcp.ToolHandlerFor[getPullInput, getPullOutput] {
|
|
return func(ctx context.Context, _ *mcp.CallToolRequest, in getPullInput) (*mcp.CallToolResult, getPullOutput, error) {
|
|
if in.Owner == "" || in.Repo == "" || in.Index == 0 {
|
|
return nil, getPullOutput{}, fmt.Errorf("owner, repo, and index are required")
|
|
}
|
|
path := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d",
|
|
url.PathEscape(in.Owner), url.PathEscape(in.Repo), in.Index)
|
|
var r rawPull
|
|
if err := api.get(ctx, path, nil, &r); err != nil {
|
|
return nil, getPullOutput{}, err
|
|
}
|
|
summary := summarizePull(r)
|
|
return nil, getPullOutput{Pull: pullFull{
|
|
pullSummary: summary, Body: r.Body,
|
|
Additions: r.Additions, Deletions: r.Deletions, Changed: r.Changed,
|
|
}}, nil
|
|
}
|
|
}
|
|
|
|
type listPullFilesInput struct {
|
|
Owner string `json:"owner"`
|
|
Repo string `json:"repo"`
|
|
Index int64 `json:"index"`
|
|
Limit int `json:"limit,omitempty"`
|
|
}
|
|
type listPullFilesOutput struct {
|
|
Files []pullFile `json:"files"`
|
|
}
|
|
|
|
func listPullFilesHandler(api *giteaAPI) mcp.ToolHandlerFor[listPullFilesInput, listPullFilesOutput] {
|
|
return func(ctx context.Context, _ *mcp.CallToolRequest, in listPullFilesInput) (*mcp.CallToolResult, listPullFilesOutput, error) {
|
|
if in.Owner == "" || in.Repo == "" || in.Index == 0 {
|
|
return nil, listPullFilesOutput{}, fmt.Errorf("owner, repo, and index are required")
|
|
}
|
|
limit := clampInt(in.Limit, 1, 50, 50)
|
|
q := url.Values{"limit": []string{strconv.Itoa(limit)}}
|
|
path := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/files",
|
|
url.PathEscape(in.Owner), url.PathEscape(in.Repo), in.Index)
|
|
var raw []pullFile
|
|
if err := api.get(ctx, path, q, &raw); err != nil {
|
|
return nil, listPullFilesOutput{}, err
|
|
}
|
|
return nil, listPullFilesOutput{Files: raw}, nil
|
|
}
|
|
}
|
|
|
|
type listPullReviewsInput struct {
|
|
Owner string `json:"owner"`
|
|
Repo string `json:"repo"`
|
|
Index int64 `json:"index"`
|
|
Limit int `json:"limit,omitempty"`
|
|
}
|
|
type listPullReviewsOutput struct {
|
|
Reviews []pullReview `json:"reviews"`
|
|
}
|
|
|
|
func listPullReviewsHandler(api *giteaAPI) mcp.ToolHandlerFor[listPullReviewsInput, listPullReviewsOutput] {
|
|
return func(ctx context.Context, _ *mcp.CallToolRequest, in listPullReviewsInput) (*mcp.CallToolResult, listPullReviewsOutput, error) {
|
|
if in.Owner == "" || in.Repo == "" || in.Index == 0 {
|
|
return nil, listPullReviewsOutput{}, fmt.Errorf("owner, repo, and index are required")
|
|
}
|
|
limit := clampInt(in.Limit, 1, 50, 50)
|
|
q := url.Values{"limit": []string{strconv.Itoa(limit)}}
|
|
path := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews",
|
|
url.PathEscape(in.Owner), url.PathEscape(in.Repo), in.Index)
|
|
var raw []struct {
|
|
ID int64 `json:"id"`
|
|
State string `json:"state"`
|
|
Body string `json:"body"`
|
|
SubmittedAt string `json:"submitted_at"`
|
|
CommitID string `json:"commit_id"`
|
|
User struct {
|
|
Login string `json:"login"`
|
|
} `json:"user"`
|
|
}
|
|
if err := api.get(ctx, path, q, &raw); err != nil {
|
|
return nil, listPullReviewsOutput{}, err
|
|
}
|
|
out := listPullReviewsOutput{Reviews: make([]pullReview, 0, len(raw))}
|
|
for _, r := range raw {
|
|
out.Reviews = append(out.Reviews, pullReview{
|
|
ID: r.ID, State: r.State, Body: r.Body,
|
|
Reviewer: r.User.Login, SubmittedAt: r.SubmittedAt, CommitID: r.CommitID,
|
|
})
|
|
}
|
|
return nil, out, nil
|
|
}
|
|
}
|