Files
amacocian f5109abfad
CI / build (push) Successful in 19s
Gitea mcp pre-release
2026-05-25 15:22:30 +02:00

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
}
}