Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitd5105a4

Browse files
author
cointem
committed
update: getOrgMembers
1 parent6a57e75 commitd5105a4

File tree

4 files changed

+250
-2
lines changed

4 files changed

+250
-2
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"annotations": {
3+
"title":"Get organization members",
4+
"readOnlyHint":true
5+
},
6+
"description":"Get member users of a specific organization. Returns a list of user objects with fields: login, id, avatar_url, type. Limited to organizations accessible with current credentials",
7+
"inputSchema": {
8+
"properties": {
9+
"org": {
10+
"description":"Organization login (owner) to get members for.",
11+
"type":"string"
12+
}
13+
,"role": {
14+
"description":"Filter by role: all, admin, member",
15+
"type":"string"
16+
},
17+
"per_page": {
18+
"description":"Results per page (max 100)",
19+
"type":"number"
20+
},
21+
"page": {
22+
"description":"Page number for pagination",
23+
"type":"number"
24+
}
25+
},
26+
"required": [
27+
"org"
28+
],
29+
"type":"object"
30+
},
31+
"name":"get_org_members"
32+
}

‎pkg/github/context_tools.go‎

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@ package github
22

33
import (
44
"context"
5+
"fmt"
6+
"strings"
57
"time"
68

79
ghErrors"github.com/github/github-mcp-server/pkg/errors"
810
"github.com/github/github-mcp-server/pkg/translations"
11+
"github.com/go-viper/mapstructure/v2"
12+
"github.com/google/go-github/v79/github"
913
"github.com/mark3labs/mcp-go/mcp"
1014
"github.com/mark3labs/mcp-go/server"
1115
"github.com/shurcooL/githubv4"
@@ -249,3 +253,99 @@ func GetTeamMembers(getGQLClient GetGQLClientFn, t translations.TranslationHelpe
249253
returnMarshalledTextResult(members),nil
250254
}
251255
}
256+
257+
funcgetOrgMembers(getClientGetClientFn,t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) {
258+
returnmcp.NewTool("get_org_members",
259+
mcp.WithDescription(t("TOOL_GET_ORG_MEMBERS_DESCRIPTION","Get member users of a specific organization. Returns a list of user objects with fields: login, id, avatar_url, type. Limited to organizations accessible with current credentials")),
260+
mcp.WithString("org",
261+
mcp.Description(t("TOOL_GET_ORG_MEMBERS_ORG_DESCRIPTION","Organization login (owner) to get members for.")),
262+
mcp.Required(),
263+
),
264+
mcp.WithString("role",
265+
mcp.Description("Filter by role: all, admin, member"),
266+
),
267+
mcp.WithNumber("per_page",
268+
mcp.Description("Results per page (max 100)"),
269+
),
270+
mcp.WithNumber("page",
271+
mcp.Description("Page number for pagination"),
272+
),
273+
mcp.WithToolAnnotation(mcp.ToolAnnotation{
274+
Title:t("TOOL_GET_ORG_MEMBERS_TITLE","Get organization members"),
275+
ReadOnlyHint:ToBoolPtr(true),
276+
}),
277+
),
278+
func(ctx context.Context,request mcp.CallToolRequest) (*mcp.CallToolResult,error) {
279+
// Decode params into struct to support optional numbers
280+
varparamsstruct {
281+
Orgstring`mapstructure:"org"`
282+
Rolestring`mapstructure:"role"`
283+
PerPageint32`mapstructure:"per_page"`
284+
Pageint32`mapstructure:"page"`
285+
}
286+
iferr:=mapstructure.Decode(request.Params.Arguments,&params);err!=nil {
287+
returnmcp.NewToolResultError(err.Error()),nil
288+
}
289+
org:=params.Org
290+
role:=params.Role
291+
perPage:=params.PerPage
292+
page:=params.Page
293+
iforg=="" {
294+
returnmcp.NewToolResultError("org is required"),nil
295+
}
296+
297+
// Defaults
298+
ifperPage<=0 {
299+
perPage=30
300+
}
301+
ifperPage>100 {
302+
perPage=100
303+
}
304+
ifpage<=0 {
305+
page=1
306+
}
307+
client,err:=getClient(ctx)
308+
iferr!=nil {
309+
returnmcp.NewToolResultErrorFromErr("failed to get GitHub client",err),nil
310+
}
311+
312+
// Map role string to REST role filter expected by GitHub API ("all","admin","member").
313+
roleFilter:=""
314+
ifrole!=""&&strings.ToLower(role)!="all" {
315+
roleFilter=strings.ToLower(role)
316+
}
317+
318+
// Use Organizations.ListMembers with pagination (page/per_page)
319+
opts:=&github.ListMembersOptions{
320+
Role:roleFilter,
321+
ListOptions: github.ListOptions{
322+
PerPage:int(perPage),
323+
Page:int(page),
324+
},
325+
}
326+
327+
users,resp,err:=client.Organizations.ListMembers(ctx,org,opts)
328+
iferr!=nil {
329+
returnghErrors.NewGitHubAPIErrorResponse(ctx,"Failed to get organization members",resp,err),nil
330+
}
331+
332+
typeoutUserstruct {
333+
Loginstring`json:"login"`
334+
IDstring`json:"id"`
335+
AvatarURLstring`json:"avatar_url"`
336+
Typestring`json:"type"`
337+
}
338+
339+
varmembers []outUser
340+
for_,u:=rangeusers {
341+
members=append(members,outUser{
342+
Login:u.GetLogin(),
343+
ID:fmt.Sprintf("%v",u.GetID()),
344+
AvatarURL:u.GetAvatarURL(),
345+
Type:u.GetType(),
346+
})
347+
}
348+
349+
returnMarshalledTextResult(members),nil
350+
}
351+
}

