Skip to content

Commit 89e3afd

Browse files
authored
Add support for org-level discussions in list_discussions tool (#775)
* make repo optional, and default to .github when not provided. improve tool description * autogen * update tests * small copy paste error fixes
1 parent d5e1f48 commit 89e3afd

File tree

3 files changed

+85
-6
lines changed

3 files changed

+85
-6
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,7 @@ The following sets of tools are available (all are on by default):
466466
- `orderBy`: Order discussions by field. If provided, the 'direction' also needs to be provided. (string, optional)
467467
- `owner`: Repository owner (string, required)
468468
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
469-
- `repo`: Repository name (string, required)
469+
- `repo`: Repository name. If not provided, discussions will be queried at the organisation level. (string, optional)
470470

471471
</details>
472472

pkg/github/discussions.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ func getQueryType(useOrdering bool, categoryID *githubv4.ID) any {
119119

120120
func ListDiscussions(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
121121
return mcp.NewTool("list_discussions",
122-
mcp.WithDescription(t("TOOL_LIST_DISCUSSIONS_DESCRIPTION", "List discussions for a repository")),
122+
mcp.WithDescription(t("TOOL_LIST_DISCUSSIONS_DESCRIPTION", "List discussions for a repository or organisation.")),
123123
mcp.WithToolAnnotation(mcp.ToolAnnotation{
124124
Title: t("TOOL_LIST_DISCUSSIONS_USER_TITLE", "List discussions"),
125125
ReadOnlyHint: ToBoolPtr(true),
@@ -129,8 +129,7 @@ func ListDiscussions(getGQLClient GetGQLClientFn, t translations.TranslationHelp
129129
mcp.Description("Repository owner"),
130130
),
131131
mcp.WithString("repo",
132-
mcp.Required(),
133-
mcp.Description("Repository name"),
132+
mcp.Description("Repository name. If not provided, discussions will be queried at the organisation level."),
134133
),
135134
mcp.WithString("category",
136135
mcp.Description("Optional filter by discussion category ID. If provided, only discussions with this category are listed."),
@@ -150,10 +149,15 @@ func ListDiscussions(getGQLClient GetGQLClientFn, t translations.TranslationHelp
150149
if err != nil {
151150
return mcp.NewToolResultError(err.Error()), nil
152151
}
153-
repo, err := RequiredParam[string](request, "repo")
152+
repo, err := OptionalParam[string](request, "repo")
154153
if err != nil {
155154
return mcp.NewToolResultError(err.Error()), nil
156155
}
156+
// when not provided, default to the .github repository
157+
// this will query discussions at the organisation level
158+
if repo == "" {
159+
repo = ".github"
160+
}
157161

158162
category, err := OptionalParam[string](request, "category")
159163
if err != nil {

pkg/github/discussions_test.go

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,46 @@ var (
5050
},
5151
}
5252

53+
discussionsOrgLevel = []map[string]any{
54+
{
55+
"number": 1,
56+
"title": "Org Discussion 1 - Community Guidelines",
57+
"createdAt": "2023-01-15T00:00:00Z",
58+
"updatedAt": "2023-01-15T00:00:00Z",
59+
"author": map[string]any{"login": "org-admin"},
60+
"url": "https://github.com/owner/.github/discussions/1",
61+
"category": map[string]any{"name": "Announcements"},
62+
},
63+
{
64+
"number": 2,
65+
"title": "Org Discussion 2 - Roadmap 2023",
66+
"createdAt": "2023-02-20T00:00:00Z",
67+
"updatedAt": "2023-02-20T00:00:00Z",
68+
"author": map[string]any{"login": "org-admin"},
69+
"url": "https://github.com/owner/.github/discussions/2",
70+
"category": map[string]any{"name": "General"},
71+
},
72+
{
73+
"number": 3,
74+
"title": "Org Discussion 3 - Roadmap 2024",
75+
"createdAt": "2023-02-20T00:00:00Z",
76+
"updatedAt": "2023-02-20T00:00:00Z",
77+
"author": map[string]any{"login": "org-admin"},
78+
"url": "https://github.com/owner/.github/discussions/3",
79+
"category": map[string]any{"name": "General"},
80+
},
81+
{
82+
"number": 4,
83+
"title": "Org Discussion 4 - Roadmap 2025",
84+
"createdAt": "2023-02-20T00:00:00Z",
85+
"updatedAt": "2023-02-20T00:00:00Z",
86+
"author": map[string]any{"login": "org-admin"},
87+
"url": "https://github.com/owner/.github/discussions/4",
88+
"category": map[string]any{"name": "General"},
89+
},
90+
91+
}
92+
5393
// Ordered mock responses
5494
discussionsOrderedCreatedAsc = []map[string]any{
5595
discussionsAll[0], // Discussion 1 (created 2023-01-01)
@@ -139,6 +179,22 @@ var (
139179
},
140180
},
141181
})
182+
183+
mockResponseOrgLevel = githubv4mock.DataResponse(map[string]any{
184+
"repository": map[string]any{
185+
"discussions": map[string]any{
186+
"nodes": discussionsOrgLevel,
187+
"pageInfo": map[string]any{
188+
"hasNextPage": false,
189+
"hasPreviousPage": false,
190+
"startCursor": "",
191+
"endCursor": "",
192+
},
193+
"totalCount": 4,
194+
},
195+
},
196+
})
197+
142198
mockErrorRepoNotFound = githubv4mock.ErrorResponse("repository not found")
143199
)
144200

@@ -151,7 +207,7 @@ func Test_ListDiscussions(t *testing.T) {
151207
assert.Contains(t, toolDef.InputSchema.Properties, "repo")
152208
assert.Contains(t, toolDef.InputSchema.Properties, "orderBy")
153209
assert.Contains(t, toolDef.InputSchema.Properties, "direction")
154-
assert.ElementsMatch(t, toolDef.InputSchema.Required, []string{"owner", "repo"})
210+
assert.ElementsMatch(t, toolDef.InputSchema.Required, []string{"owner"})
155211

156212
// Variables matching what GraphQL receives after JSON marshaling/unmarshaling
157213
varsListAll := map[string]interface{}{
@@ -204,6 +260,13 @@ func Test_ListDiscussions(t *testing.T) {
204260
"after": (*string)(nil),
205261
}
206262

263+
varsOrgLevel := map[string]interface{}{
264+
"owner": "owner",
265+
"repo": ".github", // This is what gets set when repo is not provided
266+
"first": float64(30),
267+
"after": (*string)(nil),
268+
}
269+
207270
tests := []struct {
208271
name string
209272
reqParams map[string]interface{}
@@ -314,6 +377,15 @@ func Test_ListDiscussions(t *testing.T) {
314377
expectError: true,
315378
errContains: "repository not found",
316379
},
380+
{
381+
name: "list org-level discussions (no repo provided)",
382+
reqParams: map[string]interface{}{
383+
"owner": "owner",
384+
// repo is not provided, it will default to ".github"
385+
},
386+
expectError: false,
387+
expectedCount: 4,
388+
},
317389
}
318390

319391
// Define the actual query strings that match the implementation
@@ -351,6 +423,9 @@ func Test_ListDiscussions(t *testing.T) {
351423
case "repository not found error":
352424
matcher := githubv4mock.NewQueryMatcher(qBasicNoOrder, varsRepoNotFound, mockErrorRepoNotFound)
353425
httpClient = githubv4mock.NewMockedHTTPClient(matcher)
426+
case "list org-level discussions (no repo provided)":
427+
matcher := githubv4mock.NewQueryMatcher(qBasicNoOrder, varsOrgLevel, mockResponseOrgLevel)
428+
httpClient = githubv4mock.NewMockedHTTPClient(matcher)
354429
}
355430

356431
gqlClient := githubv4.NewClient(httpClient)

0 commit comments

Comments
 (0)