🌐 AI搜索 & 代理 主页
Skip to content

Commit 8cb563a

Browse files
authored
Merge branch 'main' into main
2 parents 5221ead + 33a63a0 commit 8cb563a

File tree

3 files changed

+129
-43
lines changed

3 files changed

+129
-43
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,7 @@ The following sets of tools are available (all are on by default):
493493

494494
- **list_discussion_categories** - List discussion categories
495495
- `owner`: Repository owner (string, required)
496-
- `repo`: Repository name (string, required)
496+
- `repo`: Repository name. If not provided, discussion categories will be queried at the organisation level. (string, optional)
497497

498498
- **list_discussions** - List discussions
499499
- `after`: Cursor for pagination. Use the endCursor from the previous page's PageInfo for GraphQL APIs. (string, optional)

pkg/github/discussions.go

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,7 @@ func GetDiscussion(getGQLClient GetGQLClientFn, t translations.TranslationHelper
295295
Repository struct {
296296
Discussion struct {
297297
Number githubv4.Int
298+
Title githubv4.String
298299
Body githubv4.String
299300
CreatedAt githubv4.DateTime
300301
URL githubv4.String `graphql:"url"`
@@ -315,6 +316,7 @@ func GetDiscussion(getGQLClient GetGQLClientFn, t translations.TranslationHelper
315316
d := q.Repository.Discussion
316317
discussion := &github.Discussion{
317318
Number: github.Ptr(int(d.Number)),
319+
Title: github.Ptr(string(d.Title)),
318320
Body: github.Ptr(string(d.Body)),
319321
HTMLURL: github.Ptr(string(d.URL)),
320322
CreatedAt: &github.Timestamp{Time: d.CreatedAt.Time},
@@ -441,7 +443,7 @@ func GetDiscussionComments(getGQLClient GetGQLClientFn, t translations.Translati
441443

442444
func ListDiscussionCategories(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
443445
return mcp.NewTool("list_discussion_categories",
444-
mcp.WithDescription(t("TOOL_LIST_DISCUSSION_CATEGORIES_DESCRIPTION", "List discussion categories with their id and name, for a repository")),
446+
mcp.WithDescription(t("TOOL_LIST_DISCUSSION_CATEGORIES_DESCRIPTION", "List discussion categories with their id and name, for a repository or organisation.")),
445447
mcp.WithToolAnnotation(mcp.ToolAnnotation{
446448
Title: t("TOOL_LIST_DISCUSSION_CATEGORIES_USER_TITLE", "List discussion categories"),
447449
ReadOnlyHint: ToBoolPtr(true),
@@ -451,19 +453,23 @@ func ListDiscussionCategories(getGQLClient GetGQLClientFn, t translations.Transl
451453
mcp.Description("Repository owner"),
452454
),
453455
mcp.WithString("repo",
454-
mcp.Required(),
455-
mcp.Description("Repository name"),
456+
mcp.Description("Repository name. If not provided, discussion categories will be queried at the organisation level."),
456457
),
457458
),
458459
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
459-
// Decode params
460-
var params struct {
461-
Owner string
462-
Repo string
460+
owner, err := RequiredParam[string](request, "owner")
461+
if err != nil {
462+
return mcp.NewToolResultError(err.Error()), nil
463463
}
464-
if err := mapstructure.Decode(request.Params.Arguments, &params); err != nil {
464+
repo, err := OptionalParam[string](request, "repo")
465+
if err != nil {
465466
return mcp.NewToolResultError(err.Error()), nil
466467
}
468+
// when not provided, default to the .github repository
469+
// this will query discussion categories at the organisation level
470+
if repo == "" {
471+
repo = ".github"
472+
}
467473

468474
client, err := getGQLClient(ctx)
469475
if err != nil {
@@ -488,8 +494,8 @@ func ListDiscussionCategories(getGQLClient GetGQLClientFn, t translations.Transl
488494
} `graphql:"repository(owner: $owner, name: $repo)"`
489495
}
490496
vars := map[string]interface{}{
491-
"owner": githubv4.String(params.Owner),
492-
"repo": githubv4.String(params.Repo),
497+
"owner": githubv4.String(owner),
498+
"repo": githubv4.String(repo),
493499
"first": githubv4.Int(25),
494500
}
495501
if err := client.Query(ctx, &q, vars); err != nil {

pkg/github/discussions_test.go

Lines changed: 112 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,7 @@ func Test_GetDiscussion(t *testing.T) {
484484
assert.ElementsMatch(t, toolDef.InputSchema.Required, []string{"owner", "repo", "discussionNumber"})
485485

486486
// Use exact string query that matches implementation output
487-
qGetDiscussion := "query($discussionNumber:Int!$owner:String!$repo:String!){repository(owner: $owner, name: $repo){discussion(number: $discussionNumber){number,body,createdAt,url,category{name}}}}"
487+
qGetDiscussion := "query($discussionNumber:Int!$owner:String!$repo:String!){repository(owner: $owner, name: $repo){discussion(number: $discussionNumber){number,title,body,createdAt,url,category{name}}}}"
488488

489489
vars := map[string]interface{}{
490490
"owner": "owner",
@@ -503,6 +503,7 @@ func Test_GetDiscussion(t *testing.T) {
503503
response: githubv4mock.DataResponse(map[string]any{
504504
"repository": map[string]any{"discussion": map[string]any{
505505
"number": 1,
506+
"title": "Test Discussion Title",
506507
"body": "This is a test discussion",
507508
"url": "https://github.com/owner/repo/discussions/1",
508509
"createdAt": "2025-04-25T12:00:00Z",
@@ -513,6 +514,7 @@ func Test_GetDiscussion(t *testing.T) {
513514
expected: &github.Discussion{
514515
HTMLURL: github.Ptr("https://github.com/owner/repo/discussions/1"),
515516
Number: github.Ptr(1),
517+
Title: github.Ptr("Test Discussion Title"),
516518
Body: github.Ptr("This is a test discussion"),
517519
CreatedAt: &github.Timestamp{Time: time.Date(2025, 4, 25, 12, 0, 0, 0, time.UTC)},
518520
DiscussionCategory: &github.DiscussionCategory{
@@ -549,6 +551,7 @@ func Test_GetDiscussion(t *testing.T) {
549551
require.NoError(t, json.Unmarshal([]byte(text), &out))
550552
assert.Equal(t, *tc.expected.HTMLURL, *out.HTMLURL)
551553
assert.Equal(t, *tc.expected.Number, *out.Number)
554+
assert.Equal(t, *tc.expected.Title, *out.Title)
552555
assert.Equal(t, *tc.expected.Body, *out.Body)
553556
// Check category label
554557
assert.Equal(t, *tc.expected.DiscussionCategory.Name, *out.DiscussionCategory.Name)
@@ -635,17 +638,33 @@ func Test_GetDiscussionComments(t *testing.T) {
635638
}
636639

637640
func Test_ListDiscussionCategories(t *testing.T) {
641+
mockClient := githubv4.NewClient(nil)
642+
toolDef, _ := ListDiscussionCategories(stubGetGQLClientFn(mockClient), translations.NullTranslationHelper)
643+
assert.Equal(t, "list_discussion_categories", toolDef.Name)
644+
assert.NotEmpty(t, toolDef.Description)
645+
assert.Contains(t, toolDef.Description, "or organisation")
646+
assert.Contains(t, toolDef.InputSchema.Properties, "owner")
647+
assert.Contains(t, toolDef.InputSchema.Properties, "repo")
648+
assert.ElementsMatch(t, toolDef.InputSchema.Required, []string{"owner"})
649+
638650
// Use exact string query that matches implementation output
639651
qListCategories := "query($first:Int!$owner:String!$repo:String!){repository(owner: $owner, name: $repo){discussionCategories(first: $first){nodes{id,name},pageInfo{hasNextPage,hasPreviousPage,startCursor,endCursor},totalCount}}}"
640652

641-
// Variables matching what GraphQL receives after JSON marshaling/unmarshaling
642-
vars := map[string]interface{}{
653+
// Variables for repository-level categories
654+
varsRepo := map[string]interface{}{
643655
"owner": "owner",
644656
"repo": "repo",
645657
"first": float64(25),
646658
}
647659

648-
mockResp := githubv4mock.DataResponse(map[string]any{
660+
// Variables for organization-level categories (using .github repo)
661+
varsOrg := map[string]interface{}{
662+
"owner": "owner",
663+
"repo": ".github",
664+
"first": float64(25),
665+
}
666+
667+
mockRespRepo := githubv4mock.DataResponse(map[string]any{
649668
"repository": map[string]any{
650669
"discussionCategories": map[string]any{
651670
"nodes": []map[string]any{
@@ -662,37 +681,98 @@ func Test_ListDiscussionCategories(t *testing.T) {
662681
},
663682
},
664683
})
665-
matcher := githubv4mock.NewQueryMatcher(qListCategories, vars, mockResp)
666-
httpClient := githubv4mock.NewMockedHTTPClient(matcher)
667-
gqlClient := githubv4.NewClient(httpClient)
668684

669-
tool, handler := ListDiscussionCategories(stubGetGQLClientFn(gqlClient), translations.NullTranslationHelper)
670-
assert.Equal(t, "list_discussion_categories", tool.Name)
671-
assert.NotEmpty(t, tool.Description)
672-
assert.Contains(t, tool.InputSchema.Properties, "owner")
673-
assert.Contains(t, tool.InputSchema.Properties, "repo")
674-
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo"})
685+
mockRespOrg := githubv4mock.DataResponse(map[string]any{
686+
"repository": map[string]any{
687+
"discussionCategories": map[string]any{
688+
"nodes": []map[string]any{
689+
{"id": "789", "name": "Announcements"},
690+
{"id": "101", "name": "General"},
691+
{"id": "112", "name": "Ideas"},
692+
},
693+
"pageInfo": map[string]any{
694+
"hasNextPage": false,
695+
"hasPreviousPage": false,
696+
"startCursor": "",
697+
"endCursor": "",
698+
},
699+
"totalCount": 3,
700+
},
701+
},
702+
})
675703

676-
request := createMCPRequest(map[string]interface{}{"owner": "owner", "repo": "repo"})
677-
result, err := handler(context.Background(), request)
678-
require.NoError(t, err)
704+
tests := []struct {
705+
name string
706+
reqParams map[string]interface{}
707+
vars map[string]interface{}
708+
mockResponse githubv4mock.GQLResponse
709+
expectError bool
710+
expectedCount int
711+
expectedCategories []map[string]string
712+
}{
713+
{
714+
name: "list repository-level discussion categories",
715+
reqParams: map[string]interface{}{
716+
"owner": "owner",
717+
"repo": "repo",
718+
},
719+
vars: varsRepo,
720+
mockResponse: mockRespRepo,
721+
expectError: false,
722+
expectedCount: 2,
723+
expectedCategories: []map[string]string{
724+
{"id": "123", "name": "CategoryOne"},
725+
{"id": "456", "name": "CategoryTwo"},
726+
},
727+
},
728+
{
729+
name: "list org-level discussion categories (no repo provided)",
730+
reqParams: map[string]interface{}{
731+
"owner": "owner",
732+
// repo is not provided, it will default to ".github"
733+
},
734+
vars: varsOrg,
735+
mockResponse: mockRespOrg,
736+
expectError: false,
737+
expectedCount: 3,
738+
expectedCategories: []map[string]string{
739+
{"id": "789", "name": "Announcements"},
740+
{"id": "101", "name": "General"},
741+
{"id": "112", "name": "Ideas"},
742+
},
743+
},
744+
}
679745

680-
text := getTextResult(t, result).Text
746+
for _, tc := range tests {
747+
t.Run(tc.name, func(t *testing.T) {
748+
matcher := githubv4mock.NewQueryMatcher(qListCategories, tc.vars, tc.mockResponse)
749+
httpClient := githubv4mock.NewMockedHTTPClient(matcher)
750+
gqlClient := githubv4.NewClient(httpClient)
681751

682-
var response struct {
683-
Categories []map[string]string `json:"categories"`
684-
PageInfo struct {
685-
HasNextPage bool `json:"hasNextPage"`
686-
HasPreviousPage bool `json:"hasPreviousPage"`
687-
StartCursor string `json:"startCursor"`
688-
EndCursor string `json:"endCursor"`
689-
} `json:"pageInfo"`
690-
TotalCount int `json:"totalCount"`
752+
_, handler := ListDiscussionCategories(stubGetGQLClientFn(gqlClient), translations.NullTranslationHelper)
753+
754+
req := createMCPRequest(tc.reqParams)
755+
res, err := handler(context.Background(), req)
756+
text := getTextResult(t, res).Text
757+
758+
if tc.expectError {
759+
require.True(t, res.IsError)
760+
return
761+
}
762+
require.NoError(t, err)
763+
764+
var response struct {
765+
Categories []map[string]string `json:"categories"`
766+
PageInfo struct {
767+
HasNextPage bool `json:"hasNextPage"`
768+
HasPreviousPage bool `json:"hasPreviousPage"`
769+
StartCursor string `json:"startCursor"`
770+
EndCursor string `json:"endCursor"`
771+
} `json:"pageInfo"`
772+
TotalCount int `json:"totalCount"`
773+
}
774+
require.NoError(t, json.Unmarshal([]byte(text), &response))
775+
assert.Equal(t, tc.expectedCategories, response.Categories)
776+
})
691777
}
692-
require.NoError(t, json.Unmarshal([]byte(text), &response))
693-
assert.Len(t, response.Categories, 2)
694-
assert.Equal(t, "123", response.Categories[0]["id"])
695-
assert.Equal(t, "CategoryOne", response.Categories[0]["name"])
696-
assert.Equal(t, "456", response.Categories[1]["id"])
697-
assert.Equal(t, "CategoryTwo", response.Categories[1]["name"])
698778
}

0 commit comments

Comments
 (0)