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+
1682func 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