4 Commits
v0.3.1 ... main

Author SHA1 Message Date
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
hiifong
dc3e120e97 Update operation/repo/file.go 2025-08-29 05:57:44 +00:00
marcluer
f33b04a3df feat: added parameter 'organization' to tool 'create_repo' (#88)
Using the Gitea-mcp server I was missing the ability to create repositories in other organizations. e.g.:
* I was only able to create `https://gitea.domain.com/myuser/repo` 
* I was not able to create `https://gitea.domain.com/organization/repo` 

This feature was planned, implemented and compiled by Claude Code. I have no clue about Golang.

I then took the resulting `gitea-mcp` file and sucessfully tested it on my self-hosted gitea instance:
* Creating `https://gitea.domain.com/myuser/repo` 
* Creating `https://gitea.domain.com/organization/repo` 

Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/88
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: marcluer <gitea@marcluerssen.de>
Co-committed-by: marcluer <gitea@marcluerssen.de>
2025-08-29 05:37:19 +00:00
appleboy
ba07925969 refactor: refactor MCP tool registration and pagination handling (#86)
- Add documentation for MCP tool constants and tool registration
- Use configurable default values for pagination arguments in user organization queries
- Introduce registerTools helper to streamline MCP tool registration
- Refactor pagination argument parsing into a reusable getIntArg function
- Add descriptive logging for tool handler execution
- Improve code organization for defining and registering MCP tools

Signed-off-by: appleboy <appleboy.tw@gmail.com>

Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/86
Co-authored-by: appleboy <appleboy.tw@gmail.com>
Co-committed-by: appleboy <appleboy.tw@gmail.com>
2025-08-23 04:31:57 +00:00
17 changed files with 375 additions and 119 deletions

View File

@@ -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 <your personal access token>"
}
}
}
}
@@ -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 <your personal access token>"
}
}
}
}

View File

@@ -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 <your personal access token>"
}
}
}
}
@@ -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 <your personal access token>"
}
}
}
}

View File

@@ -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 <your personal access token>"
}
}
}
}
@@ -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 <your personal access token>"
}
}
}
}

View File

@@ -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))
}

View File

@@ -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))
}

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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))
}

View File

@@ -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))
}

View File

@@ -64,7 +64,7 @@ var (
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("filePath", mcp.Required(), mcp.Description("file path")),
mcp.WithString("sha", mcp.Required(), mcp.Description("sha is the SHA for the file that already exists")),
mcp.WithString("content", mcp.Required(), mcp.Description("file content, base64 encoded")),
mcp.WithString("content", mcp.Required(), mcp.Description("file content")),
mcp.WithString("message", mcp.Required(), mcp.Description("commit message")),
mcp.WithString("branch_name", mcp.Required(), mcp.Description("branch name")),
)
@@ -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))
}

View File

@@ -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),

View File

@@ -27,7 +27,7 @@ const (
var (
CreateRepoTool = mcp.NewTool(
CreateRepoToolName,
mcp.WithDescription("Create repository"),
mcp.WithDescription("Create repository in personal account or organization"),
mcp.WithString("name", mcp.Required(), mcp.Description("Name of the repository to create")),
mcp.WithString("description", mcp.Description("Description of the repository to create")),
mcp.WithBoolean("private", mcp.Description("Whether the repository is private")),
@@ -38,6 +38,7 @@ var (
mcp.WithString("license", mcp.Description("License to use")),
mcp.WithString("readme", mcp.Description("Readme of the repository to create")),
mcp.WithString("default_branch", mcp.Description("DefaultBranch of the repository (used when initializes and in template)")),
mcp.WithString("organization", mcp.Description("Organization name to create repository in (optional - defaults to personal account)")),
)
ForkRepoTool = mcp.NewTool(
@@ -120,6 +121,7 @@ func CreateRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe
license, _ := req.GetArguments()["license"].(string)
readme, _ := req.GetArguments()["readme"].(string)
defaultBranch, _ := req.GetArguments()["default_branch"].(string)
organization, _ := req.GetArguments()["organization"].(string)
opt := gitea_sdk.CreateRepoOption{
Name: name,
@@ -133,9 +135,22 @@ func CreateRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe
Readme: readme,
DefaultBranch: defaultBranch,
}
repo, _, err := gitea.Client().CreateRepo(opt)
var repo *gitea_sdk.Repository
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("create repo err: %v", err))
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
if organization != "" {
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 = client.CreateRepo(opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("create repository '%s' err: %v", name, err))
}
}
return to.TextResult(repo)
}
@@ -164,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))
}
@@ -187,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))
}

View File

@@ -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),

View File

@@ -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))
}

View File

