From d7addd56c4c221d41f3cb151f30f6b5354d36888 Mon Sep 17 00:00:00 2001 From: Darren Hoo Date: Fri, 12 Sep 2025 03:57:57 +0000 Subject: [PATCH] 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 ` 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 Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/89 Reviewed-by: hiifong Reviewed-by: Lunny Xiao Co-authored-by: Darren Hoo Co-committed-by: Darren Hoo --- README.md | 10 ++++-- README.zh-cn.md | 10 ++++-- README.zh-tw.md | 10 ++++-- operation/issue/issue.go | 42 ++++++++++++++++++---- operation/label/label.go | 54 ++++++++++++++++++++++++----- operation/operation.go | 21 ++++++++++- operation/pull/pull.go | 18 ++++++++-- operation/repo/branch.go | 18 ++++++++-- operation/repo/commit.go | 6 +++- operation/repo/file.go | 30 +++++++++++++--- operation/repo/release.go | 30 +++++++++++++--- operation/repo/repo.go | 21 ++++++++--- operation/repo/tag.go | 24 ++++++++++--- operation/search/search.go | 18 ++++++++-- operation/user/user.go | 12 +++++-- pkg/context/context.go | 7 ++++ pkg/gitea/gitea.go | 71 ++++++++++++++++++-------------------- 17 files changed, 310 insertions(+), 92 deletions(-) create mode 100644 pkg/context/context.go diff --git a/README.md b/README.md index 3bb5267..8ebfbcc 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,10 @@ To configure the MCP server for Gitea, add the following to your MCP configurati { "mcpServers": { "gitea": { - "url": "http://localhost:8080/sse" + "url": "http://localhost:8080/sse", + "headers": { + "Authorization": "Bearer " + } } } } @@ -151,7 +154,10 @@ To configure the MCP server for Gitea, add the following to your MCP configurati { "mcpServers": { "gitea": { - "url": "http://localhost:8080/mcp" + "url": "http://localhost:8080/mcp", + "headers": { + "Authorization": "Bearer " + } } } } diff --git a/README.zh-cn.md b/README.zh-cn.md index f6da55e..af3d111 100644 --- a/README.zh-cn.md +++ b/README.zh-cn.md @@ -139,7 +139,10 @@ cp gitea-mcp /usr/local/bin/ { "mcpServers": { "gitea": { - "url": "http://localhost:8080/sse" + "url": "http://localhost:8080/sse", + "headers": { + "Authorization": "Bearer " + } } } } @@ -151,7 +154,10 @@ cp gitea-mcp /usr/local/bin/ { "mcpServers": { "gitea": { - "url": "http://localhost:8080/mcp" + "url": "http://localhost:8080/mcp", + "headers": { + "Authorization": "Bearer " + } } } } diff --git a/README.zh-tw.md b/README.zh-tw.md index 5c5d61a..8d26b8c 100644 --- a/README.zh-tw.md +++ b/README.zh-tw.md @@ -139,7 +139,10 @@ cp gitea-mcp /usr/local/bin/ { "mcpServers": { "gitea": { - "url": "http://localhost:8080/sse" + "url": "http://localhost:8080/sse", + "headers": { + "Authorization": "Bearer " + } } } } @@ -151,7 +154,10 @@ cp gitea-mcp /usr/local/bin/ { "mcpServers": { "gitea": { - "url": "http://localhost:8080/mcp" + "url": "http://localhost:8080/mcp", + "headers": { + "Authorization": "Bearer " + } } } } diff --git a/operation/issue/issue.go b/operation/issue/issue.go index 82d81ad..fd40d62 100644 --- a/operation/issue/issue.go +++ b/operation/issue/issue.go @@ -140,7 +140,11 @@ func GetIssueByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT if !ok { return to.ErrorResult(fmt.Errorf("index is required")) } - issue, _, err := gitea.Client().GetIssue(owner, repo, int64(index)) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + issue, _, err := client.GetIssue(owner, repo, int64(index)) if err != nil { return to.ErrorResult(fmt.Errorf("get %v/%v/issue/%v err: %v", owner, repo, int64(index), err)) } @@ -177,7 +181,11 @@ func ListRepoIssuesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo PageSize: int(pageSize), }, } - issues, _, err := gitea.Client().ListRepoIssues(owner, repo, opt) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + issues, _, err := client.ListRepoIssues(owner, repo, opt) if err != nil { return to.ErrorResult(fmt.Errorf("get %v/%v/issues err: %v", owner, repo, err)) } @@ -202,7 +210,11 @@ func CreateIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolR if !ok { return to.ErrorResult(fmt.Errorf("body is required")) } - issue, _, err := gitea.Client().CreateIssue(owner, repo, gitea_sdk.CreateIssueOption{ + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + issue, _, err := client.CreateIssue(owner, repo, gitea_sdk.CreateIssueOption{ Title: title, Body: body, }) @@ -234,7 +246,11 @@ func CreateIssueCommentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca opt := gitea_sdk.CreateIssueCommentOption{ Body: body, } - issueComment, _, err := gitea.Client().CreateIssueComment(owner, repo, int64(index), opt) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + issueComment, _, err := client.CreateIssueComment(owner, repo, int64(index), opt) if err != nil { return to.ErrorResult(fmt.Errorf("create %v/%v/issue/%v/comment err: %v", owner, repo, int64(index), err)) } @@ -280,7 +296,11 @@ func EditIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRes opt.State = ptr.To(gitea_sdk.StateType(state)) } - issue, _, err := gitea.Client().EditIssue(owner, repo, int64(index), opt) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + issue, _, err := client.EditIssue(owner, repo, int64(index), opt) if err != nil { return to.ErrorResult(fmt.Errorf("edit %v/%v/issue/%v err: %v", owner, repo, int64(index), err)) } @@ -309,7 +329,11 @@ func EditIssueCommentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call opt := gitea_sdk.EditIssueCommentOption{ Body: body, } - issueComment, _, err := gitea.Client().EditIssueComment(owner, repo, int64(commentID), opt) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + issueComment, _, err := client.EditIssueComment(owner, repo, int64(commentID), opt) if err != nil { return to.ErrorResult(fmt.Errorf("edit %v/%v/issues/comments/%v err: %v", owner, repo, int64(commentID), err)) } @@ -332,7 +356,11 @@ func GetIssueCommentsByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*m return to.ErrorResult(fmt.Errorf("index is required")) } opt := gitea_sdk.ListIssueCommentOptions{} - issue, _, err := gitea.Client().ListIssueComments(owner, repo, int64(index), opt) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + issue, _, err := client.ListIssueComments(owner, repo, int64(index), opt) if err != nil { return to.ErrorResult(fmt.Errorf("get %v/%v/issues/%v/comments err: %v", owner, repo, int64(index), err)) } diff --git a/operation/label/label.go b/operation/label/label.go index 8b167e3..0954508 100644 --- a/operation/label/label.go +++ b/operation/label/label.go @@ -176,7 +176,11 @@ func ListRepoLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo PageSize: int(pageSize), }, } - labels, _, err := gitea.Client().ListRepoLabels(owner, repo, opt) + 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)) } @@ -198,7 +202,11 @@ func GetRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool return to.ErrorResult(fmt.Errorf("label ID is required")) } - label, _, err := gitea.Client().GetRepoLabel(owner, repo, int64(id)) + 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)) } @@ -231,7 +239,11 @@ func CreateRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT Description: description, } - label, _, err := gitea.Client().CreateLabel(owner, repo, opt) + 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)) } @@ -264,7 +276,11 @@ func EditRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo opt.Description = ptr.To(description) } - label, _, err := gitea.Client().EditLabel(owner, repo, int64(id), opt) + 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)) } @@ -286,7 +302,11 @@ func DeleteRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT return to.ErrorResult(fmt.Errorf("label ID is required")) } - _, err := gitea.Client().DeleteLabel(owner, repo, int64(id)) + 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)) } @@ -324,7 +344,11 @@ func AddIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo Labels: labels, } - issueLabels, _, err := gitea.Client().AddIssueLabels(owner, repo, int64(index), opt) + 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)) } @@ -362,7 +386,11 @@ func ReplaceIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca Labels: labels, } - issueLabels, _, err := gitea.Client().ReplaceIssueLabels(owner, repo, int64(index), opt) + 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)) } @@ -384,7 +412,11 @@ func ClearIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call return to.ErrorResult(fmt.Errorf("issue index is required")) } - _, err := gitea.Client().ClearIssueLabels(owner, repo, int64(index)) + 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)) } @@ -410,7 +442,11 @@ func RemoveIssueLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call return to.ErrorResult(fmt.Errorf("label ID is required")) } - _, err := gitea.Client().DeleteIssueLabel(owner, repo, int64(index), int64(labelID)) + 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)) } diff --git a/operation/operation.go b/operation/operation.go index 1de5e3f..ccc4e94 100644 --- a/operation/operation.go +++ b/operation/operation.go @@ -1,7 +1,10 @@ package operation import ( + "context" "fmt" + "net/http" + "strings" "time" "gitea.com/gitea/gitea-mcp/operation/issue" @@ -11,6 +14,7 @@ import ( "gitea.com/gitea/gitea-mcp/operation/search" "gitea.com/gitea/gitea-mcp/operation/user" "gitea.com/gitea/gitea-mcp/operation/version" + mcpContext "gitea.com/gitea/gitea-mcp/pkg/context" "gitea.com/gitea/gitea-mcp/pkg/flag" "gitea.com/gitea/gitea-mcp/pkg/log" @@ -44,6 +48,20 @@ func RegisterTool(s *server.MCPServer) { s.DeleteTools("") } +func getContextWithToken(ctx context.Context, r *http.Request) context.Context { + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + return ctx + } + + parts := strings.Split(authHeader, " ") + if len(parts) != 2 || parts[0] != "Bearer" { + return ctx + } + + return context.WithValue(ctx, mcpContext.TokenContextKey, parts[1]) +} + func Run() error { mcpServer = newMCPServer(flag.Version) RegisterTool(mcpServer) @@ -57,6 +75,7 @@ func Run() error { case "sse": sseServer := server.NewSSEServer( mcpServer, + server.WithSSEContextFunc(getContextWithToken), ) log.Infof("Gitea MCP SSE server listening on :%d", flag.Port) if err := sseServer.Start(fmt.Sprintf(":%d", flag.Port)); err != nil { @@ -67,7 +86,7 @@ func Run() error { mcpServer, server.WithLogger(log.New()), server.WithHeartbeatInterval(30*time.Second), - server.WithStateLess(true), + server.WithHTTPContextFunc(getContextWithToken), ) log.Infof("Gitea MCP HTTP server listening on :%d", flag.Port) if err := httpServer.Start(fmt.Sprintf(":%d", flag.Port)); err != nil { diff --git a/operation/pull/pull.go b/operation/pull/pull.go index 4b90682..85f151b 100644 --- a/operation/pull/pull.go +++ b/operation/pull/pull.go @@ -84,7 +84,11 @@ func GetPullRequestByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp if !ok { return to.ErrorResult(fmt.Errorf("index is required")) } - pr, _, err := gitea.Client().GetPullRequest(owner, repo, int64(index)) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + pr, _, err := client.GetPullRequest(owner, repo, int64(index)) if err != nil { return to.ErrorResult(fmt.Errorf("get %v/%v/pr/%v err: %v", owner, repo, int64(index), err)) } @@ -125,7 +129,11 @@ func ListRepoPullRequestsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp. PageSize: int(pageSize), }, } - pullRequests, _, err := gitea.Client().ListRepoPullRequests(owner, repo, opt) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + pullRequests, _, err := client.ListRepoPullRequests(owner, repo, opt) if err != nil { return to.ErrorResult(fmt.Errorf("list %v/%v/pull_requests err: %v", owner, repo, err)) } @@ -159,7 +167,11 @@ func CreatePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Cal if !ok { return to.ErrorResult(fmt.Errorf("base is required")) } - pr, _, err := gitea.Client().CreatePullRequest(owner, repo, gitea_sdk.CreatePullRequestOption{ + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + pr, _, err := client.CreatePullRequest(owner, repo, gitea_sdk.CreatePullRequestOption{ Title: title, Body: body, Head: head, diff --git a/operation/repo/branch.go b/operation/repo/branch.go index d71cc35..1680915 100644 --- a/operation/repo/branch.go +++ b/operation/repo/branch.go @@ -76,7 +76,11 @@ func CreateBranchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool } oldBranch, _ := req.GetArguments()["old_branch"].(string) - _, _, err := gitea.Client().CreateBranch(owner, repo, gitea_sdk.CreateBranchOption{ + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, _, err = client.CreateBranch(owner, repo, gitea_sdk.CreateBranchOption{ BranchName: branch, OldBranchName: oldBranch, }) @@ -101,7 +105,11 @@ func DeleteBranchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool if !ok { return to.ErrorResult(fmt.Errorf("branch is required")) } - _, _, err := gitea.Client().DeleteRepoBranch(owner, repo, branch) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, _, err = client.DeleteRepoBranch(owner, repo, branch) if err != nil { return to.ErrorResult(fmt.Errorf("delete branch error: %v", err)) } @@ -125,7 +133,11 @@ func ListBranchesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool PageSize: 100, }, } - branches, _, err := gitea.Client().ListRepoBranches(owner, repo, opt) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + branches, _, err := client.ListRepoBranches(owner, repo, opt) if err != nil { return to.ErrorResult(fmt.Errorf("list branches error: %v", err)) } diff --git a/operation/repo/commit.go b/operation/repo/commit.go index 048b767..1aa61e5 100644 --- a/operation/repo/commit.go +++ b/operation/repo/commit.go @@ -63,7 +63,11 @@ func ListRepoCommitsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT SHA: sha, Path: path, } - commits, _, err := gitea.Client().ListRepoCommits(owner, repo, opt) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + commits, _, err := client.ListRepoCommits(owner, repo, opt) if err != nil { return to.ErrorResult(fmt.Errorf("list repo commits err: %v", err)) } diff --git a/operation/repo/file.go b/operation/repo/file.go index 60a8998..01465df 100644 --- a/operation/repo/file.go +++ b/operation/repo/file.go @@ -124,7 +124,11 @@ func GetFileContentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo if !ok { return to.ErrorResult(fmt.Errorf("filePath is required")) } - content, _, err := gitea.Client().GetContents(owner, repo, ref, filePath) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + content, _, err := client.GetContents(owner, repo, ref, filePath) if err != nil { return to.ErrorResult(fmt.Errorf("get file err: %v", err)) } @@ -184,7 +188,11 @@ func GetDirContentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo if !ok { return to.ErrorResult(fmt.Errorf("filePath is required")) } - content, _, err := gitea.Client().ListContents(owner, repo, ref, filePath) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + content, _, err := client.ListContents(owner, repo, ref, filePath) if err != nil { return to.ErrorResult(fmt.Errorf("get dir content err: %v", err)) } @@ -216,7 +224,11 @@ func CreateFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe }, } - _, _, err := gitea.Client().CreateFile(owner, repo, filePath, opt) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, _, err = client.CreateFile(owner, repo, filePath, opt) if err != nil { return to.ErrorResult(fmt.Errorf("create file err: %v", err)) } @@ -253,7 +265,11 @@ func UpdateFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe BranchName: branchName, }, } - _, _, err := gitea.Client().UpdateFile(owner, repo, filePath, opt) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, _, err = client.UpdateFile(owner, repo, filePath, opt) if err != nil { return to.ErrorResult(fmt.Errorf("update file err: %v", err)) } @@ -287,7 +303,11 @@ func DeleteFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe }, SHA: sha, } - _, err := gitea.Client().DeleteFile(owner, repo, filePath, opt) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.DeleteFile(owner, repo, filePath, opt) if err != nil { return to.ErrorResult(fmt.Errorf("delete file err: %v", err)) } diff --git a/operation/repo/release.go b/operation/repo/release.go index 1167d08..7bdf4b5 100644 --- a/operation/repo/release.go +++ b/operation/repo/release.go @@ -134,7 +134,11 @@ func CreateReleaseFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo isPreRelease, _ := req.GetArguments()["is_pre_release"].(bool) body, _ := req.GetArguments()["body"].(string) - _, _, err := gitea.Client().CreateRelease(owner, repo, gitea_sdk.CreateReleaseOption{ + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, _, err = client.CreateRelease(owner, repo, gitea_sdk.CreateReleaseOption{ TagName: tagName, Target: target, Title: title, @@ -164,7 +168,11 @@ func DeleteReleaseFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo return nil, fmt.Errorf("id is required") } - _, err := gitea.Client().DeleteRelease(owner, repo, int64(id)) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.DeleteRelease(owner, repo, int64(id)) if err != nil { return nil, fmt.Errorf("delete release error: %v", err) } @@ -187,7 +195,11 @@ func GetReleaseFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe return nil, fmt.Errorf("id is required") } - release, _, err := gitea.Client().GetRelease(owner, repo, int64(id)) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + release, _, err := client.GetRelease(owner, repo, int64(id)) if err != nil { return nil, fmt.Errorf("get release error: %v", err) } @@ -206,7 +218,11 @@ func GetLatestReleaseFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call return nil, fmt.Errorf("repo is required") } - release, _, err := gitea.Client().GetLatestRelease(owner, repo) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + release, _, err := client.GetLatestRelease(owner, repo) if err != nil { return nil, fmt.Errorf("get latest release error: %v", err) } @@ -237,7 +253,11 @@ func ListReleasesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool page, _ := req.GetArguments()["page"].(float64) pageSize, _ := req.GetArguments()["pageSize"].(float64) - releases, _, err := gitea.Client().ListReleases(owner, repo, gitea_sdk.ListReleasesOptions{ + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + releases, _, err := client.ListReleases(owner, repo, gitea_sdk.ListReleasesOptions{ ListOptions: gitea_sdk.ListOptions{ Page: int(page), PageSize: int(pageSize), diff --git a/operation/repo/repo.go b/operation/repo/repo.go index b6c633a..76b202c 100644 --- a/operation/repo/repo.go +++ b/operation/repo/repo.go @@ -137,14 +137,17 @@ func CreateRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe } var repo *gitea_sdk.Repository - var err error + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } if organization != "" { - repo, _, err = gitea.Client().CreateOrgRepo(organization, opt) + repo, _, err = client.CreateOrgRepo(organization, opt) if err != nil { return to.ErrorResult(fmt.Errorf("create organization repository '%s' in '%s' err: %v", name, organization, err)) } } else { - repo, _, err = gitea.Client().CreateRepo(opt) + repo, _, err = client.CreateRepo(opt) if err != nil { return to.ErrorResult(fmt.Errorf("create repository '%s' err: %v", name, err)) } @@ -176,7 +179,11 @@ func ForkRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResu Organization: organizationPtr, Name: namePtr, } - _, _, err := gitea.Client().CreateFork(user, repo, opt) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, _, err = client.CreateFork(user, repo, opt) if err != nil { return to.ErrorResult(fmt.Errorf("fork repository error: %v", err)) } @@ -199,7 +206,11 @@ func ListMyReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolR PageSize: int(pageSize), }, } - repos, _, err := gitea.Client().ListMyRepos(opt) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + repos, _, err := client.ListMyRepos(opt) if err != nil { return to.ErrorResult(fmt.Errorf("list my repositories error: %v", err)) } diff --git a/operation/repo/tag.go b/operation/repo/tag.go index b39cf45..02fcb7a 100644 --- a/operation/repo/tag.go +++ b/operation/repo/tag.go @@ -102,7 +102,11 @@ func CreateTagFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRes target, _ := req.GetArguments()["target"].(string) message, _ := req.GetArguments()["message"].(string) - _, _, err := gitea.Client().CreateTag(owner, repo, gitea_sdk.CreateTagOption{ + 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, @@ -129,7 +133,11 @@ func DeleteTagFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRes return nil, fmt.Errorf("tag_name is required") } - _, err := gitea.Client().DeleteTag(owner, repo, tagName) + 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) } @@ -152,7 +160,11 @@ func GetTagFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult return nil, fmt.Errorf("tag_name is required") } - tag, _, err := gitea.Client().GetTag(owner, repo, tagName) + 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) } @@ -173,7 +185,11 @@ func ListTagsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResu page, _ := req.GetArguments()["page"].(float64) pageSize, _ := req.GetArguments()["pageSize"].(float64) - tags, _, err := gitea.Client().ListRepoTags(owner, repo, gitea_sdk.ListRepoTagsOptions{ + 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), diff --git a/operation/search/search.go b/operation/search/search.go index 71e0563..c18d252 100644 --- a/operation/search/search.go +++ b/operation/search/search.go @@ -94,7 +94,11 @@ func SearchUsersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolR PageSize: int(pageSize), }, } - users, _, err := gitea.Client().SearchUsers(opt) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + users, _, err := client.SearchUsers(opt) if err != nil { return to.ErrorResult(fmt.Errorf("search users err: %v", err)) } @@ -128,7 +132,11 @@ func SearchOrgTeamsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo PageSize: int(pageSize), }, } - teams, _, err := gitea.Client().SearchOrgTeams(org, &opt) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + teams, _, err := client.SearchOrgTeams(org, &opt) if err != nil { return to.ErrorResult(fmt.Errorf("search organization teams error: %v", err)) } @@ -178,7 +186,11 @@ func SearchReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolR PageSize: int(pageSize), }, } - repos, _, err := gitea.Client().SearchRepos(opt) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + repos, _, err := client.SearchRepos(opt) if err != nil { return to.ErrorResult(fmt.Errorf("search repos error: %v", err)) } diff --git a/operation/user/user.go b/operation/user/user.go index 5466f8e..acb62f3 100644 --- a/operation/user/user.go +++ b/operation/user/user.go @@ -79,7 +79,11 @@ func getIntArg(req mcp.CallToolRequest, name string, def int) int { // Logs invocation, fetches current user info from gitea, wraps result for MCP. func GetUserInfoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("[User] Called GetUserInfoFn") - user, _, err := gitea.Client().GetMyUserInfo() + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + user, _, err := client.GetMyUserInfo() if err != nil { return to.ErrorResult(fmt.Errorf("get user info err: %v", err)) } @@ -100,7 +104,11 @@ func GetUserOrgsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolR PageSize: pageSize, }, } - orgs, _, err := gitea.Client().ListMyOrgs(opt) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + orgs, _, err := client.ListMyOrgs(opt) if err != nil { return to.ErrorResult(fmt.Errorf("get user orgs err: %v", err)) } diff --git a/pkg/context/context.go b/pkg/context/context.go new file mode 100644 index 0000000..1671037 --- /dev/null +++ b/pkg/context/context.go @@ -0,0 +1,7 @@ +package context + +type contextKey string + +const ( + TokenContextKey = contextKey("token") +) diff --git a/pkg/gitea/gitea.go b/pkg/gitea/gitea.go index 8d3d008..1b7bc9c 100644 --- a/pkg/gitea/gitea.go +++ b/pkg/gitea/gitea.go @@ -1,52 +1,47 @@ package gitea import ( + "context" "crypto/tls" "fmt" "net/http" - "sync" - - "gitea.com/gitea/gitea-mcp/pkg/flag" - "gitea.com/gitea/gitea-mcp/pkg/log" "code.gitea.io/sdk/gitea" + mcpContext "gitea.com/gitea/gitea-mcp/pkg/context" + "gitea.com/gitea/gitea-mcp/pkg/flag" ) -var ( - client *gitea.Client - clientOnce sync.Once -) +func NewClient(token string) (*gitea.Client, error) { + httpClient := &http.Client{ + Transport: http.DefaultTransport, + } -func Client() *gitea.Client { - clientOnce.Do(func() { - var err error - if client != nil { - return + opts := []gitea.ClientOption{ + gitea.SetToken(token), + } + if flag.Insecure { + httpClient.Transport.(*http.Transport).TLSClientConfig = &tls.Config{ + InsecureSkipVerify: true, } + } + opts = append(opts, gitea.SetHTTPClient(httpClient)) + if flag.Debug { + opts = append(opts, gitea.SetDebugMode()) + } + client, err := gitea.NewClient(flag.Host, opts...) + if err != nil { + return nil, fmt.Errorf("create gitea client err: %w", err) + } - httpClient := &http.Client{ - Transport: http.DefaultTransport, - } - - opts := []gitea.ClientOption{ - gitea.SetToken(flag.Token), - } - if flag.Insecure { - httpClient.Transport.(*http.Transport).TLSClientConfig = &tls.Config{ - InsecureSkipVerify: true, - } - } - opts = append(opts, gitea.SetHTTPClient(httpClient)) - if flag.Debug { - opts = append(opts, gitea.SetDebugMode()) - } - client, err = gitea.NewClient(flag.Host, opts...) - if err != nil { - log.Fatalf("create gitea client err: %v", err) - } - - // Set user agent for the client - client.SetUserAgent(fmt.Sprintf("gitea-mcp-server/%s", flag.Version)) - }) - return client + // Set user agent for the client + client.SetUserAgent(fmt.Sprintf("gitea-mcp-server/%s", flag.Version)) + return client, nil +} + +func ClientFromContext(ctx context.Context) (*gitea.Client, error) { + token, ok := ctx.Value(mcpContext.TokenContextKey).(string) + if !ok { + token = flag.Token + } + return NewClient(token) }