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

145 lines
4.9 KiB
Go

package main
import (
"context"
"encoding/base64"
"fmt"
"net/url"
"strconv"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
func registerContentTools(server *mcp.Server, api *giteaAPI) {
mcp.AddTool(server, &mcp.Tool{
Name: "get_file",
Description: "Read the contents of a file in a Gitea repository at a given path and optional ref (branch/tag/commit). Capped at 256 KiB; over that, only the first 256 KiB is returned and truncated=true.",
}, getFileHandler(api))
mcp.AddTool(server, &mcp.Tool{
Name: "file_history",
Description: "List commits that touched a specific file path in a repository, newest first.",
}, fileHistoryHandler(api))
}
const maxFileBytes = 256 * 1024
type getFileInput struct {
Owner string `json:"owner" jsonschema:"repository owner (user or org)"`
Repo string `json:"repo" jsonschema:"repository name"`
Path string `json:"path" jsonschema:"path of the file inside the repository"`
Ref string `json:"ref,omitempty" jsonschema:"optional branch / tag / commit SHA (default: repository default branch)"`
}
type getFileOutput struct {
Path string `json:"path"`
Ref string `json:"ref,omitempty"`
SHA string `json:"sha,omitempty"`
Size int64 `json:"size"`
Encoding string `json:"encoding,omitempty"`
Content string `json:"content"`
Truncated bool `json:"truncated,omitempty"`
IsDirectory bool `json:"is_directory,omitempty"`
}
func getFileHandler(api *giteaAPI) mcp.ToolHandlerFor[getFileInput, getFileOutput] {
return func(ctx context.Context, _ *mcp.CallToolRequest, in getFileInput) (*mcp.CallToolResult, getFileOutput, error) {
if in.Owner == "" || in.Repo == "" || in.Path == "" {
return nil, getFileOutput{}, fmt.Errorf("owner, repo, and path are required")
}
q := url.Values{}
setIf(q, "ref", in.Ref)
path := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s",
url.PathEscape(in.Owner), url.PathEscape(in.Repo), escapePathSegments(in.Path))
var resp struct {
Type string `json:"type"`
Encoding string `json:"encoding"`
Size int64 `json:"size"`
Path string `json:"path"`
SHA string `json:"sha"`
Content string `json:"content"`
}
if err := api.get(ctx, path, q, &resp); err != nil {
return nil, getFileOutput{}, err
}
out := getFileOutput{
Path: resp.Path, Ref: in.Ref, SHA: resp.SHA, Size: resp.Size, Encoding: resp.Encoding,
}
if resp.Type != "file" {
out.IsDirectory = true
return nil, out, nil
}
decoded, err := base64.StdEncoding.DecodeString(resp.Content)
if err != nil {
out.Content = resp.Content
return nil, out, nil
}
if int64(len(decoded)) > maxFileBytes {
decoded = decoded[:maxFileBytes]
out.Truncated = true
}
out.Encoding = "utf-8"
out.Content = string(decoded)
return nil, out, nil
}
}
type fileHistoryInput struct {
Owner string `json:"owner"`
Repo string `json:"repo"`
Path string `json:"path" jsonschema:"file path within the repository"`
Ref string `json:"ref,omitempty" jsonschema:"branch / tag / commit to start listing from (default: repo default branch)"`
Limit int `json:"limit,omitempty" jsonschema:"max commits to return (1-50, default 20)"`
}
type historyCommit struct {
SHA string `json:"sha"`
Message string `json:"message"`
Author string `json:"author,omitempty"`
Date string `json:"date,omitempty"`
URL string `json:"url,omitempty"`
}
type fileHistoryOutput struct {
Commits []historyCommit `json:"commits"`
}
func fileHistoryHandler(api *giteaAPI) mcp.ToolHandlerFor[fileHistoryInput, fileHistoryOutput] {
return func(ctx context.Context, _ *mcp.CallToolRequest, in fileHistoryInput) (*mcp.CallToolResult, fileHistoryOutput, error) {
if in.Owner == "" || in.Repo == "" || in.Path == "" {
return nil, fileHistoryOutput{}, fmt.Errorf("owner, repo, and path are required")
}
limit := clampInt(in.Limit, 1, 50, 20)
q := url.Values{
"path": []string{in.Path},
"limit": []string{strconv.Itoa(limit)},
"stat": []string{"false"},
"verification": []string{"false"},
"files": []string{"false"},
}
setIf(q, "sha", in.Ref)
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"`
}
if err := api.get(ctx, path, q, &raw); err != nil {
return nil, fileHistoryOutput{}, err
}
out := fileHistoryOutput{Commits: make([]historyCommit, 0, len(raw))}
for _, c := range raw {
out.Commits = append(out.Commits, historyCommit{
SHA: c.SHA, Message: firstLine(c.RepoCommit.Message),
Author: c.RepoCommit.Author.Name, Date: c.RepoCommit.Author.Date,
URL: c.HTMLURL,
})
}
return nil, out, nil
}
}