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

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