@@ -15,68 +15,102 @@ import (
)
const (
// GetMyUserInfoToolName is the unique tool name used for MCP registration and lookup of the get_my_user_info command.
GetMyUserInfoToolName = "get_my_user_info"
// GetUserOrgsToolName is the unique tool name used for MCP registration and lookup of the get_user_orgs command.
GetUserOrgsToolName = "get_user_orgs"
// defaultPage is the default starting page number used for paginated organization listings.
defaultPage = 1
// defaultPageSize is the default number of organizations per page for paginated queries.
defaultPageSize = 100
)
// Tool is the MCP tool manager instance for registering all MCP tools in this package.
var Tool = tool.New()
var (
// GetMyUserInfoTool is the MCP tool for retrieving the current user's info.
// It is registered with a specific name and a description string.
GetMyUserInfoTool = mcp.NewTool(
GetMyUserInfoToolName,
mcp.WithDescription("Get my user info"),
)
// GetUserOrgsTool is the MCP tool for listing organizations for the authenticated user.
// It supports pagination via "page" and "pageSize" arguments with default values specified above.
GetUserOrgsTool = mcp.NewTool(
GetUserOrgsToolName,
mcp.WithDescription("Get organizations associated with the authenticated user"),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(defaultPage)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(defaultPageSize)),
)
)
// init registers all MCP tools in Tool at package initialization.
// This function ensures the handler functions are registered before server usage.
func init() {
Tool.RegisterRead(server.ServerTool{
Tool: GetMyUserInfoTool,
Handler: GetUserInfoFn,
})
Tool.RegisterRead(server.ServerTool{
Tool: GetUserOrgsTool,
Handler: GetUserOrgsFn,
})
registerTools()
}
// registerTools registers all local MCP tool definitions and their handler functions.
// To add new functionality, append your tool/handler pair to the tools slice below.
func registerTools() {
tools := []server.ServerTool{
{Tool: GetMyUserInfoTool, Handler: GetUserInfoFn},
{Tool: GetUserOrgsTool, Handler: GetUserOrgsFn},
}
for _, t := range tools {
Tool.RegisterRead(t)
}
}
// getIntArg parses an integer argument from the MCP request arguments map.
// Returns def if missing, not a number, or less than 1. Used for pagination arguments.
func getIntArg(req mcp.CallToolRequest, name string, def int) int {
val, ok := req.GetArguments()[name].(float64)
if !ok || val < 1 {
return def
}
return int(val)
}
// GetUserInfoFn is the handler for "get_my_user_info" MCP tool requests.
// 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("Called GetUserInfoFn")
user, _, err := gitea.Client().GetMyUserInfo()
log.Debugf("[User] Called GetUserInfoFn")
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))
}
return to.TextResult(user)
}
// GetUserOrgsFn is the handler for "get_user_orgs" MCP tool requests.
// Logs invocation, pulls validated pagination arguments from request,
// performs Gitea organization listing, and wraps the result for MCP.
func GetUserOrgsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetUserOrgsFn")
page, ok := req.GetArguments()["page"].(float64)
if !ok || page < 1 {
page = 1
}
pageSize, ok := req.GetArguments()["pageSize"].(float64)
if !ok || pageSize < 1 {
pageSize = 100
}
log.Debugf("[User] Called GetUserOrgsFn")
page := getIntArg(req, "page", defaultPage)
pageSize := getIntArg(req, "pageSize", defaultPageSize)
opt := gitea_sdk.ListOrgsOptions{
ListOptions: gitea_sdk.ListOptions{
Page: int(page),
PageSize: int(pageSize),
Page: page,
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))
}
return to.TextResult(orgs)
}

7
pkg/context/context.go Normal file
View File

@@ -0,0 +1,7 @@
package context
type contextKey string
const (
TokenContextKey = contextKey("token")
)

View File

@@ -1,35 +1,23 @@
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 Client() *gitea.Client {
clientOnce.Do(func() {
var err error
if client != nil {
return
}
func NewClient(token string) (*gitea.Client, error) {
httpClient := &http.Client{
Transport: http.DefaultTransport,
}
opts := []gitea.ClientOption{
gitea.SetToken(flag.Token),
gitea.SetToken(token),
}
if flag.Insecure {
httpClient.Transport.(*http.Transport).TLSClientConfig = &tls.Config{
@@ -40,13 +28,20 @@ func Client() *gitea.Client {
if flag.Debug {
opts = append(opts, gitea.SetDebugMode())
}
client, err = gitea.NewClient(flag.Host, opts...)
client, err := gitea.NewClient(flag.Host, opts...)
if err != nil {
log.Fatalf("create gitea client err: %v", err)
return nil, fmt.Errorf("create gitea client err: %w", err)
}
// Set user agent for the client
client.SetUserAgent(fmt.Sprintf("gitea-mcp-server/%s", flag.Version))
})
return client
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)
}