145 lines
4.9 KiB
Go
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
|
|
}
|
|
}
|