diff --git a/README.md b/README.md index 378f736..70c51f5 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,15 @@ The Gitea MCP Server supports the following tools: | create_branch | Branch | Create a new branch | | delete_branch | Branch | Delete a branch | | list_branches | Branch | List all branches in a repository | +| create_release | Release | Create a new release in a repository | +| delete_release | Release | Delete a release from a repository | +| get_release | Release | Get a release | +| get_latest_release | Release | Get the latest release in a repository | +| list_releases | Release | List all releases in a repository | +| create_tag | Tag | Create a new tag | +| delete_tag | Tag | Delete a tag | +| get_tag | Tag | Get a tag | +| list_tags | Tag | List all tags in a repository | | list_repo_commits | Commit | List all commits in a repository | | get_file_content | File | Get the content and metadata of a file | | create_file | File | Create a new file | diff --git a/README.zh-cn.md b/README.zh-cn.md index 14a6e04..2a15d9e 100644 --- a/README.zh-cn.md +++ b/README.zh-cn.md @@ -154,6 +154,15 @@ Gitea MCP 服务器支持以下工具: | create_branch | 分支 | 创建一个新分支 | | delete_branch | 分支 | 删除一个分支 | | list_branches | 分支 | 列出仓库中的所有分支 | +| create_release | 版本发布 | 创建一个新版本发布 | +| delete_release | 版本发布 | 删除一个版本发布 | +| get_release | 版本发布 | 获取一个版本发布 | +| get_latest_release | 版本发布 | 获取最新的版本发布 | +| list_releases | 版本发布 | 列出所有版本发布 | +| create_tag | 标签 | 创建一个新标签 | +| delete_tag | 标签 | 删除一个标签 | +| get_tag | 标签 | 获取一个标签 | +| list_tags | 标签 | 列出所有标签 | | list_repo_commits | 提交 | 列出仓库中的所有提交 | | get_file_content | 文件 | 获取文件的内容和元数据 | | create_file | 文件 | 创建一个新文件 | diff --git a/README.zh-tw.md b/README.zh-tw.md index f1f7204..81fa780 100644 --- a/README.zh-tw.md +++ b/README.zh-tw.md @@ -144,7 +144,6 @@ cp gitea-mcp /usr/local/bin/ ## ✅ 可用工具 Gitea MCP 伺服器支持以下工具: - | 工具 | 範圍 | 描述 | | :--------------------------: | :------: | :--------------------------: | | get_my_user_info | 用戶 | 獲取已認證用戶的信息 | @@ -154,6 +153,15 @@ Gitea MCP 伺服器支持以下工具: | create_branch | 分支 | 創建一個新分支 | | delete_branch | 分支 | 刪除一個分支 | | list_branches | 分支 | 列出倉庫中的所有分支 | +| create_release | 版本發布 | 創建一個新版本發布 | +| delete_release | 版本發布 | 刪除一個版本發布 | +| get_release | 版本發布 | 獲取一個版本發布 | +| get_latest_release | 版本發布 | 獲取最新的版本發布 | +| list_releases | 版本發布 | 列出所有版本發布 | +| create_tag | 標籤 | 創建一個新標籤 | +| delete_tag | 標籤 | 刪除一個標籤 | +| get_tag | 標籤 | 獲取一個標籤 | +| list_tags | 標籤 | 列出所有標籤 | | list_repo_commits | 提交 | 列出倉庫中的所有提交 | | get_file_content | 文件 | 獲取文件的內容和元數據 | | create_file | 文件 | 創建一個新文件 | diff --git a/operation/repo/release.go b/operation/repo/release.go new file mode 100644 index 0000000..2e0f341 --- /dev/null +++ b/operation/repo/release.go @@ -0,0 +1,230 @@ +package repo + +import ( + "context" + "fmt" + "time" + + gitea_sdk "code.gitea.io/sdk/gitea" + "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" + "github.com/mark3labs/mcp-go/mcp" +) + +const ( + CreateReleaseToolName = "create_release" + DeleteReleaseToolName = "delete_release" + GetReleaseToolName = "get_release" + GetLatestReleaseToolName = "get_latest_release" + ListReleasesToolName = "list_releases" +) + +var ( + CreateReleaseTool = mcp.NewTool( + CreateReleaseToolName, + mcp.WithDescription("Create release"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithString("tag_name", mcp.Required(), mcp.Description("tag name")), + mcp.WithString("target", mcp.Required(), mcp.Description("target commitish")), + mcp.WithString("title", mcp.Required(), mcp.Description("release title")), + mcp.WithBoolean("is_draft", mcp.Description("Whether the release is draft"), mcp.DefaultBool(false)), + mcp.WithBoolean("is_pre_release", mcp.Description("Whether the release is pre-release"), mcp.DefaultBool(false)), + ) + + DeleteReleaseTool = mcp.NewTool( + DeleteReleaseToolName, + mcp.WithDescription("Delete release"), + 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("release id")), + ) + + GetReleaseTool = mcp.NewTool( + GetReleaseToolName, + mcp.WithDescription("Get release"), + 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("release id")), + ) + + GetLatestReleaseTool = mcp.NewTool( + GetLatestReleaseToolName, + mcp.WithDescription("Get latest release"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + ) + + ListReleasesTool = mcp.NewTool( + ListReleasesToolName, + mcp.WithDescription("List releases"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithBoolean("is_draft", mcp.Description("Whether the release is draft"), mcp.DefaultBool(false)), + mcp.WithBoolean("is_pre_release", mcp.Description("Whether the release is pre-release"), mcp.DefaultBool(false)), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(20), mcp.Min(1)), + ) +) + +// To avoid return too many tokens, we need to provide at least information as possible +// llm can call get release to get more information +type ListReleaseResult struct { + ID int64 `json:"id"` + TagName string `json:"tag_name"` + Target string `json:"target_commitish"` + Title string `json:"title"` + IsDraft bool `json:"draft"` + IsPrerelease bool `json:"prerelease"` + CreatedAt time.Time `json:"created_at"` + PublishedAt time.Time `json:"published_at"` +} + +func CreateReleaseFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called CreateReleasesFn") + owner, ok := req.Params.Arguments["owner"].(string) + if !ok { + return nil, fmt.Errorf("owner is required") + } + repo, ok := req.Params.Arguments["repo"].(string) + if !ok { + return nil, fmt.Errorf("repo is required") + } + tagName, ok := req.Params.Arguments["tag_name"].(string) + if !ok { + return nil, fmt.Errorf("tag_name is required") + } + target, ok := req.Params.Arguments["target"].(string) + if !ok { + return nil, fmt.Errorf("target is required") + } + title, ok := req.Params.Arguments["title"].(string) + if !ok { + return nil, fmt.Errorf("title is required") + } + isDraft, _ := req.Params.Arguments["is_draft"].(bool) + isPreRelease, _ := req.Params.Arguments["is_pre_release"].(bool) + + _, _, err := gitea.Client().CreateRelease(owner, repo, gitea_sdk.CreateReleaseOption{ + TagName: tagName, + Target: target, + Title: title, + IsDraft: isDraft, + IsPrerelease: isPreRelease, + }) + if err != nil { + return nil, fmt.Errorf("create release error: %v", err) + } + + return mcp.NewToolResultText("Release Created"), nil +} + +func DeleteReleaseFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called DeleteReleaseFn") + owner, ok := req.Params.Arguments["owner"].(string) + if !ok { + return nil, fmt.Errorf("owner is required") + } + repo, ok := req.Params.Arguments["repo"].(string) + if !ok { + return nil, fmt.Errorf("repo is required") + } + id, ok := req.Params.Arguments["id"].(float64) + if !ok { + return nil, fmt.Errorf("id is required") + } + + _, err := gitea.Client().DeleteRelease(owner, repo, int64(id)) + if err != nil { + return nil, fmt.Errorf("delete release error: %v", err) + } + + return to.TextResult("Release deleted successfully") +} + +func GetReleaseFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetReleaseFn") + owner, ok := req.Params.Arguments["owner"].(string) + if !ok { + return nil, fmt.Errorf("owner is required") + } + repo, ok := req.Params.Arguments["repo"].(string) + if !ok { + return nil, fmt.Errorf("repo is required") + } + id, ok := req.Params.Arguments["id"].(float64) + if !ok { + return nil, fmt.Errorf("id is required") + } + + release, _, err := gitea.Client().GetRelease(owner, repo, int64(id)) + if err != nil { + return nil, fmt.Errorf("get release error: %v", err) + } + + return to.TextResult(release) +} + +func GetLatestReleaseFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetLatestReleaseFn") + owner, ok := req.Params.Arguments["owner"].(string) + if !ok { + return nil, fmt.Errorf("owner is required") + } + repo, ok := req.Params.Arguments["repo"].(string) + if !ok { + return nil, fmt.Errorf("repo is required") + } + + release, _, err := gitea.Client().GetLatestRelease(owner, repo) + if err != nil { + return nil, fmt.Errorf("get latest release error: %v", err) + } + + return to.TextResult(release) +} + +func ListReleasesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListReleasesFn") + owner, ok := req.Params.Arguments["owner"].(string) + if !ok { + return nil, fmt.Errorf("owner is required") + } + repo, ok := req.Params.Arguments["repo"].(string) + if !ok { + return nil, fmt.Errorf("repo is required") + } + isDraft, _ := req.Params.Arguments["is_draft"].(bool) + isPreRelease, _ := req.Params.Arguments["is_pre_release"].(bool) + page, _ := req.Params.Arguments["page"].(float64) + pageSize, _ := req.Params.Arguments["pageSize"].(float64) + + releases, _, err := gitea.Client().ListReleases(owner, repo, gitea_sdk.ListReleasesOptions{ + ListOptions: gitea_sdk.ListOptions{ + Page: int(page), + PageSize: int(pageSize), + }, + IsDraft: ptr.To(isDraft), + IsPreRelease: ptr.To(isPreRelease), + }) + if err != nil { + return nil, fmt.Errorf("list releases error: %v", err) + } + + results := make([]ListReleaseResult, len(releases)) + for _, release := range releases { + results = append(results, ListReleaseResult{ + ID: release.ID, + TagName: release.TagName, + Target: release.Target, + Title: release.Title, + IsDraft: release.IsDraft, + IsPrerelease: release.IsPrerelease, + CreatedAt: release.CreatedAt, + PublishedAt: release.PublishedAt, + }) + } + return to.TextResult(results) +} diff --git a/operation/repo/repo.go b/operation/repo/repo.go index c3eff4a..0ec96ea 100644 --- a/operation/repo/repo.go +++ b/operation/repo/repo.go @@ -70,6 +70,19 @@ func RegisterTool(s *server.MCPServer) { s.AddTool(DeleteBranchTool, DeleteBranchFn) s.AddTool(ListBranchesTool, ListBranchesFn) + // Release + s.AddTool(CreateReleaseTool, CreateReleaseFn) + s.AddTool(DeleteReleaseTool, DeleteReleaseFn) + s.AddTool(GetReleaseTool, GetReleaseFn) + s.AddTool(GetLatestReleaseTool, GetLatestReleaseFn) + s.AddTool(ListReleasesTool, ListReleasesFn) + + // Tag + s.AddTool(CreateTagTool, CreateTagFn) + s.AddTool(DeleteTagTool, DeleteTagFn) + s.AddTool(GetTagTool, GetTagFn) + s.AddTool(ListTagsTool, ListTagsFn) + // Commit s.AddTool(ListRepoCommitsTool, ListRepoCommitsFn) } diff --git a/operation/repo/tag.go b/operation/repo/tag.go new file mode 100644 index 0000000..763e872 --- /dev/null +++ b/operation/repo/tag.go @@ -0,0 +1,174 @@ +package repo + +import ( + "context" + "fmt" + + gitea_sdk "code.gitea.io/sdk/gitea" + "gitea.com/gitea/gitea-mcp/pkg/gitea" + "gitea.com/gitea/gitea-mcp/pkg/log" + "gitea.com/gitea/gitea-mcp/pkg/to" + "github.com/mark3labs/mcp-go/mcp" +) + +const ( + CreateTagToolName = "create_tag" + DeleteTagToolName = "delete_tag" + GetTagToolName = "get_tag" + ListTagsToolName = "list_tags" +) + +var ( + CreateTagTool = mcp.NewTool( + CreateTagToolName, + mcp.WithDescription("Create tag"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithString("tag_name", mcp.Required(), mcp.Description("tag name")), + mcp.WithString("target", mcp.Description("target commitish"), mcp.DefaultString("")), + mcp.WithString("message", mcp.Description("tag message"), mcp.DefaultString("")), + ) + + DeleteTagTool = mcp.NewTool( + DeleteTagToolName, + mcp.WithDescription("Delete tag"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithString("tag_name", mcp.Required(), mcp.Description("tag name")), + ) + + GetTagTool = mcp.NewTool( + GetTagToolName, + mcp.WithDescription("Get tag"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithString("tag_name", mcp.Required(), mcp.Description("tag name")), + ) + + ListTagsTool = mcp.NewTool( + ListTagsToolName, + mcp.WithDescription("List tags"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(20), mcp.Min(1)), + ) +) + +// To avoid return too many tokens, we need to provide at least information as possible +// llm can call get tag to get more information +type ListTagResult struct { + ID string `json:"id"` + Name string `json:"name"` + Commit *gitea_sdk.CommitMeta `json:"commit"` + // message may be a long text, so we should not provide it here +} + +func CreateTagFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called CreateTagFn") + owner, ok := req.Params.Arguments["owner"].(string) + if !ok { + return nil, fmt.Errorf("owner is required") + } + repo, ok := req.Params.Arguments["repo"].(string) + if !ok { + return nil, fmt.Errorf("repo is required") + } + tagName, ok := req.Params.Arguments["tag_name"].(string) + if !ok { + return nil, fmt.Errorf("tag_name is required") + } + target, _ := req.Params.Arguments["target"].(string) + message, _ := req.Params.Arguments["message"].(string) + + _, _, err := gitea.Client().CreateTag(owner, repo, gitea_sdk.CreateTagOption{ + TagName: tagName, + Target: target, + Message: message, + }) + if err != nil { + return nil, fmt.Errorf("create tag error: %v", err) + } + + return mcp.NewToolResultText("Tag Created"), nil +} + +func DeleteTagFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called DeleteTagFn") + owner, ok := req.Params.Arguments["owner"].(string) + if !ok { + return nil, fmt.Errorf("owner is required") + } + repo, ok := req.Params.Arguments["repo"].(string) + if !ok { + return nil, fmt.Errorf("repo is required") + } + tagName, ok := req.Params.Arguments["tag_name"].(string) + if !ok { + return nil, fmt.Errorf("tag_name is required") + } + + _, err := gitea.Client().DeleteTag(owner, repo, tagName) + if err != nil { + return nil, fmt.Errorf("delete tag error: %v", err) + } + + return to.TextResult("Tag deleted") +} + +func GetTagFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetTagFn") + owner, ok := req.Params.Arguments["owner"].(string) + if !ok { + return nil, fmt.Errorf("owner is required") + } + repo, ok := req.Params.Arguments["repo"].(string) + if !ok { + return nil, fmt.Errorf("repo is required") + } + tagName, ok := req.Params.Arguments["tag_name"].(string) + if !ok { + return nil, fmt.Errorf("tag_name is required") + } + + tag, _, err := gitea.Client().GetTag(owner, repo, tagName) + if err != nil { + return nil, fmt.Errorf("get tag error: %v", err) + } + + return to.TextResult(tag) +} + +func ListTagsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListTagsFn") + owner, ok := req.Params.Arguments["owner"].(string) + if !ok { + return nil, fmt.Errorf("owner is required") + } + repo, ok := req.Params.Arguments["repo"].(string) + if !ok { + return nil, fmt.Errorf("repo is required") + } + page, _ := req.Params.Arguments["page"].(float64) + pageSize, _ := req.Params.Arguments["pageSize"].(float64) + + tags, _, err := gitea.Client().ListRepoTags(owner, repo, gitea_sdk.ListRepoTagsOptions{ + ListOptions: gitea_sdk.ListOptions{ + Page: int(page), + PageSize: int(pageSize), + }, + }) + if err != nil { + return nil, fmt.Errorf("list tags error: %v", err) + } + + results := make([]ListTagResult, 0, len(tags)) + for _, tag := range tags { + results = append(results, ListTagResult{ + ID: tag.ID, + Name: tag.Name, + Commit: tag.Commit, + }) + } + return to.TextResult(results) +}