Skip to content

Commit c1afb88

Browse files
committed
refactor to map to most suitable query based on user inputs at runtime
1 parent 5d7230d commit c1afb88

File tree

1 file changed

+144
-125
lines changed

1 file changed

+144
-125
lines changed

pkg/github/discussions.go

Lines changed: 144 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"log"
78

89
"github.com/github/github-mcp-server/pkg/translations"
910
"github.com/go-viper/mapstructure/v2"
@@ -13,6 +14,71 @@ import (
1314
"github.com/shurcooL/githubv4"
1415
)
1516

17+
// Define reusable fragments for discussions
18+
type DiscussionFragment struct {
19+
Number githubv4.Int
20+
Title githubv4.String
21+
CreatedAt githubv4.DateTime
22+
UpdatedAt githubv4.DateTime
23+
Author struct {
24+
Login githubv4.String
25+
}
26+
Category struct {
27+
Name githubv4.String
28+
} `graphql:"category"`
29+
URL githubv4.String `graphql:"url"`
30+
}
31+
32+
type discussionQueries struct {
33+
BasicNoOrder struct {
34+
Repository struct {
35+
Discussions struct {
36+
Nodes []DiscussionFragment
37+
} `graphql:"discussions(first: 100)"`
38+
} `graphql:"repository(owner: $owner, name: $repo)"`
39+
}
40+
41+
BasicWithOrder struct {
42+
Repository struct {
43+
Discussions struct {
44+
Nodes []DiscussionFragment
45+
} `graphql:"discussions(first: 100, orderBy: $orderBy)"`
46+
} `graphql:"repository(owner: $owner, name: $repo)"`
47+
}
48+
49+
WithCategoryAndOrder struct {
50+
Repository struct {
51+
Discussions struct {
52+
Nodes []DiscussionFragment
53+
} `graphql:"discussions(first: 100, categoryId: $categoryId, orderBy: $orderBy)"`
54+
} `graphql:"repository(owner: $owner, name: $repo)"`
55+
}
56+
57+
WithCategoryNoOrder struct {
58+
Repository struct {
59+
Discussions struct {
60+
Nodes []DiscussionFragment
61+
} `graphql:"discussions(first: 100, categoryId: $categoryId)"`
62+
} `graphql:"repository(owner: $owner, name: $repo)"`
63+
}
64+
}
65+
66+
func fragmentToDiscussion(fragment DiscussionFragment) *github.Discussion {
67+
return &github.Discussion{
68+
Number: github.Ptr(int(fragment.Number)),
69+
Title: github.Ptr(string(fragment.Title)),
70+
HTMLURL: github.Ptr(string(fragment.URL)),
71+
CreatedAt: &github.Timestamp{Time: fragment.CreatedAt.Time},
72+
UpdatedAt: &github.Timestamp{Time: fragment.UpdatedAt.Time},
73+
User: &github.User{
74+
Login: github.Ptr(string(fragment.Author.Login)),
75+
},
76+
DiscussionCategory: &github.DiscussionCategory{
77+
Name: github.Ptr(string(fragment.Category.Name)),
78+
},
79+
}
80+
}
81+
1682
func ListDiscussions(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
1783
return mcp.NewTool("list_discussions",
1884
mcp.WithDescription(t("TOOL_LIST_DISCUSSIONS_DESCRIPTION", "List discussions for a repository")),
@@ -32,16 +98,15 @@ func ListDiscussions(getGQLClient GetGQLClientFn, t translations.TranslationHelp
3298
mcp.Description("Optional filter by discussion category ID. If provided, only discussions with this category are listed."),
3399
),
34100
mcp.WithString("orderBy",
35-
mcp.Description("Order discussions by field"),
101+
mcp.Description("Order discussions by field. If provided, the 'direction' also needs to be provided."),
36102
mcp.Enum("CREATED_AT", "UPDATED_AT"),
37103
),
38104
mcp.WithString("direction",
39-
mcp.Description("Order direction"),
105+
mcp.Description("Order direction."),
40106
mcp.Enum("ASC", "DESC"),
41107
),
42108
),
43109
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
44-
// Required params
45110
owner, err := RequiredParam[string](request, "owner")
46111
if err != nil {
47112
return mcp.NewToolResultError(err.Error()), nil
@@ -51,146 +116,100 @@ func ListDiscussions(getGQLClient GetGQLClientFn, t translations.TranslationHelp
51116
return mcp.NewToolResultError(err.Error()), nil
52117
}
53118

54-
// Optional params
55119
category, err := OptionalParam[string](request, "category")
56120
if err != nil {
57121
return mcp.NewToolResultError(err.Error()), nil
58122
}
59123

60-
client, err := getGQLClient(ctx)
61-
if err != nil {
62-
return mcp.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil
63-
}
64-
65-
// If category filter is specified, use it as the category ID for server-side filtering
66-
var categoryID *githubv4.ID
67-
if category != "" {
68-
id := githubv4.ID(category)
69-
categoryID = &id
70-
}
71-
72124
orderBy, err := OptionalParam[string](request, "orderBy")
73125
if err != nil {
74126
return mcp.NewToolResultError(err.Error()), nil
75127
}
76-
if orderBy == "" {
77-
orderBy = "CREATED_AT"
78-
}
128+
79129
direction, err := OptionalParam[string](request, "direction")
80130
if err != nil {
81131
return mcp.NewToolResultError(err.Error()), nil
82132
}
83-
if direction == "" {
84-
direction = "DESC"
85-
}
86-
87-
// Now execute the discussions query
88-
var discussions []*github.Discussion
89-
if categoryID != nil {
90-
// Query with category filter (server-side filtering)
91-
var query struct {
92-
Repository struct {
93-
Discussions struct {
94-
Nodes []struct {
95-
Number githubv4.Int
96-
Title githubv4.String
97-
CreatedAt githubv4.DateTime
98-
UpdatedAt githubv4.DateTime
99-
Author struct {
100-
Login githubv4.String
101-
}
102-
Category struct {
103-
Name githubv4.String
104-
} `graphql:"category"`
105-
URL githubv4.String `graphql:"url"`
106-
}
107-
} `graphql:"discussions(first: 100, categoryId: $categoryId, orderBy: {field: $orderByField, direction: $direction})"`
108-
} `graphql:"repository(owner: $owner, name: $repo)"`
109-
}
110-
111-
112-
vars := map[string]interface{}{
113-
"owner": githubv4.String(owner),
114-
"repo": githubv4.String(repo),
115-
"categoryId": *categoryID,
116-
"orderByField": githubv4.DiscussionOrderField(orderBy),
117-
"direction": githubv4.OrderDirection(direction),
118-
}
119-
120-
121-
if err := client.Query(ctx, &query, vars); err != nil {
122-
return mcp.NewToolResultError(err.Error()), nil
123-
}
124133

125-
// Map nodes to GitHub Discussion objects
126-
for _, n := range query.Repository.Discussions.Nodes {
127-
di := &github.Discussion{
128-
Number: github.Ptr(int(n.Number)),
129-
Title: github.Ptr(string(n.Title)),
130-
HTMLURL: github.Ptr(string(n.URL)),
131-
CreatedAt: &github.Timestamp{Time: n.CreatedAt.Time},
132-
UpdatedAt: &github.Timestamp{Time: n.UpdatedAt.Time},
133-
User: &github.User{
134-
Login: github.Ptr(string(n.Author.Login)),
135-
},
136-
DiscussionCategory: &github.DiscussionCategory{
137-
Name: github.Ptr(string(n.Category.Name)),
138-
},
139-
}
140-
discussions = append(discussions, di)
141-
}
142-
} else {
143-
// Query without category filter
144-
var query struct {
145-
Repository struct {
146-
Discussions struct {
147-
Nodes []struct {
148-
Number githubv4.Int
149-
Title githubv4.String
150-
CreatedAt githubv4.DateTime
151-
UpdatedAt githubv4.DateTime
152-
Author struct {
153-
Login githubv4.String
154-
}
155-
Category struct {
156-
Name githubv4.String
157-
} `graphql:"category"`
158-
URL githubv4.String `graphql:"url"`
159-
}
160-
} `graphql:"discussions(first: 100, orderBy: {field: $orderByField, direction: $direction})"`
161-
} `graphql:"repository(owner: $owner, name: $repo)"`
162-
}
134+
client, err := getGQLClient(ctx)
135+
if err != nil {
136+
return mcp.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil
137+
}
163138

139+
baseVars := map[string]interface{}{
140+
"owner": githubv4.String(owner),
141+
"repo": githubv4.String(repo),
142+
}
164143

165-
vars := map[string]interface{}{
166-
"owner": githubv4.String(owner),
167-
"repo": githubv4.String(repo),
168-
"orderByField": githubv4.DiscussionOrderField(orderBy),
169-
"direction": githubv4.OrderDirection(direction),
170-
}
144+
// this is an extra check in case the tool description is misinterpreted, because
145+
// we shouldn't use ordering unless both a 'field' and 'direction' are provided
146+
useOrdering := orderBy != "" && direction != ""
147+
if useOrdering {
148+
orderObject := githubv4.DiscussionOrder{
149+
Field: githubv4.DiscussionOrderField(orderBy),
150+
Direction: githubv4.OrderDirection(direction),
151+
}
152+
baseVars["orderBy"] = orderObject
153+
}
171154

172-
if err := client.Query(ctx, &query, vars); err != nil {
173-
return mcp.NewToolResultError(err.Error()), nil
174-
}
155+
var discussions []*github.Discussion
156+
queries := &discussionQueries{}
175157

176-
// Map nodes to GitHub Discussion objects
177-
for _, n := range query.Repository.Discussions.Nodes {
178-
di := &github.Discussion{
179-
Number: github.Ptr(int(n.Number)),
180-
Title: github.Ptr(string(n.Title)),
181-
HTMLURL: github.Ptr(string(n.URL)),
182-
CreatedAt: &github.Timestamp{Time: n.CreatedAt.Time},
183-
UpdatedAt: &github.Timestamp{Time: n.UpdatedAt.Time},
184-
User: &github.User{
185-
Login: github.Ptr(string(n.Author.Login)),
186-
},
187-
DiscussionCategory: &github.DiscussionCategory{
188-
Name: github.Ptr(string(n.Category.Name)),
189-
},
190-
}
191-
discussions = append(discussions, di)
192-
}
193-
}
158+
if category != "" {
159+
vars := make(map[string]interface{})
160+
for k, v := range baseVars {
161+
vars[k] = v
162+
}
163+
vars["categoryId"] = githubv4.ID(category)
164+
165+
if useOrdering {
166+
log.Printf("GraphQL Query with category and order: %+v", queries.WithCategoryAndOrder)
167+
log.Printf("GraphQL Variables: %+v", vars)
168+
169+
if err := client.Query(ctx, &queries.WithCategoryAndOrder, vars); err != nil {
170+
return mcp.NewToolResultError(err.Error()), nil
171+
}
172+
173+
for _, node := range queries.WithCategoryAndOrder.Repository.Discussions.Nodes {
174+
discussions = append(discussions, fragmentToDiscussion(node))
175+
}
176+
} else {
177+
log.Printf("GraphQL Query with category no order: %+v", queries.WithCategoryNoOrder)
178+
log.Printf("GraphQL Variables: %+v", vars)
179+
180+
if err := client.Query(ctx, &queries.WithCategoryNoOrder, vars); err != nil {
181+
return mcp.NewToolResultError(err.Error()), nil
182+
}
183+
184+
for _, node := range queries.WithCategoryNoOrder.Repository.Discussions.Nodes {
185+
discussions = append(discussions, fragmentToDiscussion(node))
186+
}
187+
}
188+
} else {
189+
if useOrdering {
190+
log.Printf("GraphQL Query basic with order: %+v", queries.BasicWithOrder)
191+
log.Printf("GraphQL Variables: %+v", baseVars)
192+
193+
if err := client.Query(ctx, &queries.BasicWithOrder, baseVars); err != nil {
194+
return mcp.NewToolResultError(err.Error()), nil
195+
}
196+
197+
for _, node := range queries.BasicWithOrder.Repository.Discussions.Nodes {
198+
discussions = append(discussions, fragmentToDiscussion(node))
199+
}
200+
} else {
201+
log.Printf("GraphQL Query basic no order: %+v", queries.BasicNoOrder)
202+
log.Printf("GraphQL Variables: %+v", baseVars)
203+
204+
if err := client.Query(ctx, &queries.BasicNoOrder, baseVars); err != nil {
205+
return mcp.NewToolResultError(err.Error()), nil
206+
}
207+
208+
for _, node := range queries.BasicNoOrder.Repository.Discussions.Nodes {
209+
discussions = append(discussions, fragmentToDiscussion(node))
210+
}
211+
}
212+
}
194213

195214
// Marshal and return
196215
out, err := json.Marshal(discussions)

0 commit comments

Comments
 (0)