Skip to content

Commit 7446a20

Browse files
committed
Merge remote-tracking branch 'upstream/main'
2 parents 71da504 + e9f748f commit 7446a20

17 files changed

+2063
-46
lines changed

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,39 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
602602
- `secret_type`: The secret types to be filtered for in a comma-separated list (string, optional)
603603
- `resolution`: The resolution status (string, optional)
604604

605+
### Notifications
606+
607+
- **list_notifications** – List notifications for a GitHub user
608+
- `filter`: Filter to apply to the response (`default`, `include_read_notifications`, `only_participating`)
609+
- `since`: Only show notifications updated after the given time (ISO 8601 format)
610+
- `before`: Only show notifications updated before the given time (ISO 8601 format)
611+
- `owner`: Optional repository owner (string)
612+
- `repo`: Optional repository name (string)
613+
- `page`: Page number (number, optional)
614+
- `perPage`: Results per page (number, optional)
615+
616+
617+
- **get_notification_details** – Get detailed information for a specific GitHub notification
618+
- `notificationID`: The ID of the notification (string, required)
619+
620+
- **dismiss_notification** – Dismiss a notification by marking it as read or done
621+
- `threadID`: The ID of the notification thread (string, required)
622+
- `state`: The new state of the notification (`read` or `done`)
623+
624+
- **mark_all_notifications_read** – Mark all notifications as read
625+
- `lastReadAt`: Describes the last point that notifications were checked (optional, RFC3339/ISO8601 string, default: now)
626+
- `owner`: Optional repository owner (string)
627+
- `repo`: Optional repository name (string)
628+
629+
- **manage_notification_subscription** – Manage a notification subscription (ignore, watch, or delete) for a notification thread
630+
- `notificationID`: The ID of the notification thread (string, required)
631+
- `action`: Action to perform: `ignore`, `watch`, or `delete` (string, required)
632+
633+
- **manage_repository_notification_subscription** – Manage a repository notification subscription (ignore, watch, or delete)
634+
- `owner`: The account owner of the repository (string, required)
635+
- `repo`: The name of the repository (string, required)
636+
- `action`: Action to perform: `ignore`, `watch`, or `delete` (string, required)
637+
605638
## Resources
606639

607640
### Repository Content

e2e/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,7 @@ The current test suite is intentionally very limited in scope. This is because t
9090
The tests are quite repetitive and verbose. This is intentional as we want to see them develop more before committing to abstractions.
9191

9292
Currently, visibility into failures is not particularly good. We're hoping that we can pull apart the mcp-go client and have it hook into streams representing stdio without requiring an exec. This way we can get breakpoints in the debugger easily.
93+
94+
### Global State Mutation Tests
95+
96+
Some tools (such as those that mark all notifications as read) would change the global state for the tester, and are also not idempotent, so they offer little value for end to end tests and instead should rely on unit testing and manual verifications.

e2e/e2e_test.go

