package github import ( "context" "encoding/json" "fmt" "io" "github.com/google/go-github/v69/github" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) // getIssue creates a tool to get details of a specific issue in a GitHub repository. func getIssue(client *github.Client) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("get_issue", mcp.WithDescription("Get details of a specific issue in a GitHub repository."), mcp.WithString("owner", mcp.Required(), mcp.Description("The owner of the repository."), ), mcp.WithString("repo", mcp.Required(), mcp.Description("The name of the repository."), ), mcp.WithNumber("issue_number", mcp.Required(), mcp.Description("The number of the issue."), ), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner := request.Params.Arguments["owner"].(string) repo := request.Params.Arguments["repo"].(string) issueNumber := int(request.Params.Arguments["issue_number"].(float64)) issue, resp, err := client.Issues.Get(ctx, owner, repo, issueNumber) if err != nil { return nil, fmt.Errorf("failed to get issue: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != 200 { body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } return mcp.NewToolResultError(fmt.Sprintf("failed to get issue: %s", string(body))), nil } r, err := json.Marshal(issue) if err != nil { return nil, fmt.Errorf("failed to marshal issue: %w", err) } return mcp.NewToolResultText(string(r)), nil } } // addIssueComment creates a tool to add a comment to an issue. func addIssueComment(client *github.Client) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("add_issue_comment", mcp.WithDescription("Add a comment to an issue"), mcp.WithString("owner", mcp.Required(), mcp.Description("Repository owner"), ), mcp.WithString("repo", mcp.Required(), mcp.Description("Repository name"), ), mcp.WithNumber("issue_number", mcp.Required(), mcp.Description("Issue number to comment on"), ), mcp.WithString("body", mcp.Required(), mcp.Description("Comment text"), ), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner := request.Params.Arguments["owner"].(string) repo := request.Params.Arguments["repo"].(string) issueNumber := int(request.Params.Arguments["issue_number"].(float64)) body := request.Params.Arguments["body"].(string) comment := &github.IssueComment{ Body: github.Ptr(body), } createdComment, resp, err := client.Issues.CreateComment(ctx, owner, repo, issueNumber, comment) if err != nil { return nil, fmt.Errorf("failed to create comment: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != 201 { body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } return mcp.NewToolResultError(fmt.Sprintf("failed to create comment: %s", string(body))), nil } r, err := json.Marshal(createdComment) if err != nil { return nil, fmt.Errorf("failed to marshal response: %w", err) } return mcp.NewToolResultText(string(r)), nil } } // searchIssues creates a tool to search for issues and pull requests. func searchIssues(client *github.Client) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("search_issues", mcp.WithDescription("Search for issues and pull requests"), mcp.WithString("q", mcp.Required(), mcp.Description("Search query using GitHub issues search syntax"), ), mcp.WithString("sort", mcp.Description("Sort field (comments, reactions, created, etc.)"), ), mcp.WithString("order", mcp.Description("Sort order ('asc' or 'desc')"), ), mcp.WithNumber("per_page", mcp.Description("Results per page (max 100)"), ), mcp.WithNumber("page", mcp.Description("Page number"), ), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { query := request.Params.Arguments["q"].(string) sort := "" if s, ok := request.Params.Arguments["sort"].(string); ok { sort = s } order := "" if o, ok := request.Params.Arguments["order"].(string); ok { order = o } perPage := 30 if pp, ok := request.Params.Arguments["per_page"].(float64); ok { perPage = int(pp) } page := 1 if p, ok := request.Params.Arguments["page"].(float64); ok { page = int(p) } opts := &github.SearchOptions{ Sort: sort, Order: order, ListOptions: github.ListOptions{ PerPage: perPage, Page: page, }, } result, resp, err := client.Search.Issues(ctx, query, opts) if err != nil { return nil, fmt.Errorf("failed to search issues: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != 200 { body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } return mcp.NewToolResultError(fmt.Sprintf("failed to search issues: %s", string(body))), nil } r, err := json.Marshal(result) if err != nil { return nil, fmt.Errorf("failed to marshal response: %w", err) } return mcp.NewToolResultText(string(r)), nil } }