Files
gitea-mcp/operation/repo/tag.go
Darren Hoo d7addd56c4 feat: read token from header in http/sse mode (#89)
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>
2025-09-12 03:57:57 +00:00

212 lines
6.1 KiB
Go

package repo
import (
"context"
"fmt"
"gitea.com/gitea/gitea-mcp/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
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)),
)
)
func init() {
Tool.RegisterWrite(server.ServerTool{
Tool: CreateTagTool,
Handler: CreateTagFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: DeleteTagTool,
Handler: DeleteTagFn,
})
Tool.RegisterRead(server.ServerTool{
Tool: GetTagTool,
Handler: GetTagFn,
})
Tool.RegisterRead(server.ServerTool{
Tool: ListTagsTool,
Handler: ListTagsFn,
})
}
// 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.GetArguments()["owner"].(string)
if !ok {
return nil, fmt.Errorf("owner is required")
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return nil, fmt.Errorf("repo is required")
}
tagName, ok := req.GetArguments()["tag_name"].(string)
if !ok {
return nil, fmt.Errorf("tag_name is required")
}
target, _ := req.GetArguments()["target"].(string)
message, _ := req.GetArguments()["message"].(string)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, _, err = 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.GetArguments()["owner"].(string)
if !ok {
return nil, fmt.Errorf("owner is required")
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return nil, fmt.Errorf("repo is required")
}
tagName, ok := req.GetArguments()["tag_name"].(string)
if !ok {
return nil, fmt.Errorf("tag_name is required")
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = 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.GetArguments()["owner"].(string)
if !ok {
return nil, fmt.Errorf("owner is required")
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return nil, fmt.Errorf("repo is required")
}
tagName, ok := req.GetArguments()["tag_name"].(string)
if !ok {
return nil, fmt.Errorf("tag_name is required")
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
tag, _, err := 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.GetArguments()["owner"].(string)
if !ok {
return nil, fmt.Errorf("owner is required")
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return nil, fmt.Errorf("repo is required")
}
page, _ := req.GetArguments()["page"].(float64)
pageSize, _ := req.GetArguments()["pageSize"].(float64)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
tags, _, err := 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)
}