Skip to content

Commit ad03f01

Browse files
committed
Simplify list discussions
1 parent 15cb577 commit ad03f01

File tree

3 files changed

+196
-332
lines changed

3 files changed

+196
-332
lines changed

README.md

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -576,17 +576,9 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
576576
- `repo`: Repository name (string, required)
577577

578578
- **list_discussions** - List discussions
579-
- `after`: Cursor for pagination, use the 'after' field from the previous response (string, optional)
580-
- `answered`: Filter by whether discussions have been answered or not (boolean, optional)
581-
- `before`: Cursor for pagination, use the 'before' field from the previous response (string, optional)
582-
- `category`: Category filter (name) (string, optional)
583-
- `direction`: Sort direction (string, optional)
584-
- `first`: Number of discussions to return per page (min 1, max 100) (number, optional)
585-
- `last`: Number of discussions to return from the end (min 1, max 100) (number, optional)
579+
- `category`: Optional filter by discussion category ID. If provided, only discussions with this category are listed. (string, optional)
586580
- `owner`: Repository owner (string, required)
587581
- `repo`: Repository name (string, required)
588-
- `since`: Filter by date (ISO 8601 timestamp) (string, optional)
589-
- `sort`: Sort field (string, optional)
590582

591583
</details>
592584

pkg/github/discussions.go

Lines changed: 108 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7-
"time"
87

