mirror of
https://gitea.com/gitea/gitea-mcp.git
synced 2025-09-13 08:23:15 +00:00
this PR introduces support for per-request authentication tokens in HTTP and SSE modes. The server now inspects incoming requests for an `Authorization: Bearer <token>` header. Previously, the server operated with a single, globally configured Gitea token. This change allows different clients to use their own tokens when communicating with the MCP server, enhancing security and flexibility. To support this, the Gitea API client initialization has been refactored: - The global singleton Gitea client has been removed. - A new `ClientFromContext` function creates a Gitea client on-demand, using a token from the request context if available, and falling back to the globally configured token otherwise. - All tool functions now retrieve the client from the context for each call. The README has also been updated to reflect the new configuration option. Update: #59 Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/89 Reviewed-by: hiifong <i@hiif.ong> Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: Darren Hoo <darren.hoo@gmail.com> Co-committed-by: Darren Hoo <darren.hoo@gmail.com>
455 lines
15 KiB
Go
455 lines
15 KiB
Go
package label
|
|
|
|
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 (
|
|
ListRepoLabelsToolName = "list_repo_labels"
|
|
GetRepoLabelToolName = "get_repo_label"
|
|
CreateRepoLabelToolName = "create_repo_label"
|
|
EditRepoLabelToolName = "edit_repo_label"
|
|
DeleteRepoLabelToolName = "delete_repo_label"
|
|
AddIssueLabelsToolName = "add_issue_labels"
|
|
ReplaceIssueLabelsToolName = "replace_issue_labels"
|
|
ClearIssueLabelsToolName = "clear_issue_labels"
|
|
RemoveIssueLabelToolName = "remove_issue_label"
|
|
)
|
|
|
|
var (
|
|
ListRepoLabelsTool = mcp.NewTool(
|
|
ListRepoLabelsToolName,
|
|
mcp.WithDescription("Lists all labels for a given repository"),
|
|
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.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
|
|
)
|
|
|
|
GetRepoLabelTool = mcp.NewTool(
|
|
GetRepoLabelToolName,
|
|
mcp.WithDescription("Gets a single label by its ID for a repository"),
|
|
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("label ID")),
|
|
)
|
|
|
|
CreateRepoLabelTool = mcp.NewTool(
|
|
CreateRepoLabelToolName,
|
|
mcp.WithDescription("Creates a new label for a repository"),
|
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
|
mcp.WithString("name", mcp.Required(), mcp.Description("label name")),
|
|
mcp.WithString("color", mcp.Required(), mcp.Description("label color (hex code, e.g., #RRGGBB)")),
|
|
mcp.WithString("description", mcp.Description("label description")),
|
|
)
|
|
|
|
EditRepoLabelTool = mcp.NewTool(
|
|
EditRepoLabelToolName,
|
|
mcp.WithDescription("Edits an existing label in a repository"),
|
|
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("label ID")),
|
|
mcp.WithString("name", mcp.Description("new label name")),
|
|
mcp.WithString("color", mcp.Description("new label color (hex code, e.g., #RRGGBB)")),
|
|
mcp.WithString("description", mcp.Description("new label description")),
|
|
)
|
|
|
|
DeleteRepoLabelTool = mcp.NewTool(
|
|
DeleteRepoLabelToolName,
|
|
mcp.WithDescription("Deletes a label from a repository"),
|
|
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("label ID")),
|
|
)
|
|
|
|
AddIssueLabelsTool = mcp.NewTool(
|
|
AddIssueLabelsToolName,
|
|
mcp.WithDescription("Adds one or more labels to an issue"),
|
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
|
mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")),
|
|
mcp.WithArray("labels", mcp.Required(), mcp.Description("array of label IDs to add"), mcp.Items(map[string]interface{}{"type": "number"})),
|
|
)
|
|
|
|
ReplaceIssueLabelsTool = mcp.NewTool(
|
|
ReplaceIssueLabelsToolName,
|
|
mcp.WithDescription("Replaces all labels on an issue"),
|
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
|
mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")),
|
|
mcp.WithArray("labels", mcp.Required(), mcp.Description("array of label IDs to replace with"), mcp.Items(map[string]interface{}{"type": "number"})),
|
|
)
|
|
|
|
ClearIssueLabelsTool = mcp.NewTool(
|
|
ClearIssueLabelsToolName,
|
|
mcp.WithDescription("Removes all labels from an issue"),
|
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
|
mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")),
|
|
)
|
|
|
|
RemoveIssueLabelTool = mcp.NewTool(
|
|
RemoveIssueLabelToolName,
|
|
mcp.WithDescription("Removes a single label from an issue"),
|
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
|
mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")),
|
|
mcp.WithNumber("label_id", mcp.Required(), mcp.Description("label ID to remove")),
|
|
)
|
|
)
|
|
|
|
func init() {
|
|
Tool.RegisterRead(server.ServerTool{
|
|
Tool: ListRepoLabelsTool,
|
|
Handler: ListRepoLabelsFn,
|
|
})
|
|
Tool.RegisterRead(server.ServerTool{
|
|
Tool: GetRepoLabelTool,
|
|
Handler: GetRepoLabelFn,
|
|
})
|
|
Tool.RegisterWrite(server.ServerTool{
|
|
Tool: CreateRepoLabelTool,
|
|
Handler: CreateRepoLabelFn,
|
|
})
|
|
Tool.RegisterWrite(server.ServerTool{
|
|
Tool: EditRepoLabelTool,
|
|
Handler: EditRepoLabelFn,
|
|
})
|
|
Tool.RegisterWrite(server.ServerTool{
|
|
Tool: DeleteRepoLabelTool,
|
|
Handler: DeleteRepoLabelFn,
|
|
})
|
|
Tool.RegisterWrite(server.ServerTool{
|
|
Tool: AddIssueLabelsTool,
|
|
Handler: AddIssueLabelsFn,
|
|
})
|
|
Tool.RegisterWrite(server.ServerTool{
|
|
Tool: ReplaceIssueLabelsTool,
|
|
Handler: ReplaceIssueLabelsFn,
|
|
})
|
|
Tool.RegisterWrite(server.ServerTool{
|
|
Tool: ClearIssueLabelsTool,
|
|
Handler: ClearIssueLabelsFn,
|
|
})
|
|
Tool.RegisterWrite(server.ServerTool{
|
|
Tool: RemoveIssueLabelTool,
|
|
Handler: RemoveIssueLabelFn,
|
|
})
|
|
}
|
|
|
|
func ListRepoLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called ListRepoLabelsFn")
|
|
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"))
|
|
}
|
|
page, ok := req.GetArguments()["page"].(float64)
|
|
if !ok {
|
|
page = 1
|
|
}
|
|
pageSize, ok := req.GetArguments()["pageSize"].(float64)
|
|
if !ok {
|
|
pageSize = 100
|
|
}
|
|
|
|
opt := gitea_sdk.ListLabelsOptions{
|
|
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))
|
|
}
|
|
labels, _, err := client.ListRepoLabels(owner, repo, opt)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("list %v/%v/labels err: %v", owner, repo, err))
|
|
}
|
|
return to.TextResult(labels)
|
|
}
|
|
|
|
func GetRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called GetRepoLabelFn")
|
|
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("label ID is required"))
|
|
}
|
|
|
|
client, err := gitea.ClientFromContext(ctx)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
|
}
|
|
label, _, err := client.GetRepoLabel(owner, repo, int64(id))
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("get %v/%v/label/%v err: %v", owner, repo, int64(id), err))
|
|
}
|
|
return to.TextResult(label)
|
|
}
|
|
|
|
func CreateRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called CreateRepoLabelFn")
|
|
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"))
|
|
}
|
|
name, ok := req.GetArguments()["name"].(string)
|
|
if !ok {
|
|
return to.ErrorResult(fmt.Errorf("name is required"))
|
|
}
|
|
color, ok := req.GetArguments()["color"].(string)
|
|
if !ok {
|
|
return to.ErrorResult(fmt.Errorf("color is required"))
|
|
}
|
|
description, _ := req.GetArguments()["description"].(string) // Optional
|
|
|
|
opt := gitea_sdk.CreateLabelOption{
|
|
Name: name,
|
|
Color: color,
|
|
Description: description,
|
|
}
|
|
|
|
client, err := gitea.ClientFromContext(ctx)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
|
}
|
|
label, _, err := client.CreateLabel(owner, repo, opt)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("create %v/%v/label err: %v", owner, repo, err))
|
|
}
|
|
return to.TextResult(label)
|
|
}
|
|
|
|
func EditRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called EditRepoLabelFn")
|
|
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("label ID is required"))
|
|
}
|
|
|
|
opt := gitea_sdk.EditLabelOption{}
|
|
if name, ok := req.GetArguments()["name"].(string); ok {
|
|
opt.Name = ptr.To(name)
|
|
}
|
|
if color, ok := req.GetArguments()["color"].(string); ok {
|
|
opt.Color = ptr.To(color)
|
|
}
|
|
if description, ok := req.GetArguments()["description"].(string); ok {
|
|
opt.Description = ptr.To(description)
|
|
}
|
|
|
|
client, err := gitea.ClientFromContext(ctx)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
|
}
|
|
label, _, err := client.EditLabel(owner, repo, int64(id), opt)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("edit %v/%v/label/%v err: %v", owner, repo, int64(id), err))
|
|
}
|
|
return to.TextResult(label)
|
|
}
|
|
|
|
func DeleteRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called DeleteRepoLabelFn")
|
|
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("label ID is required"))
|
|
}
|
|
|
|
client, err := gitea.ClientFromContext(ctx)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
|
}
|
|
_, err = client.DeleteLabel(owner, repo, int64(id))
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("delete %v/%v/label/%v err: %v", owner, repo, int64(id), err))
|
|
}
|
|
return to.TextResult("Label deleted successfully")
|
|
}
|
|
|
|
func AddIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called AddIssueLabelsFn")
|
|
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"))
|
|
}
|
|
index, ok := req.GetArguments()["index"].(float64)
|
|
if !ok {
|
|
return to.ErrorResult(fmt.Errorf("issue index is required"))
|
|
}
|
|
labelsRaw, ok := req.GetArguments()["labels"].([]interface{})
|
|
if !ok {
|
|
return to.ErrorResult(fmt.Errorf("labels (array of IDs) is required"))
|
|
}
|
|
var labels []int64
|
|
for _, l := range labelsRaw {
|
|
if labelID, ok := l.(float64); ok {
|
|
labels = append(labels, int64(labelID))
|
|
} else {
|
|
return to.ErrorResult(fmt.Errorf("invalid label ID in labels array"))
|
|
}
|
|
}
|
|
|
|
opt := gitea_sdk.IssueLabelsOption{
|
|
Labels: labels,
|
|
}
|
|
|
|
client, err := gitea.ClientFromContext(ctx)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
|
}
|
|
issueLabels, _, err := client.AddIssueLabels(owner, repo, int64(index), opt)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("add labels to %v/%v/issue/%v err: %v", owner, repo, int64(index), err))
|
|
}
|
|
return to.TextResult(issueLabels)
|
|
}
|
|
|
|
func ReplaceIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called ReplaceIssueLabelsFn")
|
|
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"))
|
|
}
|
|
index, ok := req.GetArguments()["index"].(float64)
|
|
if !ok {
|
|
return to.ErrorResult(fmt.Errorf("issue index is required"))
|
|
}
|
|
labelsRaw, ok := req.GetArguments()["labels"].([]interface{})
|
|
if !ok {
|
|
return to.ErrorResult(fmt.Errorf("labels (array of IDs) is required"))
|
|
}
|
|
var labels []int64
|
|
for _, l := range labelsRaw {
|
|
if labelID, ok := l.(float64); ok {
|
|
labels = append(labels, int64(labelID))
|
|
} else {
|
|
return to.ErrorResult(fmt.Errorf("invalid label ID in labels array"))
|
|
}
|
|
}
|
|
|
|
opt := gitea_sdk.IssueLabelsOption{
|
|
Labels: labels,
|
|
}
|
|
|
|
client, err := gitea.ClientFromContext(ctx)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
|
}
|
|
issueLabels, _, err := client.ReplaceIssueLabels(owner, repo, int64(index), opt)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("replace labels on %v/%v/issue/%v err: %v", owner, repo, int64(index), err))
|
|
}
|
|
return to.TextResult(issueLabels)
|
|
}
|
|
|
|
func ClearIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called ClearIssueLabelsFn")
|
|
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"))
|
|
}
|
|
index, ok := req.GetArguments()["index"].(float64)
|
|
if !ok {
|
|
return to.ErrorResult(fmt.Errorf("issue index is required"))
|
|
}
|
|
|
|
client, err := gitea.ClientFromContext(ctx)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
|
}
|
|
_, err = client.ClearIssueLabels(owner, repo, int64(index))
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("clear labels on %v/%v/issue/%v err: %v", owner, repo, int64(index), err))
|
|
}
|
|
return to.TextResult("Labels cleared successfully")
|
|
}
|
|
|
|
func RemoveIssueLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called RemoveIssueLabelFn")
|
|
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"))
|
|
}
|
|
index, ok := req.GetArguments()["index"].(float64)
|
|
if !ok {
|
|
return to.ErrorResult(fmt.Errorf("issue index is required"))
|
|
}
|
|
labelID, ok := req.GetArguments()["label_id"].(float64)
|
|
if !ok {
|
|
return to.ErrorResult(fmt.Errorf("label ID is required"))
|
|
}
|
|
|
|
client, err := gitea.ClientFromContext(ctx)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
|
}
|
|
_, err = client.DeleteIssueLabel(owner, repo, int64(index), int64(labelID))
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("remove label %v from %v/%v/issue/%v err: %v", int64(labelID), owner, repo, int64(index), err))
|
|
}
|
|
return to.TextResult("Label removed successfully")
|
|
}
|