woodpecker-mcp/internal/tools/repositories.go
2026-08-08 01:37:47 +03:00

198 lines
6.6 KiB
Go

package tools
import (
"context"
"fmt"
"net/url"
"regexp"
"github.com/mark3labs/mcp-go/mcp"
mcpgo "github.com/mark3labs/mcp-go/server"
"gitea.fe.kaien.ru/kaien/woodpecker-mcp/internal/woodpecker"
)
const (
defaultPage int64 = 1
defaultPerPage int64 = 50
maxPerPage int64 = 100
)
var repoNamePattern = regexp.MustCompile(`^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$`)
// repositoryOutput is the client-facing shape of a repository. Field names
// and presence semantics match the original implementation.
type repositoryOutput struct {
ID int64 `json:"id"`
ForgeRemoteID string `json:"forge_remote_id"`
Name string `json:"name"`
Owner string `json:"owner"`
FullName string `json:"full_name"`
DefaultBranch *string `json:"default_branch,omitempty"`
Private bool `json:"private"`
Trusted bool `json:"trusted"`
Active bool `json:"active"`
ConfigFile *string `json:"config_file,omitempty"`
Timeout *int64 `json:"timeout,omitempty"`
ForgeID int64 `json:"forge_id"`
}
func toRepositoryOutput(r woodpecker.Repo) repositoryOutput {
return repositoryOutput{
ID: r.ID,
ForgeRemoteID: r.ForgeRemoteID,
Name: r.Name,
Owner: r.Owner,
FullName: r.FullName,
DefaultBranch: r.DefaultBranch,
Private: r.Private,
Trusted: r.Trusted,
Active: r.Active,
ConfigFile: r.ConfigFile,
Timeout: r.Timeout,
ForgeID: r.ForgeID,
}
}
// registerRepositories registers the repository tools: search_repository,
// get_repository_by_id and list_repositories.
func registerRepositories(s *mcpgo.MCPServer, c *woodpecker.Client) {
tool, handler := searchRepositoryTool(c)
s.AddTool(tool, handler)
tool, handler = getRepositoryByIDTool(c)
s.AddTool(tool, handler)
tool, handler = listRepositoriesTool(c)
s.AddTool(tool, handler)
}
// searchRepositoryTool finds a repository by its full name (owner/repo).
func searchRepositoryTool(c *woodpecker.Client) (mcp.Tool, mcpgo.ToolHandlerFunc) {
tool := newReadOnlyTool("search_repository",
mcp.WithToolTitle("Search Repository"),
mcp.WithDescription(`Find a repository by its full name (owner/repo format). Returns repository details including ID, URLs, and configuration.
If required information is not provided, use the git command or any available tool to get more context about the current commit, the repository, or the author of the commit.`),
mcp.WithString("name",
mcp.Required(),
mcp.Description("Repository full name (e.g., 'microsoft/vscode')"),
mcp.Pattern(repoNamePattern.String()),
),
)
handler := func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args := decodedArguments(req)
name := argString(args, "name")
if !repoNamePattern.MatchString(name) {
return mcp.NewToolResultError(render(map[string]string{
"error": fmt.Sprintf("Invalid repository name format: %s. Expected format: owner/repo", name),
})), nil
}
var repo woodpecker.Repo
if err := c.Get(ctx, "/repos/lookup/"+encodePath(name), &repo); err != nil {
return renderError(err, "repository lookup for "+name), nil
}
return mcp.NewToolResultText(render(map[string]any{
"name": name,
"repository": toRepositoryOutput(repo),
})), nil
}
return tool, handler
}
// getRepositoryByIDTool returns repository details by numeric ID.
func getRepositoryByIDTool(c *woodpecker.Client) (mcp.Tool, mcpgo.ToolHandlerFunc) {
tool := newReadOnlyTool("get_repository_by_id",
mcp.WithToolTitle("Get Repository by ID"),
mcp.WithDescription("Get repository details by ID. Useful when you already have the repo ID from other operations and need repository context."),
mcp.WithInteger("repoId",
mcp.Required(),
mcp.Description("The repository ID"),
mcp.Min(1),
),
)
handler := func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args := decodedArguments(req)
repoID, ok := argInt64(args, "repoId")
if !ok || repoID < 1 {
return mcp.NewToolResultError(render(map[string]string{
"error": "repoId must be a positive integer",
})), nil
}
var repo woodpecker.Repo
if err := c.Get(ctx, "/repos/"+formatInt64(repoID), &repo); err != nil {
return renderError(err, fmt.Sprintf("repository with ID %d", repoID)), nil
}
return mcp.NewToolResultText(render(map[string]any{
"repoId": repoID,
"repository": toRepositoryOutput(repo),
})), nil
}
return tool, handler
}
// listRepositoriesTool lists all repositories with optional filtering.
func listRepositoriesTool(c *woodpecker.Client) (mcp.Tool, mcpgo.ToolHandlerFunc) {
tool := newReadOnlyTool("list_repositories",
mcp.WithToolTitle("List Repositories"),
mcp.WithDescription(`List all repositories with optional filtering. Returns repository metadata including configuration, status, and access details.
If required information is not provided, use the git command or any available tool to get more context about the current commit, the repository, or the author of the commit.`),
mcp.WithInteger("page",
mcp.Description("Page number for pagination"),
mcp.Min(1),
mcp.DefaultNumber(defaultPage),
),
mcp.WithInteger("perPage",
mcp.Description("Number of repositories per page"),
mcp.Min(1),
mcp.Max(maxPerPage),
mcp.DefaultNumber(defaultPerPage),
),
mcp.WithBoolean("active", mcp.Description("Filter by active status")),
mcp.WithBoolean("trusted", mcp.Description("Filter by trusted status")),
)
handler := func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args := decodedArguments(req)
page, err := parseIntParam(args, "page", defaultPage, 1, 1<<31-1)
if err != nil {
return mcp.NewToolResultError(render(map[string]string{"error": err.Error()})), nil
}
perPage, err := parseIntParam(args, "perPage", defaultPerPage, 1, maxPerPage)
if err != nil {
return mcp.NewToolResultError(render(map[string]string{"error": err.Error()})), nil
}
q := url.Values{}
q.Set("page", formatInt64(page))
q.Set("perPage", formatInt64(perPage))
if active, ok := argBool(args, "active"); ok {
q.Set("active", fmt.Sprintf("%t", active))
}
if trusted, ok := argBool(args, "trusted"); ok {
q.Set("trusted", fmt.Sprintf("%t", trusted))
}
var repos []woodpecker.Repo
if err := c.Get(ctx, "/repos?"+q.Encode(), &repos); err != nil {
return renderError(err, "repositories list"), nil
}
output := make([]repositoryOutput, 0, len(repos))
for _, repo := range repos {
output = append(output, toRepositoryOutput(repo))
}
return mcp.NewToolResultText(render(map[string]any{
"repositories": output,
"count": len(output),
})), nil
}
return tool, handler
}