Files
github--github-mcp-server/pkg/github/discussions.go
Avinash Sridhar e014779374
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
Unit Tests / build (macos-latest) (push) Has been cancelled
Unit Tests / build (ubuntu-latest) (push) Has been cancelled
Unit Tests / build (windows-latest) (push) Has been cancelled
License Check / license-check (push) Has been cancelled
Lint / lint (push) Has been cancelled
feat: enhance GetRepositoryDiscussions to include comments and comment count
2025-04-13 18:36:57 -04:00

261 lines
8.0 KiB
Go

package github
import (
"context"
"encoding/json"
"fmt"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
"github.com/shurcooL/githubv4"
)
// Comment represents a comment on a GitHub Discussion
type Comment struct {
ID string `json:"id"`
Body string `json:"body"`
CreatedAt string `json:"createdAt"`
Author string `json:"author"`
}
// Discussion represents a GitHub Discussion with its essential fields
type Discussion struct {
ID string `json:"id"`
Number int `json:"number"`
Title string `json:"title"`
Body string `json:"body"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
URL string `json:"url"`
Category string `json:"category"`
Author string `json:"author"`
Locked bool `json:"locked"`
UpvoteCount int `json:"upvoteCount"`
CommentCount int `json:"commentCount"`
Comments []Comment `json:"comments,omitempty"`
}
// GetRepositoryDiscussions creates a tool to fetch discussions from a specific repository.
func GetRepositoryDiscussions(getGraphQLClient GetGraphQLClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("get_repository_discussions",
mcp.WithDescription(t("TOOL_GET_REPOSITORY_DISCUSSIONS_DESCRIPTION", "Get discussions from a specific GitHub repository")),
mcp.WithString("owner",
mcp.Required(),
mcp.Description("Repository owner"),
),
mcp.WithString("repo",
mcp.Required(),
mcp.Description("Repository name"),
),
),
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
owner, err := requiredParam[string](request, "owner")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
repo, err := requiredParam[string](request, "repo")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
categoryId, err := OptionalParam[string](request, "categoryId")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
pagination, err := OptionalPaginationParams(request)
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
// Get GraphQL client
client, err := getGraphQLClient(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get GitHub GraphQL client: %w", err)
}
// Define GraphQL query variables
variables := map[string]interface{}{
"owner": githubv4.String(owner),
"name": githubv4.String(repo),
"first": githubv4.Int(pagination.perPage),
"after": (*githubv4.String)(nil), // For pagination - null means first page
}
// For pagination beyond the first page
// TODO Fix
if pagination.page > 1 {
// We'd need an actual cursor here, but for simplicity we'll compute a rough offset
// In real implementation, you should store and use actual cursor values
cursorStr := githubv4.String(fmt.Sprintf("%d", (pagination.page-1)*pagination.perPage))
variables["after"] = &cursorStr
}
// Define the GraphQL query structure and query string based on whether categoryId is provided
var query struct {
Repository struct {
Discussions struct {
TotalCount int
Nodes []struct {
ID githubv4.ID
Number int
Title string
Body string
CreatedAt githubv4.DateTime
UpdatedAt githubv4.DateTime
URL githubv4.URI
Category struct {
Name string
}
Author struct {
Login string
}
Locked bool
UpvoteCount int
Comments struct {
TotalCount int
Nodes []struct {
ID githubv4.ID
Body string
CreatedAt githubv4.DateTime
Author struct {
Login string
}
}
} `graphql:"comments(first: 10)"`
}
PageInfo struct {
EndCursor githubv4.String
HasNextPage bool
}
} `graphql:"discussions(first: $first, after: $after)"`
} `graphql:"repository(owner: $owner, name: $name)"`
}
// Define a type for the Discussions GraphQL query to avoid duplication
type discussionQueryType struct {
TotalCount int
Nodes []struct {
ID githubv4.ID
Number int
Title string
Body string
CreatedAt githubv4.DateTime
UpdatedAt githubv4.DateTime
URL githubv4.URI
Category struct {
Name string
}
Author struct {
Login string
}
Locked bool
UpvoteCount int
Comments struct {
TotalCount int
Nodes []struct {
ID githubv4.ID
Body string
CreatedAt githubv4.DateTime
Author struct {
Login string
}
}
} `graphql:"comments(first: 10)"`
}
PageInfo struct {
EndCursor githubv4.String
HasNextPage bool
}
}
// Add categoryId to query if it was provided
if categoryId != "" {
variables["categoryId"] = githubv4.ID(categoryId)
// Use a separate query structure that includes the categoryId parameter
var queryWithCategory struct {
Repository struct {
Discussions discussionQueryType `graphql:"discussions(first: $first, after: $after, categoryId: $categoryId)"`
} `graphql:"repository(owner: $owner, name: $name)"`
}
// Execute the query with categoryId
err = client.Query(ctx, &queryWithCategory, variables)
if err != nil {
return nil, fmt.Errorf("failed to query discussions with category: %w", err)
}
// Copy the results to our main query structure
query.Repository.Discussions = queryWithCategory.Repository.Discussions
} else {
// Execute the original query without categoryId
err = client.Query(ctx, &query, variables)
if err != nil {
return nil, fmt.Errorf("failed to query discussions: %w", err)
}
}
// Execute the GraphQL query
err = client.Query(ctx, &query, variables)
if err != nil {
return nil, fmt.Errorf("failed to query discussions: %w", err)
}
// Convert the GraphQL response to our Discussion type
discussions := make([]Discussion, 0, len(query.Repository.Discussions.Nodes))
for _, node := range query.Repository.Discussions.Nodes {
// Process comments for this discussion
comments := make([]Comment, 0, len(node.Comments.Nodes))
for _, commentNode := range node.Comments.Nodes {
comment := Comment{
ID: fmt.Sprintf("%v", commentNode.ID),
Body: commentNode.Body,
CreatedAt: commentNode.CreatedAt.String(),
Author: commentNode.Author.Login,
}
comments = append(comments, comment)
}
discussion := Discussion{
ID: fmt.Sprintf("%v", node.ID),
Number: node.Number,
Title: node.Title,
Body: node.Body,
CreatedAt: node.CreatedAt.String(),
UpdatedAt: node.UpdatedAt.String(),
URL: node.URL.String(),
Category: node.Category.Name,
Author: node.Author.Login,
Locked: node.Locked,
UpvoteCount: node.UpvoteCount,
CommentCount: node.Comments.TotalCount,
Comments: comments,
}
discussions = append(discussions, discussion)
}
// Create the response
result := struct {
TotalCount int `json:"totalCount"`
Discussions []Discussion `json:"discussions"`
HasNextPage bool `json:"hasNextPage"`
EndCursor string `json:"endCursor"`
}{
TotalCount: query.Repository.Discussions.TotalCount,
Discussions: discussions,
HasNextPage: query.Repository.Discussions.PageInfo.HasNextPage,
EndCursor: string(query.Repository.Discussions.PageInfo.EndCursor),
}
// Marshal the result to JSON
r, err := json.Marshal(result)
if err != nil {
return nil, fmt.Errorf("failed to marshal discussions result: %w", err)
}
return mcp.NewToolResultText(string(r)), nil
}
}