// Package connector provides the "search" and "fetch" tools expected by // ChatGPT-style MCP connectors (https://platform.openai.com/docs/mcp), on // top of the same Gitea SDK calls the search and file domains already use. package connector import ( "context" "encoding/base64" "fmt" "gitea.com/gitea/gitea-mcp/pkg/annotation" "gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/params" "gitea.com/gitea/gitea-mcp/pkg/to" "gitea.com/gitea/gitea-mcp/pkg/tool" gitea_sdk "gitea.dev/sdk" "github.com/modelcontextprotocol/go-sdk/mcp" ) // Tool holds the connector tools (scope "connector"): search and fetch. var Tool = tool.New("connector") const ( SearchToolName = "search" FetchToolName = "fetch" ) var ( SearchTool = tool.NewDefinition( SearchToolName, "Search Gitea repositories by keyword. Returns concise entries suitable for ChatGPT-style connectors.", annotation.ReadOnly("Search"), tool.String("query", tool.Required(), tool.Description("search keyword")), tool.String("owner", tool.Description("filter results to repositories owned by this user")), tool.String("org", tool.Description("filter results to repositories owned by this organization")), tool.Number("page", tool.Description(params.PageDesc), tool.Default(1)), tool.Number("per_page", tool.Description(params.PaginationDesc), tool.Default(30)), ) FetchTool = tool.NewDefinition( FetchToolName, "Fetch a file's decoded content and metadata from a Gitea repository. Suitable for ChatGPT-style connectors.", annotation.ReadOnly("Fetch"), tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), tool.String("path", tool.Required()), tool.String("ref", tool.Description("branch, tag, or commit SHA")), ) ) func init() { Tool.RegisterRead(tool.ServerTool{ Tool: SearchTool, Handler: SearchFn, }) Tool.RegisterRead(tool.ServerTool{ Tool: FetchTool, Handler: FetchFn, }) } // SearchFn searches Gitea repositories by keyword, optionally scoped to an // owner or organization, and returns concise entries. func SearchFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { query, err := params.GetString(args, "query") if err != nil { return to.ErrorResult(err) } client, err := gitea.ClientFromContext(ctx) if err != nil { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) } ownerName := params.GetOptionalString(args, "owner", "") if ownerName == "" { ownerName = params.GetOptionalString(args, "org", "") } var ownerID int64 if ownerName != "" { owner, _, err := client.Users.GetUserInfo(ctx, ownerName) if err != nil { return to.ErrorResult(fmt.Errorf("resolve owner %q err: %v", ownerName, err)) } ownerID = owner.ID } page, pageSize := params.GetPagination(args, 30) opt := gitea_sdk.SearchRepoOptions{ Keyword: query, OwnerID: ownerID, ListOptions: gitea_sdk.ListOptions{ Page: page, PageSize: pageSize, }, } repos, _, err := client.Repositories.SearchRepos(ctx, opt) if err != nil { return to.ErrorResult(fmt.Errorf("search repos err: %v", err)) } return to.TextResult(slimSearchResults(repos)) } // FetchFn fetches a file's content and decodes it, returning the decoded // content alongside path, sha, size, and encoding metadata. func FetchFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) { owner, err := params.GetString(args, "owner") if err != nil { return to.ErrorResult(err) } repo, err := params.GetString(args, "repo") if err != nil { return to.ErrorResult(err) } filePath, err := params.GetString(args, "path") if err != nil { return to.ErrorResult(err) } ref := params.GetOptionalString(args, "ref", "") client, err := gitea.ClientFromContext(ctx) if err != nil { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) } content, _, err := client.Repositories.GetContents(ctx, owner, repo, ref, filePath) if err != nil { return to.ErrorResult(fmt.Errorf("get file err: %v", err)) } decoded, err := decodeContent(content) if err != nil { return to.ErrorResult(err) } return to.TextResult(map[string]any{ "path": content.Path, "sha": content.SHA, "size": content.Size, "encoding": stringOrEmpty(content.Encoding), "content": decoded, }) } func decodeContent(c *gitea_sdk.ContentsResponse) (string, error) { if c.Content == nil { return "", nil } raw, err := base64.StdEncoding.DecodeString(*c.Content) if err != nil { return "", fmt.Errorf("decode base64 content err: %v", err) } return string(raw), nil } func stringOrEmpty(s *string) string { if s == nil { return "" } return *s }