Skip to content

Commit 46389d6

Browse files
committed
Create org invitation tool
1 parent 99acea6 commit 46389d6

File tree

4 files changed

+462
-0
lines changed

4 files changed

+462
-0
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
"annotations": {
3+
"title": "Create Organization Invitation",
4+
"readOnlyHint": false
5+
},
6+
"description": "Invite a user to join an organization by GitHub user ID or email address. Requires organization owner permissions. This endpoint triggers notifications and may be subject to rate limiting.",
7+
"inputSchema": {
8+
"properties": {
9+
"email": {
10+
"description": "Email address of the person you are inviting. Required unless invitee_id is provided.",
11+
"type": "string"
12+
},
13+
"invitee_id": {
14+
"description": "GitHub user ID for the person you are inviting. Required unless email is provided.",
15+
"type": "number"
16+
},
17+
"org": {
18+
"description": "The organization name (not case sensitive)",
19+
"type": "string"
20+
},
21+
"role": {
22+
"default": "direct_member",
23+
"description": "The role for the new member",
24+
"enum": [
25+
"admin",
26+
"direct_member",
27+
"billing_manager",
28+
"reinstate"
29+
],
30+
"type": "string"
31+
},
32+
"team_ids": {
33+
"description": "Team IDs to invite new members to",
34+
"items": {
35+
"type": "number"
36+
},
37+
"type": "array"
38+
}
39+
},
40+
"required": [
41+
"org"
42+
],
43+
"type": "object"
44+
},
45+
"name": "create_org_invitation"
46+
}

