feat: add bulk operations - 4 new MCP tools for repo/file management #1
@@ -0,0 +1,191 @@
|
||||
// release_asset.go - upload_release_attachment
|
||||
//
|
||||
// מטרה: השלמת workflow הפצה - העלאת קובץ ZIP של extension ל-release ב-Gitea.
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gitea.com/gitea/gitea-mcp/pkg/flag"
|
||||
"gitea.com/gitea/gitea-mcp/pkg/log"
|
||||
"gitea.com/gitea/gitea-mcp/pkg/params"
|
||||
"gitea.com/gitea/gitea-mcp/pkg/to"
|
||||
|
||||
mcpContext "gitea.com/gitea/gitea-mcp/pkg/context"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
const (
|
||||
UploadReleaseAttachmentToolName = "upload_release_attachment"
|
||||
|
||||
// MaxAttachmentSizeBytes — תקרה לגודל attachment (100MB).
|
||||
MaxAttachmentSizeBytes = 100 * 1024 * 1024
|
||||
)
|
||||
|
||||
var UploadReleaseAttachmentTool = mcp.NewTool(
|
||||
UploadReleaseAttachmentToolName,
|
||||
mcp.WithDescription(
|
||||
"Upload a file as an asset to an existing release. "+
|
||||
"Essential for publishing extension .zip packages. "+
|
||||
"The file is read from the local filesystem and uploaded via multipart form.",
|
||||
),
|
||||
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
||||
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
||||
mcp.WithNumber("release_id", mcp.Required(), mcp.Description("release ID")),
|
||||
mcp.WithString("file_path", mcp.Required(),
|
||||
mcp.Description("absolute path to the local file to upload")),
|
||||
mcp.WithString("name",
|
||||
mcp.Description("filename in the release (defaults to basename of file_path)")),
|
||||
)
|
||||
|
||||
func init() {
|
||||
Tool.RegisterWrite(server.ServerTool{
|
||||
Tool: UploadReleaseAttachmentTool,
|
||||
Handler: UploadReleaseAttachmentFn,
|
||||
})
|
||||
}
|
||||
|
||||
// ReleaseAsset — תוצאה מ-Gitea API.
|
||||
type ReleaseAsset struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
DownloadCount int64 `json:"download_count"`
|
||||
BrowserDownloadURL string `json:"browser_download_url"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
func UploadReleaseAttachmentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called UploadReleaseAttachmentFn")
|
||||
args := req.GetArguments()
|
||||
|
||||
owner, err := params.GetString(args, "owner")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
repo, err := params.GetString(args, "repo")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
releaseID, err := params.GetIndex(args, "release_id")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
filePath, err := params.GetString(args, "file_path")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
name, _ := args["name"].(string)
|
||||
|
||||
// אבטחה: דרוש absolute path
|
||||
if !filepath.IsAbs(filePath) {
|
||||
return to.ErrorResult(fmt.Errorf("file_path must be absolute"))
|
||||
}
|
||||
|
||||
fileInfo, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("stat file: %w", err))
|
||||
}
|
||||
if fileInfo.Size() > MaxAttachmentSizeBytes {
|
||||
return to.ErrorResult(fmt.Errorf(
|
||||
"file too large (%d bytes > max %d)",
|
||||
fileInfo.Size(), MaxAttachmentSizeBytes,
|
||||
))
|
||||
}
|
||||
if name == "" {
|
||||
name = filepath.Base(filePath)
|
||||
}
|
||||
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("open file: %w", err))
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// בניית multipart form
|
||||
var body bytes.Buffer
|
||||
writer := multipart.NewWriter(&body)
|
||||
part, err := writer.CreateFormFile("attachment", name)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("create form file: %w", err))
|
||||
}
|
||||
if _, err := io.Copy(part, file); err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("copy file content: %w", err))
|
||||
}
|
||||
if err := writer.Close(); err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("close writer: %w", err))
|
||||
}
|
||||
|
||||
host := flag.Host
|
||||
if host == "" {
|
||||
return to.ErrorResult(fmt.Errorf("gitea host not configured"))
|
||||
}
|
||||
apiURL := fmt.Sprintf("%s/api/v1/repos/%s/%s/releases/%d/assets",
|
||||
host,
|
||||
url.PathEscape(owner),
|
||||
url.PathEscape(repo),
|
||||
releaseID,
|
||||
)
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, "POST", apiURL, &body)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("create request: %w", err))
|
||||
}
|
||||
|
||||
// אימות - token מ-context או flag
|
||||
token := flag.Token
|
||||
if t, ok := ctx.Value(mcpContext.TokenContextKey).(string); ok && t != "" {
|
||||
token = t
|
||||
}
|
||||
if token != "" {
|
||||
httpReq.Header.Set("Authorization", "token "+token)
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
httpReq.Header.Set("Accept", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("upload request: %w", err))
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return to.ErrorResult(fmt.Errorf(
|
||||
"upload failed (status %d): %s",
|
||||
resp.StatusCode, string(respBody),
|
||||
))
|
||||
}
|
||||
|
||||
var asset ReleaseAsset
|
||||
if err := json.Unmarshal(respBody, &asset); err != nil {
|
||||
return to.ErrorResult(fmt.Errorf("parse response: %w", err))
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf(
|
||||
"Asset uploaded successfully:\n"+
|
||||
" Asset ID: %d\n"+
|
||||
" Name: %s\n"+
|
||||
" Size: %s\n"+
|
||||
" Download URL: %s\n"+
|
||||
" Created at: %s\n",
|
||||
asset.ID,
|
||||
asset.Name,
|
||||
formatBytes(asset.Size),
|
||||
asset.BrowserDownloadURL,
|
||||
asset.CreatedAt,
|
||||
)
|
||||
return to.TextResult(msg)
|
||||
}
|
||||
Reference in New Issue
Block a user