98
"github.com/github/github-mcp-server/pkg/translations"
109
"github.com/go-viper/mapstructure/v2"
@@ -80,142 +79,121 @@ func ListDiscussions(getGQLClient GetGQLClientFn, t translations.TranslationHelp
8079
mcp.Description("Repository name"),
8180
),
8281
mcp.WithString("category",
83-
mcp.Description("Category filter (name)"),
84-
),
85-
mcp.WithString("since",
86-
mcp.Description("Filter by date (ISO 8601 timestamp)"),
87-
),
88-
mcp.WithString("sort",
89-
mcp.Description("Sort field"),
90-
mcp.DefaultString("CREATED_AT"),
91-
mcp.Enum("CREATED_AT", "UPDATED_AT"),
92-
),
93-
mcp.WithString("direction",
94-
mcp.Description("Sort direction"),
95-
mcp.DefaultString("DESC"),
96-
mcp.Enum("ASC", "DESC"),
97-
),
98-
mcp.WithNumber("first",
99-
mcp.Description("Number of discussions to return per page (min 1, max 100)"),
100-
mcp.Min(1),
101-
mcp.Max(100),
102-
),
103-
mcp.WithNumber("last",
104-
mcp.Description("Number of discussions to return from the end (min 1, max 100)"),
105-
mcp.Min(1),
106-
mcp.Max(100),
107-
),
108-
mcp.WithString("after",
109-
mcp.Description("Cursor for pagination, use the 'after' field from the previous response"),
110-
),
111-
mcp.WithString("before",
112-
mcp.Description("Cursor for pagination, use the 'before' field from the previous response"),
113-
),
114-
mcp.WithBoolean("answered",
115-
mcp.Description("Filter by whether discussions have been answered or not"),
82+
mcp.Description("Optional filter by discussion category ID. If provided, only discussions with this category are listed."),
11683
),
11784
),
11885
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
119-
// Decode params
120-
var params struct {
121-
Owner string
122-
Repo string
123-
Category string
124-
Since string
125-
Sort string
126-
Direction string
127-
First int32
128-
Last int32
129-
After string
130-
Before string
131-
Answered bool
132-
}
133-
if err := mapstructure.Decode(request.Params.Arguments, &params); err != nil {
86+
// Required params
87+
owner, err := RequiredParam[string](request, "owner")
88+
if err != nil {
13489
return mcp.NewToolResultError(err.Error()), nil
13590
}
136-
if params.First != 0 && params.Last != 0 {
137-
return mcp.NewToolResultError("only one of 'first' or 'last' may be specified"), nil
138-
}
139-
if params.After != "" && params.Before != "" {
140-
return mcp.NewToolResultError("only one of 'after' or 'before' may be specified"), nil
141-
}
142-
if params.After != "" && params.Last != 0 {
143-
return mcp.NewToolResultError("'after' cannot be used with 'last'. Did you mean to use 'before' instead?"), nil
91+
repo, err := RequiredParam[string](request, "repo")
92+
if err != nil {
93+
return mcp.NewToolResultError(err.Error()), nil
14494
}
145-
if params.Before != "" && params.First != 0 {
146-
return mcp.NewToolResultError("'before' cannot be used with 'first'. Did you mean to use 'after' instead?"), nil
95+
96+
// Optional params
97+
category, err := OptionalParam[string](request, "category")
98+
if err != nil {
99+
return mcp.NewToolResultError(err.Error()), nil
147100
}
148-
// Get GraphQL client
101+
149102
client, err := getGQLClient(ctx)
150103
if err != nil {
151104
return mcp.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil
152105
}
153-
// Prepare GraphQL query
154-
var q struct {
155-
Repository struct {
156-
Discussions struct {
157-
Nodes []struct {
158-
Number githubv4.Int
159-
Title githubv4.String
160-
CreatedAt githubv4.DateTime
161-
Category struct {
162-
Name githubv4.String
163-
} `graphql:"category"`
164-
URL githubv4.String `graphql:"url"`
165-
}
166-
} `graphql:"discussions(categoryId: $categoryId, orderBy: {field: $sort, direction: $direction}, first: $first, after: $after, last: $last, before: $before, answered: $answered)"`
167-
} `graphql:"repository(owner: $owner, name: $repo)"`
168-
}
169-
categories, err := GetAllDiscussionCategories(ctx, client, params.Owner, params.Repo)
170-
if err != nil {
171-
return mcp.NewToolResultError(fmt.Sprintf("failed to get discussion categories: %v", err)), nil
172-
}
173-
var categoryID githubv4.ID = categories[params.Category]
174-
if categoryID == "" && params.Category != "" {
175-
return mcp.NewToolResultError(fmt.Sprintf("category '%s' not found", params.Category)), nil
176-
}
177-
// Build query variables
178-
vars := map[string]interface{}{
179-
"owner": githubv4.String(params.Owner),
180-
"repo": githubv4.String(params.Repo),
181-
"categoryId": categoryID,
182-
"sort": githubv4.DiscussionOrderField(params.Sort),
183-
"direction": githubv4.OrderDirection(params.Direction),
184-
"first": githubv4.Int(params.First),
185-
"last": githubv4.Int(params.Last),
186-
"after": githubv4.String(params.After),
187-
"before": githubv4.String(params.Before),
188-
"answered": githubv4.Boolean(params.Answered),
189-
}
190-
// Execute query
191-
if err := client.Query(ctx, &q, vars); err != nil {
192-
return mcp.NewToolResultError(err.Error()), nil
106+
107+
// If category filter is specified, use it as the category ID for server-side filtering
108+
var categoryID *githubv4.ID
109+
if category != "" {
110+
id := githubv4.ID(category)
111+
categoryID = &id
193112
}
194-
// Map nodes to GitHub Issue objects - there is no discussion type in the GitHub API, so we use Issue to benefit from existing code
113+
114+
// Now execute the discussions query
195115
var discussions []*github.Issue
196-
for _, n := range q.Repository.Discussions.Nodes {
197-
di := &github.Issue{
198-
Number: github.Ptr(int(n.Number)),
199-
Title: github.Ptr(string(n.Title)),
200-
HTMLURL: github.Ptr(string(n.URL)),
201-
CreatedAt: &github.Timestamp{Time: n.CreatedAt.Time},
116+
if categoryID != nil {
117+
// Query with category filter (server-side filtering)
118+
var query struct {
119+
Repository struct {
120+
Discussions struct {
121+
Nodes []struct {
122+
Number githubv4.Int
123+
Title githubv4.String
124+
CreatedAt githubv4.DateTime
125+
Category struct {
126+
Name githubv4.String
127+
} `graphql:"category"`
128+
URL githubv4.String `graphql:"url"`
129+
}
130+
} `graphql:"discussions(first: 100, categoryId: $categoryId)"`
131+
} `graphql:"repository(owner: $owner, name: $repo)"`
132+
}
133+
vars := map[string]interface{}{
134+
"owner": githubv4.String(owner),
135+
"repo": githubv4.String(repo),
136+
"categoryId": *categoryID,
137+
}
138+
if err := client.Query(ctx, &query, vars); err != nil {
139+
return mcp.NewToolResultError(err.Error()), nil
202140
}
203-
discussions = append(discussions, di)
204-
}
205141

206-
// Post filtering discussions based on 'since' parameter
207-
if params.Since != "" {
208-
sinceTime, err := time.Parse(time.RFC3339, params.Since)
209-
if err != nil {
210-
return mcp.NewToolResultError(fmt.Sprintf("invalid 'since' timestamp: %v", err)), nil
142+
// Map nodes to GitHub Issue objects
143+
for _, n := range query.Repository.Discussions.Nodes {
144+
di := &github.Issue{
145+
Number: github.Ptr(int(n.Number)),
146+
Title: github.Ptr(string(n.Title)),
147+
HTMLURL: github.Ptr(string(n.URL)),
148+
CreatedAt: &github.Timestamp{Time: n.CreatedAt.Time},
149+
Labels: []*github.Label{
150+
{
151+
Name: github.Ptr(fmt.Sprintf("category:%s", string(n.Category.Name))),
152+
},
153+
},
154+
}
155+
discussions = append(discussions, di)
156+
}
157+
} else {
158+
// Query without category filter
159+
var query struct {
160+
Repository struct {
161+
Discussions struct {
162+
Nodes []struct {
163+
Number githubv4.Int
164+
Title githubv4.String
165+
CreatedAt githubv4.DateTime
166+
Category struct {
167+
Name githubv4.String
168+
} `graphql:"category"`
169+
URL githubv4.String `graphql:"url"`
170+
}
171+
} `graphql:"discussions(first: 100)"`
172+
} `graphql:"repository(owner: $owner, name: $repo)"`
211173
}
212-
var filteredDiscussions []*github.Issue
213-
for _, d := range discussions {
214-
if d.CreatedAt.Time.After(sinceTime) {
215-
filteredDiscussions = append(filteredDiscussions, d)
174+
vars := map[string]interface{}{
175+
"owner": githubv4.String(owner),
176+
"repo": githubv4.String(repo),
177+
}
178+
if err := client.Query(ctx, &query, vars); err != nil {
179+
return mcp.NewToolResultError(err.Error()), nil
180+
}
181+
182+
// Map nodes to GitHub Issue objects
183+
for _, n := range query.Repository.Discussions.Nodes {
184+
di := &github.Issue{
185+
Number: github.Ptr(int(n.Number)),
186+
Title: github.Ptr(string(n.Title)),
187+
HTMLURL: github.Ptr(string(n.URL)),
188+
CreatedAt: &github.Timestamp{Time: n.CreatedAt.Time},
189+
Labels: []*github.Label{
190+
{
191+
Name: github.Ptr(fmt.Sprintf("category:%s", string(n.Category.Name))),
192+
},
193+
},
216194
}
195+
discussions = append(discussions, di)
217196
}
218-
discussions = filteredDiscussions
219197
}
220198

221199
// Marshal and return
@@ -270,6 +248,9 @@ func GetDiscussion(getGQLClient GetGQLClientFn, t translations.TranslationHelper
270248
State githubv4.String
271249
CreatedAt githubv4.DateTime
272250
URL githubv4.String `graphql:"url"`
251+
Category struct {
252+
Name githubv4.String
253+
} `graphql:"category"`
273254
} `graphql:"discussion(number: $discussionNumber)"`
274255
} `graphql:"repository(owner: $owner, name: $repo)"`
275256
}
@@ -288,6 +269,11 @@ func GetDiscussion(getGQLClient GetGQLClientFn, t translations.TranslationHelper
288269
State: github.Ptr(string(d.State)),
289270
HTMLURL: github.Ptr(string(d.URL)),
290271
CreatedAt: &github.Timestamp{Time: d.CreatedAt.Time},
272+
Labels: []*github.Label{
273+
{
274+
Name: github.Ptr(fmt.Sprintf("category:%s", string(d.Category.Name))),
275+
},
276+
},
291277
}
292278
out, err := json.Marshal(discussion)
293279
if err != nil {
@@ -429,16 +415,12 @@ func ListDiscussionCategories(getGQLClient GetGQLClientFn, t translations.Transl
429415
ID githubv4.ID
430416
Name githubv4.String
431417
}
432-
} `graphql:"discussionCategories(first: $first, last: $last, after: $after, before: $before)"`
418+
} `graphql:"discussionCategories(first: 100)"`
433419
} `graphql:"repository(owner: $owner, name: $repo)"`
434420
}
435421
vars := map[string]interface{}{
436-
"owner": githubv4.String(params.Owner),
437-
"repo": githubv4.String(params.Repo),
438-
"first": githubv4.Int(params.First),
439-
"last": githubv4.Int(params.Last),
440-
"after": githubv4.String(params.After),
441-
"before": githubv4.String(params.Before),
422+
"owner": githubv4.String(params.Owner),
423+
"repo": githubv4.String(params.Repo),
442424
}
443425
if err := client.Query(ctx, &q, vars); err != nil {
444426
return mcp.NewToolResultError(err.Error()), nil

0 commit comments

Comments
 (0)