‎pkg/github/context_tools_test.go‎

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"net/http"
78
"testing"
89
"time"
910

@@ -497,3 +498,115 @@ func Test_GetTeamMembers(t *testing.T) {
497498
})
498499
}
499500
}
501+
502+
funcTest_GetOrgMembers(t*testing.T) {
503+
t.Parallel()
504+
505+
tool,_:=getOrgMembers(nil,translations.NullTranslationHelper)
506+
require.NoError(t,toolsnaps.Test(tool.Name,tool))
507+
508+
assert.Equal(t,"get_org_members",tool.Name)
509+
assert.True(t,*tool.Annotations.ReadOnlyHint,"get_org_members tool should be read-only")
510+
511+
// Mocked REST users as returned by GitHub REST API
512+
mockUsers:= []map[string]any{
513+
{"login":"user1","id":11,"avatar_url":"https://example.com/avatars/1","type":"User"},
514+
{"login":"user2","id":22,"avatar_url":"https://example.com/avatars/2","type":"User"},
515+
}
516+
517+
tests:= []struct {
518+
namestring
519+
mockedClient*http.Client
520+
stubGetClientGetClientFn
521+
requestArgsmap[string]any
522+
expectToolErrbool
523+
expectErrMsgstring
524+
expectCountint
525+
}{
526+
{
527+
name:"successful get org members",
528+
mockedClient:mock.NewMockedHTTPClient(
529+
mock.WithRequestMatchHandler(
530+
mock.EndpointPattern{Pattern:"/orgs/{org}/members",Method:http.MethodGet},
531+
http.HandlerFunc(func(w http.ResponseWriter,_*http.Request) {
532+
w.WriteHeader(http.StatusOK)
533+
_,_=w.Write(mock.MustMarshal(mockUsers))
534+
}),
535+
),
536+
),
537+
requestArgs:map[string]any{"org":"testorg","role":"all","per_page":30,"page":1},
538+
expectCount:2,
539+
},
540+
{
541+
name:"org with no members",
542+
mockedClient:mock.NewMockedHTTPClient(
543+
mock.WithRequestMatchHandler(
544+
mock.EndpointPattern{Pattern:"/orgs/{org}/members",Method:http.MethodGet},
545+
http.HandlerFunc(func(w http.ResponseWriter,_*http.Request) {
546+
w.WriteHeader(http.StatusOK)
547+
_,_=w.Write(mock.MustMarshal([]map[string]any{}))
548+
}),
549+
),
550+
),
551+
requestArgs:map[string]any{"org":"testorg","role":"all","per_page":30,"page":1},
552+
expectCount:0,
553+
},
554+
{
555+
name:"getting client fails",
556+
mockedClient:nil,
557+
stubGetClient:stubGetClientFnErr("expected test error"),
558+
requestArgs:map[string]any{"org":"testorg"},
559+
expectToolErr:true,
560+
expectErrMsg:"failed to get GitHub client: expected test error",
561+
},
562+
{
563+
name:"api error",
564+
mockedClient:mock.NewMockedHTTPClient(
565+
mock.WithRequestMatchHandler(
566+
mock.EndpointPattern{Pattern:"/orgs/{org}/members",Method:http.MethodGet},
567+
mockResponse(t,http.StatusInternalServerError,map[string]string{"message":"boom"}),
568+
),
569+
),
570+
requestArgs:map[string]any{"org":"testorg"},
571+
expectToolErr:true,
572+
expectErrMsg:"Failed to get organization members",
573+
},
574+
}
575+
576+
for_,tc:=rangetests {
577+
t.Run(tc.name,func(t*testing.T) {
578+
varstubFnGetClientFn
579+
iftc.stubGetClient!=nil {
580+
stubFn=tc.stubGetClient
581+
}elseiftc.mockedClient!=nil {
582+
stubFn=stubGetClientFromHTTPFn(tc.mockedClient)
583+
}else {
584+
stubFn=nil
585+
}
586+
587+
_,handler:=getOrgMembers(stubFn,translations.NullTranslationHelper)
588+
589+
request:=createMCPRequest(tc.requestArgs)
590+
result,err:=handler(context.Background(),request)
591+
require.NoError(t,err)
592+
textContent:=getTextResult(t,result)
593+
594+
iftc.expectToolErr {
595+
assert.True(t,result.IsError)
596+
assert.Contains(t,textContent.Text,tc.expectErrMsg)
597+
return
598+
}
599+
600+
varmembers []struct {
601+
Loginstring`json:"login"`
602+
IDstring`json:"id"`
603+
AvatarURLstring`json:"avatar_url"`
604+
Typestring`json:"type"`
605+
}
606+
err=json.Unmarshal([]byte(textContent.Text),&members)
607+
require.NoError(t,err)
608+
609+
assert.Len(t,members,tc.expectCount)
610+
})
611+
}
612+
}

‎pkg/github/tools.go‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ import (
1414
"github.com/shurcooL/githubv4"
1515
)
1616

17-
typeGetClientFnfunc(context.Context) (*github.Client,error)
18-
typeGetGQLClientFnfunc(context.Context) (*githubv4.Client,error)
17+
type (
18+
GetClientFnfunc(context.Context) (*github.Client,error)
19+
GetGQLClientFnfunc(context.Context) (*githubv4.Client,error)
20+
)
1921

2022
// ToolsetMetadata holds metadata for a toolset including its ID and description
2123
typeToolsetMetadatastruct {
@@ -312,6 +314,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG
312314
toolsets.NewServerTool(GetMe(getClient,t)),
313315
toolsets.NewServerTool(GetTeams(getClient,getGQLClient,t)),
314316
toolsets.NewServerTool(GetTeamMembers(getGQLClient,t)),
317+
toolsets.NewServerTool(getOrgMembers(getClient,t)),
315318
)
316319

317320
gists:=toolsets.NewToolset(ToolsetMetadataGists.ID,ToolsetMetadataGists.Description).

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp