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

194 lines
6.4 KiB
Go

package main
import (
"context"
"fmt"
"net/url"
"strconv"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
func registerRefTools(server *mcp.Server, api *giteaAPI) {
mcp.AddTool(server, &mcp.Tool{
Name: "list_branches",
Description: "List branches in a repository.",
}, listBranchesHandler(api))
mcp.AddTool(server, &mcp.Tool{
Name: "get_branch",
Description: "Get details for a single branch including its tip commit SHA.",
}, getBranchHandler(api))
mcp.AddTool(server, &mcp.Tool{
Name: "list_tags",
Description: "List git tags in a repository, newest first.",
}, listTagsHandler(api))
mcp.AddTool(server, &mcp.Tool{
Name: "get_tag",
Description: "Get details for a single tag.",
}, getTagHandler(api))
}
type branch struct {
Name string `json:"name"`
CommitSHA string `json:"commit_sha"`
CommitURL string `json:"commit_url,omitempty"`
Protected bool `json:"protected,omitempty"`
RequiredApprov int64 `json:"required_approvals,omitempty"`
}
type tag struct {
Name string `json:"name"`
CommitSHA string `json:"commit_sha"`
Message string `json:"message,omitempty"`
ZipballURL string `json:"zipball_url,omitempty"`
TarballURL string `json:"tarball_url,omitempty"`
}
type listBranchesInput struct {
Owner string `json:"owner"`
Repo string `json:"repo"`
Limit int `json:"limit,omitempty"`
}
type listBranchesOutput struct {
Branches []branch `json:"branches"`
}
func listBranchesHandler(api *giteaAPI) mcp.ToolHandlerFor[listBranchesInput, listBranchesOutput] {
return func(ctx context.Context, _ *mcp.CallToolRequest, in listBranchesInput) (*mcp.CallToolResult, listBranchesOutput, error) {
if in.Owner == "" || in.Repo == "" {
return nil, listBranchesOutput{}, fmt.Errorf("owner and repo 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/branches",
url.PathEscape(in.Owner), url.PathEscape(in.Repo))
var raw []struct {
Name string `json:"name"`
Commit struct{ ID, URL string } `json:"commit"`
Protected bool `json:"protected"`
RequiredApprovals int64 `json:"required_approvals"`
}
if err := api.get(ctx, path, q, &raw); err != nil {
return nil, listBranchesOutput{}, err
}
out := listBranchesOutput{Branches: make([]branch, 0, len(raw))}
for _, b := range raw {
out.Branches = append(out.Branches, branch{
Name: b.Name, CommitSHA: b.Commit.ID, CommitURL: b.Commit.URL,
Protected: b.Protected, RequiredApprov: b.RequiredApprovals,
})
}
return nil, out, nil
}
}
type getBranchInput struct {
Owner string `json:"owner"`
Repo string `json:"repo"`
Branch string `json:"branch"`
}
type getBranchOutput struct {
Branch branch `json:"branch"`
}
func getBranchHandler(api *giteaAPI) mcp.ToolHandlerFor[getBranchInput, getBranchOutput] {
return func(ctx context.Context, _ *mcp.CallToolRequest, in getBranchInput) (*mcp.CallToolResult, getBranchOutput, error) {
if in.Owner == "" || in.Repo == "" || in.Branch == "" {
return nil, getBranchOutput{}, fmt.Errorf("owner, repo, and branch are required")
}
path := fmt.Sprintf("/api/v1/repos/%s/%s/branches/%s",
url.PathEscape(in.Owner), url.PathEscape(in.Repo), url.PathEscape(in.Branch))
var raw struct {
Name string `json:"name"`
Commit struct{ ID, URL string } `json:"commit"`
Protected bool `json:"protected"`
RequiredApprovals int64 `json:"required_approvals"`
}
if err := api.get(ctx, path, nil, &raw); err != nil {
return nil, getBranchOutput{}, err
}
return nil, getBranchOutput{Branch: branch{
Name: raw.Name, CommitSHA: raw.Commit.ID, CommitURL: raw.Commit.URL,
Protected: raw.Protected, RequiredApprov: raw.RequiredApprovals,
}}, nil
}
}
type listTagsInput struct {
Owner string `json:"owner"`
Repo string `json:"repo"`
Limit int `json:"limit,omitempty"`
}
type listTagsOutput struct {
Tags []tag `json:"tags"`
}
func listTagsHandler(api *giteaAPI) mcp.ToolHandlerFor[listTagsInput, listTagsOutput] {
return func(ctx context.Context, _ *mcp.CallToolRequest, in listTagsInput) (*mcp.CallToolResult, listTagsOutput, error) {
if in.Owner == "" || in.Repo == "" {
return nil, listTagsOutput{}, fmt.Errorf("owner and repo 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/tags",
url.PathEscape(in.Owner), url.PathEscape(in.Repo))
var raw []struct {
Name string `json:"name"`
Message string `json:"message"`
ZipballURL string `json:"zipball_url"`
TarballURL string `json:"tarball_url"`
Commit struct {
SHA string `json:"sha"`
} `json:"commit"`
}
if err := api.get(ctx, path, q, &raw); err != nil {
return nil, listTagsOutput{}, err
}
out := listTagsOutput{Tags: make([]tag, 0, len(raw))}
for _, t := range raw {
out.Tags = append(out.Tags, tag{
Name: t.Name, CommitSHA: t.Commit.SHA, Message: t.Message,
ZipballURL: t.ZipballURL, TarballURL: t.TarballURL,
})
}
return nil, out, nil
}
}
type getTagInput struct {
Owner string `json:"owner"`
Repo string `json:"repo"`
Tag string `json:"tag"`
}
type getTagOutput struct {
Tag tag `json:"tag"`
}
func getTagHandler(api *giteaAPI) mcp.ToolHandlerFor[getTagInput, getTagOutput] {
return func(ctx context.Context, _ *mcp.CallToolRequest, in getTagInput) (*mcp.CallToolResult, getTagOutput, error) {
if in.Owner == "" || in.Repo == "" || in.Tag == "" {
return nil, getTagOutput{}, fmt.Errorf("owner, repo, and tag are required")
}
path := fmt.Sprintf("/api/v1/repos/%s/%s/tags/%s",
url.PathEscape(in.Owner), url.PathEscape(in.Repo), url.PathEscape(in.Tag))
var raw struct {
Name string `json:"name"`
Message string `json:"message"`
ZipballURL string `json:"zipball_url"`
TarballURL string `json:"tarball_url"`
Commit struct {
SHA string `json:"sha"`
} `json:"commit"`
}
if err := api.get(ctx, path, nil, &raw); err != nil {
return nil, getTagOutput{}, err
}
return nil, getTagOutput{Tag: tag{
Name: raw.Name, CommitSHA: raw.Commit.SHA, Message: raw.Message,
ZipballURL: raw.ZipballURL, TarballURL: raw.TarballURL,
}}, nil
}
}