pkg/github/orgs.go

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"strconv"
10+
11+
"github.com/google/go-github/v74/github"
12+
"github.com/mark3labs/mcp-go/mcp"
13+
"github.com/mark3labs/mcp-go/server"
14+
15+
"github.com/github/github-mcp-server/pkg/translations"
16+
)
17+
18+
// CreateOrgInvitation creates a new invitation for a user to join an organization
19+
func CreateOrgInvitation(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
20+
return mcp.NewTool("create_org_invitation",
21+
mcp.WithDescription(t("TOOL_CREATE_ORG_INVITATION_DESCRIPTION", "Invite a user to join an organization by GitHub user ID or email address. Requires organization owner permissions. This endpoint triggers notifications and may be subject to rate limiting.")),
22+
mcp.WithToolAnnotation(mcp.ToolAnnotation{
23+
Title: t("TOOL_CREATE_ORG_INVITATION", "Create Organization Invitation"),
24+
ReadOnlyHint: ToBoolPtr(false),
25+
}),
26+
mcp.WithString("org",
27+
mcp.Required(),
28+
mcp.Description("The organization name (not case sensitive)"),
29+
),
30+
mcp.WithNumber("invitee_id",
31+
mcp.Description("GitHub user ID for the person you are inviting. Required unless email is provided."),
32+
),
33+
mcp.WithString("email",
34+
mcp.Description("Email address of the person you are inviting. Required unless invitee_id is provided."),
35+
),
36+
mcp.WithString("role",
37+
mcp.Description("The role for the new member"),
38+
mcp.Enum("admin", "direct_member", "billing_manager", "reinstate"),
39+
mcp.DefaultString("direct_member"),
40+
),
41+
mcp.WithArray("team_ids",
42+
mcp.Description("Team IDs to invite new members to"),
43+
mcp.Items(map[string]any{
44+
"type": "number",
45+
}),
46+
),
47+
),
48+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
49+
org, err := RequiredParam[string](request, "org")
50+
if err != nil {
51+
return mcp.NewToolResultError(err.Error()), nil
52+
}
53+
54+
inviteeID, err := OptionalParam[float64](request, "invitee_id")
55+
if err != nil {
56+
return mcp.NewToolResultError(err.Error()), nil
57+
}
58+
59+
email, err := OptionalParam[string](request, "email")
60+
if err != nil {
61+
return mcp.NewToolResultError(err.Error()), nil
62+
}
63+
64+
// Validate that at least one of invitee_id or email is provided
65+
if inviteeID == 0 && email == "" {
66+
return mcp.NewToolResultError("either invitee_id or email must be provided"), nil
67+
}
68+
69+
role, err := OptionalParam[string](request, "role")
70+
if err != nil {
71+
return mcp.NewToolResultError(err.Error()), nil
72+
}
73+
if role == "" {
74+
role = "direct_member"
75+
}
76+
77+
var teamIDs []int64
78+
if rawTeamIDs, ok := request.GetArguments()["team_ids"]; ok {
79+
switch v := rawTeamIDs.(type) {
80+
case nil:
81+
// nothing to do
82+
case []any:
83+
for _, item := range v {
84+
id, parseErr := parseTeamID(item)
85+
if parseErr != nil {
86+
return mcp.NewToolResultError(parseErr.Error()), nil
87+
}
88+
teamIDs = append(teamIDs, id)
89+
}
90+
case []float64:
91+
for _, item := range v {
92+
teamIDs = append(teamIDs, int64(item))
93+
}
94+
default:
95+
return mcp.NewToolResultError("team_ids must be an array of numbers"), nil
96+
}
97+
}
98+
99+
client, err := getClient(ctx)
100+
if err != nil {
101+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
102+
}
103+
104+
// Create the invitation request
105+
invitation := &github.CreateOrgInvitationOptions{
106+
Role: github.Ptr(role),
107+
TeamID: teamIDs,
108+
}
109+
110+
if inviteeID != 0 {
111+
invitation.InviteeID = github.Ptr(int64(inviteeID))
112+
}
113+
114+
if email != "" {
115+
invitation.Email = github.Ptr(email)
116+
}
117+
118+
createdInvitation, resp, err := client.Organizations.CreateOrgInvitation(ctx, org, invitation)
119+
if err != nil {
120+
return nil, fmt.Errorf("failed to create organization invitation: %w", err)
121+
}
122+
defer func() { _ = resp.Body.Close() }()
123+
124+
if resp.StatusCode != http.StatusCreated {
125+
body, err := io.ReadAll(resp.Body)
126+
if err != nil {
127+
return nil, fmt.Errorf("failed to read response body: %w", err)
128+
}
129+
return mcp.NewToolResultError(fmt.Sprintf("failed to create organization invitation: %s", string(body))), nil
130+
}
131+
132+
// Return a minimal response with relevant information
133+
type InvitationResponse struct {
134+
ID int64 `json:"id"`
135+
Login string `json:"login,omitempty"`
136+
Email string `json:"email,omitempty"`
137+
Role string `json:"role"`
138+
InvitationTeamsURL string `json:"invitation_teams_url"`
139+
CreatedAt string `json:"created_at"`
140+
InviterLogin string `json:"inviter_login,omitempty"`
141+
}
142+
143+
response := InvitationResponse{
144+
ID: createdInvitation.GetID(),
145+
Login: createdInvitation.GetLogin(),
146+
Email: createdInvitation.GetEmail(),
147+
Role: createdInvitation.GetRole(),
148+
InvitationTeamsURL: createdInvitation.GetInvitationTeamURL(),
149+
CreatedAt: createdInvitation.GetCreatedAt().Format("2006-01-02T15:04:05Z07:00"),
150+
}
151+
152+
if createdInvitation.Inviter != nil {
153+
response.InviterLogin = createdInvitation.Inviter.GetLogin()
154+
}
155+
156+
r, err := json.Marshal(response)
157+
if err != nil {
158+
return nil, fmt.Errorf("failed to marshal response: %w", err)
159+
}
160+
161+
return mcp.NewToolResultText(string(r)), nil
162+
}
163+
}
164+
165+
func parseTeamID(value any) (int64, error) {
166+
switch v := value.(type) {
167+
case float64:
168+
// JSON numbers decode to float64; ensure they are whole numbers
169+
if v != float64(int64(v)) {
170+
return 0, fmt.Errorf("team_id must be an integer value")
171+
}
172+
return int64(v), nil
173+
case int:
174+
return int64(v), nil
175+
case int64:
176+
return v, nil
177+
case string:
178+
id, err := strconv.ParseInt(v, 10, 64)
179+
if err != nil {
180+
return 0, fmt.Errorf("invalid team_id")
181+
}
182+
return id, nil
183+
default:
184+
return 0, fmt.Errorf("invalid team_id")
185+
}
186+
}

0 commit comments

Comments
 (0)