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>
199 lines
5.9 KiB
Go
199 lines
5.9 KiB
Go
package search
|
|
|
|
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 (
|
|
SearchUsersToolName = "search_users"
|
|
SearchOrgTeamsToolName = "search_org_teams"
|
|
SearchReposToolName = "search_repos"
|
|
)
|
|
|
|
var (
|
|
SearchUsersTool = mcp.NewTool(
|
|
SearchUsersToolName,
|
|
mcp.WithDescription("search users"),
|
|
mcp.WithString("keyword", mcp.Description("Keyword")),
|
|
mcp.WithNumber("page", mcp.Description("Page"), mcp.DefaultNumber(1)),
|
|
mcp.WithNumber("pageSize", mcp.Description("PageSize"), mcp.DefaultNumber(100)),
|
|
)
|
|
|
|
SearOrgTeamsTool = mcp.NewTool(
|
|
SearchOrgTeamsToolName,
|
|
mcp.WithDescription("search organization teams"),
|
|
mcp.WithString("org", mcp.Description("organization name")),
|
|
mcp.WithString("query", mcp.Description("search organization teams")),
|
|
mcp.WithBoolean("includeDescription", mcp.Description("include description?")),
|
|
mcp.WithNumber("page", mcp.Description("Page"), mcp.DefaultNumber(1)),
|
|
mcp.WithNumber("pageSize", mcp.Description("PageSize"), mcp.DefaultNumber(100)),
|
|
)
|
|
|
|
SearchReposTool = mcp.NewTool(
|
|
SearchReposToolName,
|
|
mcp.WithDescription("search repos"),
|
|
mcp.WithString("keyword", mcp.Description("Keyword")),
|
|
mcp.WithBoolean("keywordIsTopic", mcp.Description("KeywordIsTopic")),
|
|
mcp.WithBoolean("keywordInDescription", mcp.Description("KeywordInDescription")),
|
|
mcp.WithNumber("ownerID", mcp.Description("OwnerID")),
|
|
mcp.WithBoolean("isPrivate", mcp.Description("IsPrivate")),
|
|
mcp.WithBoolean("isArchived", mcp.Description("IsArchived")),
|
|
mcp.WithString("sort", mcp.Description("Sort")),
|
|
mcp.WithString("order", mcp.Description("Order")),
|
|
mcp.WithNumber("page", mcp.Description("Page"), mcp.DefaultNumber(1)),
|
|
mcp.WithNumber("pageSize", mcp.Description("PageSize"), mcp.DefaultNumber(100)),
|
|
)
|
|
)
|
|
|
|
func init() {
|
|
Tool.RegisterRead(server.ServerTool{
|
|
Tool: SearchUsersTool,
|
|
Handler: SearchUsersFn,
|
|
})
|
|
Tool.RegisterRead(server.ServerTool{
|
|
Tool: SearOrgTeamsTool,
|
|
Handler: SearchOrgTeamsFn,
|
|
})
|
|
Tool.RegisterRead(server.ServerTool{
|
|
Tool: SearchReposTool,
|
|
Handler: SearchReposFn,
|
|
})
|
|
}
|
|
|
|
func SearchUsersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called SearchUsersFn")
|
|
keyword, ok := req.GetArguments()["keyword"].(string)
|
|
if !ok {
|
|
return to.ErrorResult(fmt.Errorf("keyword 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.SearchUsersOption{
|
|
KeyWord: keyword,
|
|
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))
|
|
}
|
|
users, _, err := client.SearchUsers(opt)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("search users err: %v", err))
|
|
}
|
|
return to.TextResult(users)
|
|
}
|
|
|
|
func SearchOrgTeamsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called SearchOrgTeamsFn")
|
|
org, ok := req.GetArguments()["org"].(string)
|
|
if !ok {
|
|
return to.ErrorResult(fmt.Errorf("organization is required"))
|
|
}
|
|
query, ok := req.GetArguments()["query"].(string)
|
|
if !ok {
|
|
return to.ErrorResult(fmt.Errorf("query is required"))
|
|
}
|
|
includeDescription, _ := req.GetArguments()["includeDescription"].(bool)
|
|
page, ok := req.GetArguments()["page"].(float64)
|
|
if !ok {
|
|
page = 1
|
|
}
|
|
pageSize, ok := req.GetArguments()["pageSize"].(float64)
|
|
if !ok {
|
|
pageSize = 100
|
|
}
|
|
opt := gitea_sdk.SearchTeamsOptions{
|
|
Query: query,
|
|
IncludeDescription: includeDescription,
|
|
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))
|
|
}
|
|
teams, _, err := client.SearchOrgTeams(org, &opt)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("search organization teams error: %v", err))
|
|
}
|
|
return to.TextResult(teams)
|
|
}
|
|
|
|
func SearchReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called SearchReposFn")
|
|
keyword, ok := req.GetArguments()["keyword"].(string)
|
|
if !ok {
|
|
return to.ErrorResult(fmt.Errorf("keyword is required"))
|
|
}
|
|
keywordIsTopic, _ := req.GetArguments()["keywordIsTopic"].(bool)
|
|
keywordInDescription, _ := req.GetArguments()["keywordInDescription"].(bool)
|
|
ownerID, _ := req.GetArguments()["ownerID"].(float64)
|
|
var pIsPrivate *bool
|
|
isPrivate, ok := req.GetArguments()["isPrivate"].(bool)
|
|
if ok {
|
|
pIsPrivate = ptr.To(isPrivate)
|
|
}
|
|
var pIsArchived *bool
|
|
isArchived, ok := req.GetArguments()["isArchived"].(bool)
|
|
if ok {
|
|
pIsArchived = ptr.To(isArchived)
|
|
}
|
|
sort, _ := req.GetArguments()["sort"].(string)
|
|
order, _ := req.GetArguments()["order"].(string)
|
|
page, ok := req.GetArguments()["page"].(float64)
|
|
if !ok {
|
|
page = 1
|
|
}
|
|
pageSize, ok := req.GetArguments()["pageSize"].(float64)
|
|
if !ok {
|
|
pageSize = 100
|
|
}
|
|
opt := gitea_sdk.SearchRepoOptions{
|
|
Keyword: keyword,
|
|
KeywordIsTopic: keywordIsTopic,
|
|
KeywordInDescription: keywordInDescription,
|
|
OwnerID: int64(ownerID),
|
|
IsPrivate: pIsPrivate,
|
|
IsArchived: pIsArchived,
|
|
Sort: sort,
|
|
Order: order,
|
|
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))
|
|
}
|
|
repos, _, err := client.SearchRepos(opt)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("search repos error: %v", err))
|
|
}
|
|
return to.TextResult(repos)
|
|
}
|