diff --git a/BUILDING.md b/BUILDING.md new file mode 100644 index 0000000..3b25276 --- /dev/null +++ b/BUILDING.md @@ -0,0 +1,63 @@ +# Building gitea-mcp on Windows + +This project includes PowerShell and batch scripts to build the gitea-mcp application on Windows systems. + +## Prerequisites + +- Go 1.24 or later +- Git (for version information) +- PowerShell 5.1 or later (included with Windows 10/11) + +## Build Scripts + +### PowerShell Script (`build.ps1`) + +The main build script that replicates all Makefile functionality: + +```powershell +# Show help +.\build.ps1 help + +# Build the application +.\build.ps1 build + +# Install the application +.\build.ps1 install + +# Clean build artifacts +.\build.ps1 clean + +# Run in development mode (hot reload) +.\build.ps1 dev + +# Update vendor dependencies +.\build.ps1 vendor +``` + +### Batch File Wrapper (`build.bat`) + +A simple wrapper to run the PowerShell script: + +```cmd +# Run with default help target +build.bat + +# Run specific target +build.bat build +build.bat install +``` + +## Available Targets + +- **help** - Print help message +- **build** - Build the application executable +- **install** - Build and install to GOPATH/bin +- **uninstall** - Remove executable from GOPATH/bin +- **clean** - Remove build artifacts +- **air** - Install air for hot reload development +- **dev** - Run with hot reload development +- **vendor** - Tidy and verify Go module dependencies + +## Output + +The build process creates `gitea-mcp.exe` in the project directory. diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..b17ac33 --- /dev/null +++ b/build.bat @@ -0,0 +1,2 @@ +@echo off +powershell -ExecutionPolicy Bypass -File "%~dp0build.ps1" %* diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..da8c595 --- /dev/null +++ b/build.ps1 @@ -0,0 +1,220 @@ +#!/usr/bin/env pwsh + +# PowerShell build script for gitea-mcp +# Replicates the functionality of the Makefile + +param( + [string]$Target = "help" +) + +# Configuration +$EXECUTABLE = "gitea-mcp.exe" +$VERSION = & git describe --tags --always 2>$null | ForEach-Object { $_ -replace '-', '+' -replace '^v', '' } +if (-not $VERSION) { $VERSION = "dev" } +$LDFLAGS = "-X `"main.Version=$VERSION`"" + +# Colors for output (Windows PowerShell compatible) +$CYAN = "Cyan" +$RESET = "White" + +function Write-Header { + param([string]$Message) + Write-Host "=== $Message ===" -ForegroundColor Green +} + +function Write-Info { + param([string]$Message) + Write-Host $Message -ForegroundColor Yellow +} + +function Write-Success { + param([string]$Message) + Write-Host $Message -ForegroundColor Green +} + +function Write-Error { + param([string]$Message) + Write-Host $Message -ForegroundColor Red +} + +function Get-Help { + Write-Host "Usage: .\build.ps1 [target]" -ForegroundColor Green + Write-Host "" + Write-Host "Targets:" -ForegroundColor Green + Write-Host "" + + Write-Host ("{0,-30}" -f "help") -ForegroundColor Cyan -NoNewline + Write-Host " Print this help message." + Write-Host ("{0,-30}" -f "build") -ForegroundColor Cyan -NoNewline + Write-Host " Build the application." + Write-Host ("{0,-30}" -f "install") -ForegroundColor Cyan -NoNewline + Write-Host " Install the application." + Write-Host ("{0,-30}" -f "uninstall") -ForegroundColor Cyan -NoNewline + Write-Host " Uninstall the application." + Write-Host ("{0,-30}" -f "clean") -ForegroundColor Cyan -NoNewline + Write-Host " Clean the build artifacts." + Write-Host ("{0,-30}" -f "air") -ForegroundColor Cyan -NoNewline + Write-Host " Install air for hot reload." + Write-Host ("{0,-30}" -f "dev") -ForegroundColor Cyan -NoNewline + Write-Host " Run the application with hot reload." + Write-Host ("{0,-30}" -f "vendor") -ForegroundColor Cyan -NoNewline + Write-Host " Tidy and verify module dependencies." +} + +function Build-App { + Write-Header "Building application" + + $ldflags = "-s -w $LDFLAGS" + Write-Info "go build -v -ldflags '$ldflags' -o $EXECUTABLE" + + try { + & go build -v -ldflags $ldflags -o $EXECUTABLE + if ($LASTEXITCODE -eq 0) { + Write-Success "Build successful: $EXECUTABLE" + } else { + Write-Error "Build failed with exit code: $LASTEXITCODE" + exit $LASTEXITCODE + } + } catch { + Write-Error "Build failed: $_" + exit 1 + } +} + +function Install-App { + Write-Header "Installing application" + + # First build the application + Build-App + + $GOPATH = $env:GOPATH + if (-not $GOPATH) { + $GOPATH = Join-Path $env:USERPROFILE "go" + } + + $installDir = Join-Path $GOPATH "bin" + $installPath = Join-Path $installDir $EXECUTABLE + + Write-Info "Installing $EXECUTABLE to $installPath" + + # Create directory if it doesn't exist + if (-not (Test-Path $installDir)) { + New-Item -ItemType Directory -Path $installDir -Force | Out-Null + } + + # Copy the executable + if (Test-Path $EXECUTABLE) { + Copy-Item $EXECUTABLE $installPath -Force + Write-Success "Installed $EXECUTABLE to $installPath" + Write-Info "Please add $installDir to your PATH if it is not already there." + } else { + Write-Error "Executable not found. Please build first." + exit 1 + } +} + +function Uninstall-App { + Write-Header "Uninstalling application" + + $GOPATH = $env:GOPATH + if (-not $GOPATH) { + $GOPATH = Join-Path $env:USERPROFILE "go" + } + + $installPath = Join-Path $GOPATH "bin" $EXECUTABLE + + Write-Info "Uninstalling $EXECUTABLE from $installPath" + + if (Test-Path $installPath) { + Remove-Item $installPath -Force + Write-Success "Uninstalled $EXECUTABLE from $installPath" + } else { + Write-Warning "$EXECUTABLE not found at $installPath" + } +} + +function Clean-Build { + Write-Header "Cleaning build artifacts" + + Write-Info "Cleaning up $EXECUTABLE" + + if (Test-Path $EXECUTABLE) { + Remove-Item $EXECUTABLE -Force + Write-Success "Cleaned up $EXECUTABLE" + } else { + Write-Warning "$EXECUTABLE not found" + } +} + +function Install-Air { + Write-Header "Installing air for hot reload" + + # Check if air is already installed + $airPath = Get-Command air -ErrorAction SilentlyContinue + if ($airPath) { + Write-Success "air is already installed" + return + } + + Write-Info "Installing github.com/air-verse/air@latest" + try { + & go install github.com/air-verse/air@latest + if ($LASTEXITCODE -eq 0) { + Write-Success "air installed successfully" + } else { + Write-Error "Failed to install air" + exit $LASTEXITCODE + } + } catch { + Write-Error "Failed to install air: $_" + exit 1 + } +} + +function Start-Dev { + Write-Header "Starting development mode with hot reload" + + # Install air first + Install-Air + + Write-Info "Starting air with build configuration" + & air --build.cmd "go build -o $EXECUTABLE" --build.bin "./$EXECUTABLE" +} + +function Update-Vendor { + Write-Header "Tidying and verifying module dependencies" + + Write-Info "Running go mod tidy" + & go mod tidy + if ($LASTEXITCODE -ne 0) { + Write-Error "go mod tidy failed" + exit $LASTEXITCODE + } + + Write-Info "Running go mod verify" + & go mod verify + if ($LASTEXITCODE -ne 0) { + Write-Error "go mod verify failed" + exit $LASTEXITCODE + } + + Write-Success "Dependencies updated successfully" +} + +# Main execution logic +switch ($Target.ToLower()) { + "help" { Get-Help } + "build" { Build-App } + "install" { Install-App } + "uninstall" { Uninstall-App } + "clean" { Clean-Build } + "air" { Install-Air } + "dev" { Start-Dev } + "vendor" { Update-Vendor } + default { + Write-Error "Unknown target: $Target" + Write-Host "" + Get-Help + exit 1 + } +} diff --git a/operation/milestone/milestone.go b/operation/milestone/milestone.go new file mode 100644 index 0000000..ca49ab8 --- /dev/null +++ b/operation/milestone/milestone.go @@ -0,0 +1,275 @@ +package milestone + +import ( + "context" + "fmt" + + "gitea.com/gitea/gitea-mcp/pkg/gitea" + "gitea.com/gitea/gitea-mcp/pkg/log" + "gitea.com/gitea/gitea-mcp/pkg/ptr" + "gitea.com/gitea/gitea-mcp/pkg/to" + "gitea.com/gitea/gitea-mcp/pkg/tool" + + gitea_sdk "code.gitea.io/sdk/gitea" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +var Tool = tool.New() + +const ( + GetMilestoneToolName = "get_milestone" + ListMilestonesToolName = "list_milestones" + CreateMilestoneToolName = "create_milestone" + EditMilestoneToolName = "edit_milestone" + DeleteMilestoneToolName = "delete_milestone" +) + +var ( + GetMilestoneTool = mcp.NewTool( + GetMilestoneToolName, + mcp.WithDescription("get milestone by id"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithNumber("id", mcp.Required(), mcp.Description("milestone id")), + ) + + ListMilestonesTool = mcp.NewTool( + ListMilestonesToolName, + mcp.WithDescription("List milestones"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithString("state", mcp.Description("milestone state"), mcp.DefaultString("all")), + mcp.WithString("name", mcp.Description("milestone name")), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + CreateMilestoneTool = mcp.NewTool( + CreateMilestoneToolName, + mcp.WithDescription("create milestone"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithString("title", mcp.Required(), mcp.Description("milestone title")), + mcp.WithString("description", mcp.Description("milestone description")), + mcp.WithString("due_on", mcp.Description("due date")), + ) + + EditMilestoneTool = mcp.NewTool( + EditMilestoneToolName, + mcp.WithDescription("edit milestone"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithNumber("id", mcp.Required(), mcp.Description("milestone id")), + mcp.WithString("title", mcp.Description("milestone title")), + mcp.WithString("description", mcp.Description("milestone description")), + mcp.WithString("due_on", mcp.Description("due date")), + mcp.WithString("state", mcp.Description("milestone state, one of open, closed")), + ) + + DeleteMilestoneTool = mcp.NewTool( + DeleteMilestoneToolName, + mcp.WithDescription("delete milestone"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithNumber("id", mcp.Required(), mcp.Description("milestone id")), + ) +) + +func init() { + Tool.RegisterRead(server.ServerTool{ + Tool: GetMilestoneTool, + Handler: GetMilestoneFn, + }) + Tool.RegisterRead(server.ServerTool{ + Tool: ListMilestonesTool, + Handler: ListMilestonesFn, + }) + Tool.RegisterWrite(server.ServerTool{ + Tool: CreateMilestoneTool, + Handler: CreateMilestoneFn, + }) + Tool.RegisterWrite(server.ServerTool{ + Tool: EditMilestoneTool, + Handler: EditMilestoneFn, + }) + Tool.RegisterWrite(server.ServerTool{ + Tool: DeleteMilestoneTool, + Handler: DeleteMilestoneFn, + }) +} + +func GetMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetMilestoneFn") + owner, ok := req.GetArguments()["owner"].(string) + if !ok { + return to.ErrorResult(fmt.Errorf("owner is required")) + } + repo, ok := req.GetArguments()["repo"].(string) + if !ok { + return to.ErrorResult(fmt.Errorf("repo is required")) + } + id, ok := req.GetArguments()["id"].(float64) + if !ok { + return to.ErrorResult(fmt.Errorf("id is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + milestone, _, err := client.GetMilestone(owner, repo, int64(id)) + if err != nil { + return to.ErrorResult(fmt.Errorf("get %v/%v/milestone/%v err: %v", owner, repo, int64(id), err)) + } + + return to.TextResult(milestone) +} + +func ListMilestonesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListMilestonesFn") + owner, ok := req.GetArguments()["owner"].(string) + if !ok { + return to.ErrorResult(fmt.Errorf("owner is required")) + } + repo, ok := req.GetArguments()["repo"].(string) + if !ok { + return to.ErrorResult(fmt.Errorf("repo is required")) + } + state, ok := req.GetArguments()["state"].(string) + if !ok { + state = "all" + } + name, ok := req.GetArguments()["name"].(string) + if !ok { + name = "" + } + page, ok := req.GetArguments()["page"].(float64) + if !ok { + page = 1 + } + pageSize, ok := req.GetArguments()["pageSize"].(float64) + if !ok { + pageSize = 100 + } + opt := gitea_sdk.ListMilestoneOption{ + State: gitea_sdk.StateType(state), + Name: name, + ListOptions: gitea_sdk.ListOptions{ + Page: int(page), + PageSize: int(pageSize), + }, + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + milestones, _, err := client.ListRepoMilestones(owner, repo, opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("get %v/%v/milestones err: %v", owner, repo, err)) + } + return to.TextResult(milestones) +} + +func CreateMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called CreateMilestoneFn") + owner, ok := req.GetArguments()["owner"].(string) + if !ok { + return to.ErrorResult(fmt.Errorf("owner is required")) + } + repo, ok := req.GetArguments()["repo"].(string) + if !ok { + return to.ErrorResult(fmt.Errorf("repo is required")) + } + title, ok := req.GetArguments()["title"].(string) + if !ok { + return to.ErrorResult(fmt.Errorf("title is required")) + } + + opt := gitea_sdk.CreateMilestoneOption{ + Title: title, + } + + description, ok := req.GetArguments()["description"].(string) + if ok { + opt.Description = description + } + + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + milestone, _, err := client.CreateMilestone(owner, repo, opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("create %v/%v/milestone err: %v", owner, repo, err)) + } + + return to.TextResult(milestone) +} + +func EditMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called EditMilestoneFn") + owner, ok := req.GetArguments()["owner"].(string) + if !ok { + return to.ErrorResult(fmt.Errorf("owner is required")) + } + repo, ok := req.GetArguments()["repo"].(string) + if !ok { + return to.ErrorResult(fmt.Errorf("repo is required")) + } + id, ok := req.GetArguments()["id"].(float64) + if !ok { + return to.ErrorResult(fmt.Errorf("id is required")) + } + + opt := gitea_sdk.EditMilestoneOption{} + + title, ok := req.GetArguments()["title"].(string) + if ok { + opt.Title = title + } + description, ok := req.GetArguments()["description"].(string) + if ok { + opt.Description = ptr.To(description) + } + state, ok := req.GetArguments()["state"].(string) + if ok { + opt.State = ptr.To(gitea_sdk.StateType(state)) + } + + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + milestone, _, err := client.EditMilestone(owner, repo, int64(id), opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("edit %v/%v/milestone/%v err: %v", owner, repo, int64(id), err)) + } + + return to.TextResult(milestone) +} + +func DeleteMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called DeleteMilestoneFn") + owner, ok := req.GetArguments()["owner"].(string) + if !ok { + return to.ErrorResult(fmt.Errorf("owner is required")) + } + repo, ok := req.GetArguments()["repo"].(string) + if !ok { + return to.ErrorResult(fmt.Errorf("repo is required")) + } + id, ok := req.GetArguments()["id"].(float64) + if !ok { + return to.ErrorResult(fmt.Errorf("id is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.DeleteMilestone(owner, repo, int64(id)) + if err != nil { + return to.ErrorResult(fmt.Errorf("delete %v/%v/milestone/%v err: %v", owner, repo, int64(id), err)) + } + + return to.TextResult("Milestone deleted successfully") +} diff --git a/operation/operation.go b/operation/operation.go index 3e7715d..d127b90 100644 --- a/operation/operation.go +++ b/operation/operation.go @@ -12,6 +12,7 @@ import ( "gitea.com/gitea/gitea-mcp/operation/issue" "gitea.com/gitea/gitea-mcp/operation/label" + "gitea.com/gitea/gitea-mcp/operation/milestone" "gitea.com/gitea/gitea-mcp/operation/pull" "gitea.com/gitea/gitea-mcp/operation/repo" "gitea.com/gitea/gitea-mcp/operation/search" @@ -40,6 +41,9 @@ func RegisterTool(s *server.MCPServer) { // Label Tool s.AddTools(label.Tool.Tools()...) + // Milestone Tool + s.AddTools(milestone.Tool.Tools()...) + // Pull Tool s.AddTools(pull.Tool.Tools()...)