Initial commit: Gitea MCP Server
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
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/params"
|
||||
"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.Required(), mcp.Description("Keyword")),
|
||||
mcp.WithNumber("page", mcp.Description("Page"), mcp.DefaultNumber(1)),
|
||||
mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30)),
|
||||
)
|
||||
|
||||
SearOrgTeamsTool = mcp.NewTool(
|
||||
SearchOrgTeamsToolName,
|
||||
mcp.WithDescription("search organization teams"),
|
||||
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
|
||||
mcp.WithString("query", mcp.Required(), mcp.Description("search organization teams")),
|
||||
mcp.WithBoolean("includeDescription", mcp.Description("include description?")),
|
||||
mcp.WithNumber("page", mcp.Description("Page"), mcp.DefaultNumber(1)),
|
||||
mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30)),
|
||||
)
|
||||
|
||||
SearchReposTool = mcp.NewTool(
|
||||
SearchReposToolName,
|
||||
mcp.WithDescription("search repos"),
|
||||
mcp.WithString("keyword", mcp.Required(), 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("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30)),
|
||||
)
|
||||
)
|
||||
|
||||
func init() {
|
||||
Tool.RegisterRead(server.ServerTool{
|
||||
Tool: SearchUsersTool,
|
||||
Handler: UsersFn,
|
||||
})
|
||||
Tool.RegisterRead(server.ServerTool{
|
||||
Tool: SearOrgTeamsTool,
|
||||
Handler: OrgTeamsFn,
|
||||
})
|
||||
Tool.RegisterRead(server.ServerTool{
|
||||
Tool: SearchReposTool,
|
||||
Handler: ReposFn,
|
||||
})
|
||||
}
|
||||
|
||||
func UsersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called UsersFn")
|
||||
keyword, err := params.GetString(req.GetArguments(), "keyword")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
page, pageSize := params.GetPagination(req.GetArguments(), 30)
|
||||
opt := gitea_sdk.SearchUsersOption{
|
||||
KeyWord: keyword,
|
||||
ListOptions: gitea_sdk.ListOptions{
|
||||
Page: page,
|
||||
PageSize: 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(slimUserDetails(users))
|
||||
}
|
||||
|
||||
func OrgTeamsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called OrgTeamsFn")
|
||||
org, err := params.GetString(req.GetArguments(), "org")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
query, err := params.GetString(req.GetArguments(), "query")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
includeDescription, _ := req.GetArguments()["includeDescription"].(bool)
|
||||
page, pageSize := params.GetPagination(req.GetArguments(), 30)
|
||||
opt := gitea_sdk.SearchTeamsOptions{
|
||||
Query: query,
|
||||
IncludeDescription: includeDescription,
|
||||
ListOptions: gitea_sdk.ListOptions{
|
||||
Page: page,
|
||||
PageSize: 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(slimTeams(teams))
|
||||
}
|
||||
|
||||
func ReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
log.Debugf("Called ReposFn")
|
||||
keyword, err := params.GetString(req.GetArguments(), "keyword")
|
||||
if err != nil {
|
||||
return to.ErrorResult(err)
|
||||
}
|
||||
keywordIsTopic, _ := req.GetArguments()["keywordIsTopic"].(bool)
|
||||
keywordInDescription, _ := req.GetArguments()["keywordInDescription"].(bool)
|
||||
ownerID := params.GetOptionalInt(req.GetArguments(), "ownerID", 0)
|
||||
var pIsPrivate *bool
|
||||
isPrivate, ok := req.GetArguments()["isPrivate"].(bool)
|
||||
if ok {
|
||||
pIsPrivate = new(isPrivate)
|
||||
}
|
||||
var pIsArchived *bool
|
||||
isArchived, ok := req.GetArguments()["isArchived"].(bool)
|
||||
if ok {
|
||||
pIsArchived = new(isArchived)
|
||||
}
|
||||
sort, _ := req.GetArguments()["sort"].(string)
|
||||
order, _ := req.GetArguments()["order"].(string)
|
||||
page, pageSize := params.GetPagination(req.GetArguments(), 30)
|
||||
opt := gitea_sdk.SearchRepoOptions{
|
||||
Keyword: keyword,
|
||||
KeywordIsTopic: keywordIsTopic,
|
||||
KeywordInDescription: keywordInDescription,
|
||||
OwnerID: ownerID,
|
||||
IsPrivate: pIsPrivate,
|
||||
IsArchived: pIsArchived,
|
||||
Sort: sort,
|
||||
Order: order,
|
||||
ListOptions: gitea_sdk.ListOptions{
|
||||
Page: page,
|
||||
PageSize: 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(slimRepos(repos))
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package search
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
func TestSearchToolsRequiredFields(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tool mcp.Tool
|
||||
required []string
|
||||
}{
|
||||
{
|
||||
name: "search_users",
|
||||
tool: SearchUsersTool,
|
||||
required: []string{"keyword"},
|
||||
},
|
||||
{
|
||||
name: "search_org_teams",
|
||||
tool: SearOrgTeamsTool,
|
||||
required: []string{"org", "query"},
|
||||
},
|
||||
{
|
||||
name: "search_repos",
|
||||
tool: SearchReposTool,
|
||||
required: []string{"keyword"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
for _, field := range tt.required {
|
||||
if !slices.Contains(tt.tool.InputSchema.Required, field) {
|
||||
t.Errorf("tool %s: expected %q to be required, got required=%v", tt.name, field, tt.tool.InputSchema.Required)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package search
|
||||
|
||||
import (
|
||||
gitea_sdk "code.gitea.io/sdk/gitea"
|
||||
)
|
||||
|
||||
func slimUserDetail(u *gitea_sdk.User) map[string]any {
|
||||
if u == nil {
|
||||
return nil
|
||||
}
|
||||
return map[string]any{
|
||||
"id": u.ID,
|
||||
"login": u.UserName,
|
||||
"full_name": u.FullName,
|
||||
"email": u.Email,
|
||||
"avatar_url": u.AvatarURL,
|
||||
"html_url": u.HTMLURL,
|
||||
"is_admin": u.IsAdmin,
|
||||
}
|
||||
}
|
||||
|
||||
func slimUserDetails(users []*gitea_sdk.User) []map[string]any {
|
||||
out := make([]map[string]any, 0, len(users))
|
||||
for _, u := range users {
|
||||
out = append(out, slimUserDetail(u))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func slimTeam(t *gitea_sdk.Team) map[string]any {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
return map[string]any{
|
||||
"id": t.ID,
|
||||
"name": t.Name,
|
||||
"description": t.Description,
|
||||
"permission": t.Permission,
|
||||
}
|
||||
}
|
||||
|
||||
func slimTeams(teams []*gitea_sdk.Team) []map[string]any {
|
||||
out := make([]map[string]any, 0, len(teams))
|
||||
for _, t := range teams {
|
||||
out = append(out, slimTeam(t))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func slimRepo(r *gitea_sdk.Repository) map[string]any {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
m := map[string]any{
|
||||
"id": r.ID,
|
||||
"full_name": r.FullName,
|
||||
"description": r.Description,
|
||||
"html_url": r.HTMLURL,
|
||||
"clone_url": r.CloneURL,
|
||||
"ssh_url": r.SSHURL,
|
||||
"default_branch": r.DefaultBranch,
|
||||
"private": r.Private,
|
||||
"fork": r.Fork,
|
||||
"archived": r.Archived,
|
||||
"language": r.Language,
|
||||
"stars_count": r.Stars,
|
||||
"forks_count": r.Forks,
|
||||
"open_issues_count": r.OpenIssues,
|
||||
"open_pr_counter": r.OpenPulls,
|
||||
"created_at": r.Created,
|
||||
"updated_at": r.Updated,
|
||||
}
|
||||
if r.Owner != nil {
|
||||
m["owner"] = r.Owner.UserName
|
||||
}
|
||||
if len(r.Topics) > 0 {
|
||||
m["topics"] = r.Topics
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func slimRepos(repos []*gitea_sdk.Repository) []map[string]any {
|
||||
out := make([]map[string]any, 0, len(repos))
|
||||
for _, r := range repos {
|
||||
out = append(out, slimRepo(r))
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user