4
4
"context"
5
5
"encoding/json"
6
6
"fmt"
7
+ "log"
7
8
8
9
"github.com/github/github-mcp-server/pkg/translations"
9
10
"github.com/go-viper/mapstructure/v2"
@@ -13,6 +14,71 @@ import (
13
14
"github.com/shurcooL/githubv4"
14
15
)
15
16
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
+
16
82
func ListDiscussions (getGQLClient GetGQLClientFn , t translations.TranslationHelperFunc ) (tool mcp.Tool , handler server.ToolHandlerFunc ) {
17
83
return mcp .NewTool ("list_discussions" ,
18
84
mcp .WithDescription (t ("TOOL_LIST_DISCUSSIONS_DESCRIPTION" , "List discussions for a repository" )),
@@ -32,16 +98,15 @@ func ListDiscussions(getGQLClient GetGQLClientFn, t translations.TranslationHelp
32
98
mcp .Description ("Optional filter by discussion category ID. If provided, only discussions with this category are listed." ),
33
99
),
34
100
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. " ),
36
102
mcp .Enum ("CREATED_AT" , "UPDATED_AT" ),
37
103
),
38
104
mcp .WithString ("direction" ,
39
- mcp .Description ("Order direction" ),
105
+ mcp .Description ("Order direction. " ),
40
106
mcp .Enum ("ASC" , "DESC" ),
41
107
),
42
108
),
43
109
func (ctx context.Context , request mcp.CallToolRequest ) (* mcp.CallToolResult , error ) {
44
- // Required params
45
110
owner , err := RequiredParam [string ](request , "owner" )
46
111
if err != nil {
47
112
return mcp .NewToolResultError (err .Error ()), nil
@@ -51,146 +116,100 @@ func ListDiscussions(getGQLClient GetGQLClientFn, t translations.TranslationHelp
51
116
return mcp .NewToolResultError (err .Error ()), nil
52
117
}
53
118
54
- // Optional params
55
119
category , err := OptionalParam [string ](request , "category" )
56
120
if err != nil {
57
121
return mcp .NewToolResultError (err .Error ()), nil
58
122
}
59
123
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
-
72
124
orderBy , err := OptionalParam [string ](request , "orderBy" )
73
125
if err != nil {
74
126
return mcp .NewToolResultError (err .Error ()), nil
75
127
}
76
- if orderBy == "" {
77
- orderBy = "CREATED_AT"
78
- }
128
+
79
129
direction , err := OptionalParam [string ](request , "direction" )
80
130
if err != nil {
81
131
return mcp .NewToolResultError (err .Error ()), nil
82
132
}
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
- }
124
133
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
+ }
163
138
139
+ baseVars := map [string ]interface {}{
140
+ "owner" : githubv4 .String (owner ),
141
+ "repo" : githubv4 .String (repo ),
142
+ }
164
143
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
+ }
171
154
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 {}
175
157
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
+ }
194
213
195
214
// Marshal and return
196
215
out , err := json .Marshal (discussions )
0 commit comments