198 lines
6.6 KiB
Go
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
|
|
}
|