192 lines
6.5 KiB
Go
192 lines
6.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/url"
|
|
"strconv"
|
|
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
)
|
|
|
|
func registerCommitTools(server *mcp.Server, api *giteaAPI) {
|
|
mcp.AddTool(server, &mcp.Tool{
|
|
Name: "list_commits",
|
|
Description: "List commits in a repository's history. Optional sha/branch starting point, since/until date filters (ISO 8601).",
|
|
}, listCommitsHandler(api))
|
|
|
|
mcp.AddTool(server, &mcp.Tool{
|
|
Name: "get_commit",
|
|
Description: "Get a single commit by SHA, including the full message and parent SHAs.",
|
|
}, getCommitHandler(api))
|
|
|
|
mcp.AddTool(server, &mcp.Tool{
|
|
Name: "get_commit_status",
|
|
Description: "Get the combined CI / status check state for a commit or branch ref.",
|
|
}, getCommitStatusHandler(api))
|
|
}
|
|
|
|
type commitSummary struct {
|
|
SHA string `json:"sha"`
|
|
Message string `json:"message"`
|
|
Author string `json:"author,omitempty"`
|
|
Date string `json:"date,omitempty"`
|
|
URL string `json:"url,omitempty"`
|
|
Parents []string `json:"parents,omitempty"`
|
|
}
|
|
|
|
type listCommitsInput struct {
|
|
Owner string `json:"owner"`
|
|
Repo string `json:"repo"`
|
|
SHA string `json:"sha,omitempty" jsonschema:"branch / tag / commit SHA to start from (default: repo default branch)"`
|
|
Since string `json:"since,omitempty" jsonschema:"only commits on or after this ISO 8601 timestamp"`
|
|
Until string `json:"until,omitempty" jsonschema:"only commits on or before this ISO 8601 timestamp"`
|
|
Limit int `json:"limit,omitempty"`
|
|
}
|
|
type listCommitsOutput struct {
|
|
Commits []commitSummary `json:"commits"`
|
|
}
|
|
|
|
func listCommitsHandler(api *giteaAPI) mcp.ToolHandlerFor[listCommitsInput, listCommitsOutput] {
|
|
return func(ctx context.Context, _ *mcp.CallToolRequest, in listCommitsInput) (*mcp.CallToolResult, listCommitsOutput, error) {
|
|
if in.Owner == "" || in.Repo == "" {
|
|
return nil, listCommitsOutput{}, fmt.Errorf("owner and repo are required")
|
|
}
|
|
limit := clampInt(in.Limit, 1, 50, 20)
|
|
q := url.Values{
|
|
"limit": []string{strconv.Itoa(limit)},
|
|
"stat": []string{"false"},
|
|
"verification": []string{"false"},
|
|
"files": []string{"false"},
|
|
}
|
|
setIf(q, "sha", in.SHA)
|
|
setIf(q, "since", in.Since)
|
|
setIf(q, "until", in.Until)
|
|
path := fmt.Sprintf("/api/v1/repos/%s/%s/commits",
|
|
url.PathEscape(in.Owner), url.PathEscape(in.Repo))
|
|
var raw []struct {
|
|
SHA string `json:"sha"`
|
|
HTMLURL string `json:"html_url"`
|
|
RepoCommit struct {
|
|
Message string `json:"message"`
|
|
Author struct {
|
|
Name string `json:"name"`
|
|
Date string `json:"date"`
|
|
} `json:"author"`
|
|
} `json:"commit"`
|
|
Parents []struct {
|
|
SHA string `json:"sha"`
|
|
} `json:"parents"`
|
|
}
|
|
if err := api.get(ctx, path, q, &raw); err != nil {
|
|
return nil, listCommitsOutput{}, err
|
|
}
|
|
out := listCommitsOutput{Commits: make([]commitSummary, 0, len(raw))}
|
|
for _, c := range raw {
|
|
parents := make([]string, 0, len(c.Parents))
|
|
for _, p := range c.Parents {
|
|
parents = append(parents, p.SHA)
|
|
}
|
|
out.Commits = append(out.Commits, commitSummary{
|
|
SHA: c.SHA, Message: firstLine(c.RepoCommit.Message),
|
|
Author: c.RepoCommit.Author.Name, Date: c.RepoCommit.Author.Date,
|
|
URL: c.HTMLURL, Parents: parents,
|
|
})
|
|
}
|
|
return nil, out, nil
|
|
}
|
|
}
|
|
|
|
type getCommitInput struct {
|
|
Owner string `json:"owner"`
|
|
Repo string `json:"repo"`
|
|
SHA string `json:"sha"`
|
|
}
|
|
type getCommitOutput struct {
|
|
SHA string `json:"sha"`
|
|
Message string `json:"message"`
|
|
Author string `json:"author,omitempty"`
|
|
Email string `json:"email,omitempty"`
|
|
Date string `json:"date,omitempty"`
|
|
URL string `json:"url,omitempty"`
|
|
Parents []string `json:"parents,omitempty"`
|
|
TreeSHA string `json:"tree_sha,omitempty"`
|
|
}
|
|
|
|
func getCommitHandler(api *giteaAPI) mcp.ToolHandlerFor[getCommitInput, getCommitOutput] {
|
|
return func(ctx context.Context, _ *mcp.CallToolRequest, in getCommitInput) (*mcp.CallToolResult, getCommitOutput, error) {
|
|
if in.Owner == "" || in.Repo == "" || in.SHA == "" {
|
|
return nil, getCommitOutput{}, fmt.Errorf("owner, repo, and sha are required")
|
|
}
|
|
path := fmt.Sprintf("/api/v1/repos/%s/%s/git/commits/%s",
|
|
url.PathEscape(in.Owner), url.PathEscape(in.Repo), url.PathEscape(in.SHA))
|
|
var raw struct {
|
|
SHA string `json:"sha"`
|
|
HTMLURL string `json:"html_url"`
|
|
Message string `json:"message"`
|
|
Author struct {
|
|
Name string `json:"name"`
|
|
Email string `json:"email"`
|
|
Date string `json:"date"`
|
|
} `json:"author"`
|
|
Tree struct {
|
|
SHA string `json:"sha"`
|
|
} `json:"tree"`
|
|
Parents []struct {
|
|
SHA string `json:"sha"`
|
|
} `json:"parents"`
|
|
}
|
|
if err := api.get(ctx, path, nil, &raw); err != nil {
|
|
return nil, getCommitOutput{}, err
|
|
}
|
|
parents := make([]string, 0, len(raw.Parents))
|
|
for _, p := range raw.Parents {
|
|
parents = append(parents, p.SHA)
|
|
}
|
|
return nil, getCommitOutput{
|
|
SHA: raw.SHA, Message: raw.Message,
|
|
Author: raw.Author.Name, Email: raw.Author.Email, Date: raw.Author.Date,
|
|
URL: raw.HTMLURL, Parents: parents, TreeSHA: raw.Tree.SHA,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
type getCommitStatusInput struct {
|
|
Owner string `json:"owner"`
|
|
Repo string `json:"repo"`
|
|
Ref string `json:"ref" jsonschema:"commit SHA or branch/tag name"`
|
|
}
|
|
type commitStatus struct {
|
|
Context string `json:"context"`
|
|
State string `json:"state"`
|
|
Description string `json:"description,omitempty"`
|
|
TargetURL string `json:"target_url,omitempty"`
|
|
CreatedAt string `json:"created_at,omitempty"`
|
|
}
|
|
type getCommitStatusOutput struct {
|
|
State string `json:"state" jsonschema:"combined: success | pending | failure | error | warning"`
|
|
TotalCount int `json:"total_count"`
|
|
Statuses []commitStatus `json:"statuses"`
|
|
}
|
|
|
|
func getCommitStatusHandler(api *giteaAPI) mcp.ToolHandlerFor[getCommitStatusInput, getCommitStatusOutput] {
|
|
return func(ctx context.Context, _ *mcp.CallToolRequest, in getCommitStatusInput) (*mcp.CallToolResult, getCommitStatusOutput, error) {
|
|
if in.Owner == "" || in.Repo == "" || in.Ref == "" {
|
|
return nil, getCommitStatusOutput{}, fmt.Errorf("owner, repo, and ref are required")
|
|
}
|
|
path := fmt.Sprintf("/api/v1/repos/%s/%s/commits/%s/status",
|
|
url.PathEscape(in.Owner), url.PathEscape(in.Repo), url.PathEscape(in.Ref))
|
|
var resp struct {
|
|
State string `json:"state"`
|
|
TotalCount int `json:"total_count"`
|
|
Statuses []commitStatus `json:"statuses"`
|
|
}
|
|
if err := api.get(ctx, path, nil, &resp); err != nil {
|
|
return nil, getCommitStatusOutput{}, err
|
|
}
|
|
return nil, getCommitStatusOutput{
|
|
State: resp.State, TotalCount: resp.TotalCount, Statuses: resp.Statuses,
|
|
}, nil
|
|
}
|
|
}
|