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
261 lines
8.0 KiB
Go
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
|
|
}
|
|
}
|