Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 31 additions & 19 deletions cmd/mcpcurl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,33 @@ var (
}
)

// loadToolsFromServer fetches the schema from the MCP server and adds tool commands.
// This function reduces cognitive complexity by encapsulating the schema loading logic (SonarQube S3776).
func loadToolsFromServer(serverCmd string, prettyPrint bool) {
if serverCmd == "" {
return
}

jsonRequest, err := buildJSONRPCRequest("tools/list", "", nil)
if err != nil {
return
}

response, err := executeServerCommand(serverCmd, jsonRequest)
if err != nil {
return
}

var schemaResp SchemaResponse
if err := json.Unmarshal([]byte(response), &schemaResp); err != nil {
return
}

for _, tool := range schemaResp.Result.Tools {
addCommandFromTool(toolsCmd, &tool, prettyPrint)
}
}

func main() {
rootCmd.AddCommand(schemaCmd)

Expand All @@ -174,25 +201,10 @@ func main() {
_, _ = fmt.Fprintf(os.Stderr, "Error getting pretty flag: %v\n", err)
os.Exit(1)
}
// Get server command
serverCmd, err := rootCmd.Flags().GetString("stdio-server-cmd")
if err == nil && serverCmd != "" {
// Fetch schema from server
jsonRequest, err := buildJSONRPCRequest("tools/list", "", nil)
if err == nil {
response, err := executeServerCommand(serverCmd, jsonRequest)
if err == nil {
// Parse the schema response
var schemaResp SchemaResponse
if err := json.Unmarshal([]byte(response), &schemaResp); err == nil {
// Add all the generated commands as subcommands of tools
for _, tool := range schemaResp.Result.Tools {
addCommandFromTool(toolsCmd, &tool, prettyPrint)
}
}
}
}
}

// Get server command and load tools from server
serverCmd, _ := rootCmd.Flags().GetString("stdio-server-cmd")
loadToolsFromServer(serverCmd, prettyPrint)

// Execute
if err := rootCmd.Execute(); err != nil {
Expand Down
231 changes: 136 additions & 95 deletions pkg/github/discussions.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,120 @@ import (
"github.com/shurcooL/githubv4"
)

// Constants for commonly used strings to avoid duplication (SonarQube S1192)
const (
descRepositoryOwner = "Repository owner"
descRepositoryName = "Repository name"
errFailedToGetGQLClient = "failed to get GitHub GQL client: %v"
categoryLabelFormat = "category:%s"
)

// discussionNode represents a discussion node from GraphQL query results.
// This type is used to reduce cognitive complexity by providing a common structure
// for mapping discussion data to GitHub Issue objects (SonarQube S3776).
type discussionNode struct {
Number int
Title string
URL string
CreatedAt github.Timestamp
CategoryName string
}

// gqlDiscussionNode represents the common structure for GraphQL discussion nodes.
// This type is used to reduce code duplication when mapping query results (SonarQube S1192).
type gqlDiscussionNode struct {
Number githubv4.Int
Title githubv4.String
CreatedAt githubv4.DateTime
Category struct {
Name githubv4.String
} `graphql:"category"`
URL githubv4.String `graphql:"url"`
}

// toDiscussionNode converts a GraphQL discussion node to a discussionNode.
func (n gqlDiscussionNode) toDiscussionNode() discussionNode {
return discussionNode{
Number: int(n.Number),
Title: string(n.Title),
URL: string(n.URL),
CreatedAt: github.Timestamp{Time: n.CreatedAt.Time},
CategoryName: string(n.Category.Name),
}
}

// mapDiscussionNodeToIssue converts a discussionNode to a GitHub Issue object.
func mapDiscussionNodeToIssue(node discussionNode) *github.Issue {
return &github.Issue{
Number: github.Ptr(node.Number),
Title: github.Ptr(node.Title),
HTMLURL: github.Ptr(node.URL),
CreatedAt: &node.CreatedAt,
Labels: []*github.Label{
{
Name: github.Ptr(fmt.Sprintf(categoryLabelFormat, node.CategoryName)),
},
},
}
}

// mapGQLNodesToDiscussionNodes converts a slice of GraphQL nodes to discussionNodes.
func mapGQLNodesToDiscussionNodes(gqlNodes []gqlDiscussionNode) []discussionNode {
nodes := make([]discussionNode, 0, len(gqlNodes))
for _, n := range gqlNodes {
nodes = append(nodes, n.toDiscussionNode())
}
return nodes
}

// fetchDiscussionNodes fetches discussion nodes from GitHub GraphQL API.
// This function reduces cognitive complexity by encapsulating the query logic (SonarQube S3776).
func fetchDiscussionNodes(ctx context.Context, client *githubv4.Client, owner, repo string, categoryID *githubv4.ID) ([]discussionNode, error) {
if categoryID != nil {
return fetchDiscussionsWithCategory(ctx, client, owner, repo, *categoryID)
}
return fetchDiscussionsWithoutCategory(ctx, client, owner, repo)
}

// fetchDiscussionsWithCategory fetches discussions filtered by category.
func fetchDiscussionsWithCategory(ctx context.Context, client *githubv4.Client, owner, repo string, categoryID githubv4.ID) ([]discussionNode, error) {
var query struct {
Repository struct {
Discussions struct {
Nodes []gqlDiscussionNode
} `graphql:"discussions(first: 100, categoryId: $categoryId)"`
} `graphql:"repository(owner: $owner, name: $repo)"`
}
vars := map[string]interface{}{
"owner": githubv4.String(owner),
"repo": githubv4.String(repo),
"categoryId": categoryID,
}
if err := client.Query(ctx, &query, vars); err != nil {
return nil, err
}
return mapGQLNodesToDiscussionNodes(query.Repository.Discussions.Nodes), nil
}

// fetchDiscussionsWithoutCategory fetches all discussions without category filter.
func fetchDiscussionsWithoutCategory(ctx context.Context, client *githubv4.Client, owner, repo string) ([]discussionNode, error) {
var query struct {
Repository struct {
Discussions struct {
Nodes []gqlDiscussionNode
} `graphql:"discussions(first: 100)"`
} `graphql:"repository(owner: $owner, name: $repo)"`
}
vars := map[string]interface{}{
"owner": githubv4.String(owner),
"repo": githubv4.String(repo),
}
if err := client.Query(ctx, &query, vars); err != nil {
return nil, err
}
return mapGQLNodesToDiscussionNodes(query.Repository.Discussions.Nodes), nil
}

func ListDiscussions(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("list_discussions",
mcp.WithDescription(t("TOOL_LIST_DISCUSSIONS_DESCRIPTION", "List discussions for a repository")),
Expand All @@ -22,11 +136,11 @@ func ListDiscussions(getGQLClient GetGQLClientFn, t translations.TranslationHelp
}),
mcp.WithString("owner",
mcp.Required(),
mcp.Description("Repository owner"),
mcp.Description(descRepositoryOwner),
),
mcp.WithString("repo",
mcp.Required(),
mcp.Description("Repository name"),
mcp.Description(descRepositoryName),
),
mcp.WithString("category",
mcp.Description("Optional filter by discussion category ID. If provided, only discussions with this category are listed."),
Expand All @@ -51,7 +165,7 @@ func ListDiscussions(getGQLClient GetGQLClientFn, t translations.TranslationHelp

client, err := getGQLClient(ctx)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil
return mcp.NewToolResultError(fmt.Sprintf(errFailedToGetGQLClient, err)), nil
}

// If category filter is specified, use it as the category ID for server-side filtering
Expand All @@ -61,89 +175,16 @@ func ListDiscussions(getGQLClient GetGQLClientFn, t translations.TranslationHelp
categoryID = &id
}

// Now execute the discussions query
var discussions []*github.Issue
if categoryID != nil {
// Query with category filter (server-side filtering)
var query struct {
Repository struct {
Discussions struct {
Nodes []struct {
Number githubv4.Int
Title githubv4.String
CreatedAt githubv4.DateTime
Category struct {
Name githubv4.String
} `graphql:"category"`
URL githubv4.String `graphql:"url"`
}
} `graphql:"discussions(first: 100, categoryId: $categoryId)"`
} `graphql:"repository(owner: $owner, name: $repo)"`
}
vars := map[string]interface{}{
"owner": githubv4.String(owner),
"repo": githubv4.String(repo),
"categoryId": *categoryID,
}
if err := client.Query(ctx, &query, vars); err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

// Map nodes to GitHub Issue objects
for _, n := range query.Repository.Discussions.Nodes {
di := &github.Issue{
Number: github.Ptr(int(n.Number)),
Title: github.Ptr(string(n.Title)),
HTMLURL: github.Ptr(string(n.URL)),
CreatedAt: &github.Timestamp{Time: n.CreatedAt.Time},
Labels: []*github.Label{
{
Name: github.Ptr(fmt.Sprintf("category:%s", string(n.Category.Name))),
},
},
}
discussions = append(discussions, di)
}
} else {
// Query without category filter
var query struct {
Repository struct {
Discussions struct {
Nodes []struct {
Number githubv4.Int
Title githubv4.String
CreatedAt githubv4.DateTime
Category struct {
Name githubv4.String
} `graphql:"category"`
URL githubv4.String `graphql:"url"`
}
} `graphql:"discussions(first: 100)"`
} `graphql:"repository(owner: $owner, name: $repo)"`
}
vars := map[string]interface{}{
"owner": githubv4.String(owner),
"repo": githubv4.String(repo),
}
if err := client.Query(ctx, &query, vars); err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
// Fetch discussions using the appropriate query based on category filter
nodes, err := fetchDiscussionNodes(ctx, client, owner, repo, categoryID)
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

// Map nodes to GitHub Issue objects
for _, n := range query.Repository.Discussions.Nodes {
di := &github.Issue{
Number: github.Ptr(int(n.Number)),
Title: github.Ptr(string(n.Title)),
HTMLURL: github.Ptr(string(n.URL)),
CreatedAt: &github.Timestamp{Time: n.CreatedAt.Time},
Labels: []*github.Label{
{
Name: github.Ptr(fmt.Sprintf("category:%s", string(n.Category.Name))),
},
},
}
discussions = append(discussions, di)
}
// Map nodes to GitHub Issue objects using helper function
discussions := make([]*github.Issue, 0, len(nodes))
for _, node := range nodes {
discussions = append(discussions, mapDiscussionNodeToIssue(node))
}

// Marshal and return
Expand All @@ -164,11 +205,11 @@ func GetDiscussion(getGQLClient GetGQLClientFn, t translations.TranslationHelper
}),
mcp.WithString("owner",
mcp.Required(),
mcp.Description("Repository owner"),
mcp.Description(descRepositoryOwner),
),
mcp.WithString("repo",
mcp.Required(),
mcp.Description("Repository name"),
mcp.Description(descRepositoryName),
),
mcp.WithNumber("discussionNumber",
mcp.Required(),
Expand All @@ -187,7 +228,7 @@ func GetDiscussion(getGQLClient GetGQLClientFn, t translations.TranslationHelper
}
client, err := getGQLClient(ctx)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil
return mcp.NewToolResultError(fmt.Sprintf(errFailedToGetGQLClient, err)), nil
}

var q struct {
Expand Down Expand Up @@ -221,7 +262,7 @@ func GetDiscussion(getGQLClient GetGQLClientFn, t translations.TranslationHelper
CreatedAt: &github.Timestamp{Time: d.CreatedAt.Time},
Labels: []*github.Label{
{
Name: github.Ptr(fmt.Sprintf("category:%s", string(d.Category.Name))),
Name: github.Ptr(fmt.Sprintf(categoryLabelFormat, string(d.Category.Name))),
},
},
}
Expand All @@ -241,8 +282,8 @@ func GetDiscussionComments(getGQLClient GetGQLClientFn, t translations.Translati
Title: t("TOOL_GET_DISCUSSION_COMMENTS_USER_TITLE", "Get discussion comments"),
ReadOnlyHint: ToBoolPtr(true),
}),
mcp.WithString("owner", mcp.Required(), mcp.Description("Repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("Repository name")),
mcp.WithString("owner", mcp.Required(), mcp.Description(descRepositoryOwner)),
mcp.WithString("repo", mcp.Required(), mcp.Description(descRepositoryName)),
mcp.WithNumber("discussionNumber", mcp.Required(), mcp.Description("Discussion Number")),
),
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
Expand All @@ -258,7 +299,7 @@ func GetDiscussionComments(getGQLClient GetGQLClientFn, t translations.Translati

client, err := getGQLClient(ctx)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil
return mcp.NewToolResultError(fmt.Sprintf(errFailedToGetGQLClient, err)), nil
}

var q struct {
Expand Down Expand Up @@ -303,11 +344,11 @@ func ListDiscussionCategories(getGQLClient GetGQLClientFn, t translations.Transl
}),
mcp.WithString("owner",
mcp.Required(),
mcp.Description("Repository owner"),
mcp.Description(descRepositoryOwner),
),
mcp.WithString("repo",
mcp.Required(),
mcp.Description("Repository name"),
mcp.Description(descRepositoryName),
),
mcp.WithNumber("first",
mcp.Description("Number of categories to return per page (min 1, max 100)"),
Expand Down Expand Up @@ -356,7 +397,7 @@ func ListDiscussionCategories(getGQLClient GetGQLClientFn, t translations.Transl

client, err := getGQLClient(ctx)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil
return mcp.NewToolResultError(fmt.Sprintf(errFailedToGetGQLClient, err)), nil
}
var q struct {
Repository struct {
Expand Down
Loading