Lines changed: 112 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"encoding/base64"
88
"encoding/json"
99
"fmt"
10+
"net/http"
1011
"os"
1112
"os/exec"
1213
"slices"
@@ -61,7 +62,8 @@ func getRESTClient(t *testing.T) *gogithub.Client {
6162

6263
// Create a new GitHub client with the token
6364
ghClient := gogithub.NewClient(nil).WithAuthToken(token)
64-
if host := getE2EHost(); host != "https://github.com" {
65+
66+
if host := getE2EHost(); host != "" && host != "https://github.com" {
6567
var err error
6668
// Currently this works for GHEC because the API is exposed at the api subdomain and the path prefix
6769
// but it would be preferable to extract the host parsing from the main server logic, and use it here.
@@ -210,7 +212,6 @@ func TestGetMe(t *testing.T) {
210212
t.Parallel()
211213

212214
mcpClient := setupMCPClient(t)
213-
214215
ctx := context.Background()
215216

216217
// When we call the "get_me" tool
@@ -795,14 +796,13 @@ func TestDirectoryDeletion(t *testing.T) {
795796
}
796797

797798
func TestRequestCopilotReview(t *testing.T) {
799+
t.Parallel()
800+
798801
if getE2EHost() != "" && getE2EHost() != "https://github.com" {
799802
t.Skip("Skipping test because the host does not support copilot reviews")
800803
}
801804

802-
t.Parallel()
803-
804805
mcpClient := setupMCPClient(t)
805-
806806
ctx := context.Background()
807807

808808
// First, who am I
@@ -943,6 +943,112 @@ func TestRequestCopilotReview(t *testing.T) {
943943
require.Equal(t, "Bot", *reviewRequests.Users[0].Type, "expected review request to be for Bot")
944944
}
945945

946+
func TestAssignCopilotToIssue(t *testing.T) {
947+
t.Parallel()
948+
949+
if getE2EHost() != "" && getE2EHost() != "https://github.com" {
950+
t.Skip("Skipping test because the host does not support copilot being assigned to issues")
951+
}
952+
953+
mcpClient := setupMCPClient(t)
954+
ctx := context.Background()
955+
956+
// First, who am I
957+
getMeRequest := mcp.CallToolRequest{}
958+
getMeRequest.Params.Name = "get_me"
959+
960+
t.Log("Getting current user...")
961+
resp, err := mcpClient.CallTool(ctx, getMeRequest)
962+
require.NoError(t, err, "expected to call 'get_me' tool successfully")
963+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
964+
965+
require.False(t, resp.IsError, "expected result not to be an error")
966+
require.Len(t, resp.Content, 1, "expected content to have one item")
967+
968+
textContent, ok := resp.Content[0].(mcp.TextContent)
969+
require.True(t, ok, "expected content to be of type TextContent")
970+
971+
var trimmedGetMeText struct {
972+
Login string `json:"login"`
973+
}
974+
err = json.Unmarshal([]byte(textContent.Text), &trimmedGetMeText)
975+
require.NoError(t, err, "expected to unmarshal text content successfully")
976+
977+
currentOwner := trimmedGetMeText.Login
978+
979+
// Then create a repository with a README (via autoInit)
980+
repoName := fmt.Sprintf("github-mcp-server-e2e-%s-%d", t.Name(), time.Now().UnixMilli())
981+
createRepoRequest := mcp.CallToolRequest{}
982+
createRepoRequest.Params.Name = "create_repository"
983+
createRepoRequest.Params.Arguments = map[string]any{
984+
"name": repoName,
985+
"private": true,
986+
"autoInit": true,
987+
}
988+
989+
t.Logf("Creating repository %s/%s...", currentOwner, repoName)
990+
_, err = mcpClient.CallTool(ctx, createRepoRequest)
991+
require.NoError(t, err, "expected to call 'create_repository' tool successfully")
992+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
993+
994+
// Cleanup the repository after the test
995+
t.Cleanup(func() {
996+
// MCP Server doesn't support deletions, but we can use the GitHub Client
997+
ghClient := getRESTClient(t)
998+
t.Logf("Deleting repository %s/%s...", currentOwner, repoName)
999+
_, err := ghClient.Repositories.Delete(context.Background(), currentOwner, repoName)
1000+
require.NoError(t, err, "expected to delete repository successfully")
1001+
})
1002+
1003+
// Create an issue
1004+
createIssueRequest := mcp.CallToolRequest{}
1005+
createIssueRequest.Params.Name = "create_issue"
1006+
createIssueRequest.Params.Arguments = map[string]any{
1007+
"owner": currentOwner,
1008+
"repo": repoName,
1009+
"title": "Test issue to assign copilot to",
1010+
}
1011+
1012+
t.Logf("Creating issue in %s/%s...", currentOwner, repoName)
1013+
resp, err = mcpClient.CallTool(ctx, createIssueRequest)
1014+
require.NoError(t, err, "expected to call 'create_issue' tool successfully")
1015+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
1016+
1017+
// Assign copilot to the issue
1018+
assignCopilotRequest := mcp.CallToolRequest{}
1019+
assignCopilotRequest.Params.Name = "assign_copilot_to_issue"
1020+
assignCopilotRequest.Params.Arguments = map[string]any{
1021+
"owner": currentOwner,
1022+
"repo": repoName,
1023+
"issueNumber": 1,
1024+
}
1025+
1026+
t.Logf("Assigning copilot to issue in %s/%s...", currentOwner, repoName)
1027+
resp, err = mcpClient.CallTool(ctx, assignCopilotRequest)
1028+
require.NoError(t, err, "expected to call 'assign_copilot_to_issue' tool successfully")
1029+
1030+
textContent, ok = resp.Content[0].(mcp.TextContent)
1031+
require.True(t, ok, "expected content to be of type TextContent")
1032+
1033+
possibleExpectedFailure := "copilot isn't available as an assignee for this issue. Please inform the user to visit https://docs.github.com/en/copilot/using-github-copilot/using-copilot-coding-agent-to-work-on-tasks/about-assigning-tasks-to-copilot for more information."
1034+
if resp.IsError && textContent.Text == possibleExpectedFailure {
1035+
t.Skip("skipping because copilot wasn't available as an assignee on this issue, it's likely that the owner doesn't have copilot enabled in their settings")
1036+
}
1037+
1038+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
1039+
1040+
require.Equal(t, "successfully assigned copilot to issue", textContent.Text)
1041+
1042+
// Check that copilot is assigned to the issue
1043+
// MCP Server doesn't support getting assignees yet
1044+
ghClient := getRESTClient(t)
1045+
assignees, response, err := ghClient.Issues.Get(context.Background(), currentOwner, repoName, 1)
1046+
require.NoError(t, err, "expected to get issue successfully")
1047+
require.Equal(t, http.StatusOK, response.StatusCode, "expected to get issue successfully")
1048+
require.Len(t, assignees.Assignees, 1, "expected to find one assignee")
1049+
require.Equal(t, "Copilot", *assignees.Assignees[0].Login, "expected copilot to be assigned to the issue")
1050+
}
1051+
9461052
func TestPullRequestAtomicCreateAndSubmit(t *testing.T) {
9471053
t.Parallel()
9481054

@@ -1145,7 +1251,7 @@ func TestPullRequestReviewCommentSubmit(t *testing.T) {
11451251

11461252
t.Logf("Creating repository %s/%s...", currentOwner, repoName)
11471253
_, err = mcpClient.CallTool(ctx, createRepoRequest)
1148-
require.NoError(t, err, "expected to call 'get_me' tool successfully")
1254+
require.NoError(t, err, "expected to call 'create_repository' tool successfully")
11491255
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
11501256

11511257
// Cleanup the repository after the test

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.23.7
44

55
require (
66
github.com/google/go-github/v69 v69.2.0
7-
github.com/mark3labs/mcp-go v0.27.0
7+
github.com/mark3labs/mcp-go v0.28.0
88
github.com/migueleliasweb/go-github-mock v1.3.0
99
github.com/sirupsen/logrus v1.9.3
1010
github.com/spf13/cobra v1.9.1

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
3131
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
3232
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
3333
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
34-
github.com/mark3labs/mcp-go v0.27.0 h1:iok9kU4DUIU2/XVLgFS2Q9biIDqstC0jY4EQTK2Erzc=
35-
github.com/mark3labs/mcp-go v0.27.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
34+
github.com/mark3labs/mcp-go v0.28.0 h1:7yl4y5D1KYU2f/9Uxp7xfLIggfunHoESCRbrjcytcLM=
35+
github.com/mark3labs/mcp-go v0.28.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
3636
github.com/migueleliasweb/go-github-mock v1.3.0 h1:2sVP9JEMB2ubQw1IKto3/fzF51oFC6eVWOOFDgQoq88=
3737
github.com/migueleliasweb/go-github-mock v1.3.0/go.mod h1:ipQhV8fTcj/G6m7BKzin08GaJ/3B5/SonRAkgrk0zCY=
3838
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=

internal/githubv4mock/objects_are_equal_values.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// The contents of this file are taken from https://github.com/stretchr/testify/blob/016e2e9c269209287f33ec203f340a9a723fe22c/assert/assertions.go#L166
22
// because I do not want to take a dependency on the entire testify module just to use this equality check.
33
//
4+
// There is a modification in objectsAreEqual to check that typed nils are equal, even if their types are different.
5+
//
46
// The original license, copied from https://github.com/stretchr/testify/blob/016e2e9c269209287f33ec203f340a9a723fe22c/LICENSE
57
//
68
// MIT License
@@ -69,8 +71,10 @@ func objectsAreEqualValues(expected, actual any) bool {
6971
//
7072
// This function does no assertion of any kind.
7173
func objectsAreEqual(expected, actual any) bool {
72-
if expected == nil || actual == nil {
73-
return expected == actual
74+
// There is a modification in objectsAreEqual to check that typed nils are equal, even if their types are different.
75+
// This is required because when a nil is provided as a variable, the type is not known.
76+
if isNil(expected) && isNil(actual) {
77+
return true
7478
}
7579

7680
exp, ok := expected.([]byte)
@@ -94,3 +98,16 @@ func objectsAreEqual(expected, actual any) bool {
9498
func isNumericType(t reflect.Type) bool {
9599
return t.Kind() >= reflect.Int && t.Kind() <= reflect.Complex128
96100
}
101+
102+
func isNil(i any) bool {
103+
if i == nil {
104+
return true
105+
}
106+
v := reflect.ValueOf(i)
107+
switch v.Kind() {
108+
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
109+
return v.IsNil()
110+
default:
111+
return false
112+
}
113+
}

internal/githubv4mock/objects_are_equal_values_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// The contents of this file are taken from https://github.com/stretchr/testify/blob/016e2e9c269209287f33ec203f340a9a723fe22c/assert/assertions_test.go#L140-L174
22
//
3+
// There is a modification to test objectsAreEqualValues to check that typed nils are equal, even if their types are different.
4+
35
// The original license, copied from https://github.com/stretchr/testify/blob/016e2e9c269209287f33ec203f340a9a723fe22c/LICENSE
46
//
57
// MIT License
@@ -55,6 +57,8 @@ func TestObjectsAreEqualValues(t *testing.T) {
5557
{3.14, complex128(1e+100 + 1e+100i), false},
5658
{complex128(1e+10 + 1e+10i), complex64(1e+10 + 1e+10i), true},
5759
{complex64(1e+10 + 1e+10i), complex128(1e+10 + 1e+10i), true},
60+
{(*string)(nil), nil, true}, // typed nil vs untyped nil
61+
{(*string)(nil), (*int)(nil), true}, // different typed nils
5862
}
5963

6064
for _, c := range cases {

pkg/github/helper_test.go

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -109,14 +109,12 @@ func mockResponse(t *testing.T, code int, body interface{}) http.HandlerFunc {
109109
}
110110

111111
// createMCPRequest is a helper function to create a MCP request with the given arguments.
112-
func createMCPRequest(args map[string]interface{}) mcp.CallToolRequest {
112+
func createMCPRequest(args map[string]any) mcp.CallToolRequest {
113113
return mcp.CallToolRequest{
114114
Params: struct {
115-
Name string `json:"name"`
116-
Arguments map[string]interface{} `json:"arguments,omitempty"`
117-
Meta *struct {
118-
ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"`
119-
} `json:"_meta,omitempty"`
115+
Name string `json:"name"`
116+
Arguments map[string]any `json:"arguments,omitempty"`
117+
Meta *mcp.Meta `json:"_meta,omitempty"`
120118
}{
121119
Arguments: args,
122120
},

0 commit comments

Comments
 (0)