- Notifications
You must be signed in to change notification settings - Fork940
Add list notifications tool#297
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
e0cde71
2e52386
ea562ae
9424fbd
81f093e
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
package github | ||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"github.com/github/github-mcp-server/pkg/translations" | ||
"github.com/google/go-github/v69/github" | ||
"github.com/mark3labs/mcp-go/mcp" | ||
"github.com/mark3labs/mcp-go/server" | ||
) | ||
// ListNotifications creates a tool to list notifications for a GitHub user. | ||
func ListNotifications(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { | ||
return mcp.NewTool("list_notifications", | ||
mcp.WithDescription(t("TOOL_LIST_NOTIFICATIONS_DESCRIPTION", "List notifications for a GitHub user")), | ||
mcp.WithNumber("page", | ||
mcp.Description("Page number"), | ||
), | ||
mcp.WithNumber("per_page", | ||
mcp.Description("Number of records per page"), | ||
), | ||
mcp.WithBoolean("all", | ||
mcp.Description("Whether to fetch all notifications, including read ones"), | ||
), | ||
), | ||
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { | ||
page, err := OptionalIntParamWithDefault(request, "page", 1) | ||
if err != nil { | ||
return mcp.NewToolResultError(err.Error()), nil | ||
} | ||
perPage, err := OptionalIntParamWithDefault(request, "per_page", 30) | ||
if err != nil { | ||
return mcp.NewToolResultError(err.Error()), nil | ||
} | ||
all := false | ||
if val, err := OptionalParam[bool](request, "all"); err == nil { | ||
all = val | ||
} | ||
opts := &github.NotificationListOptions{ | ||
ListOptions: github.ListOptions{ | ||
Page: page, | ||
PerPage: perPage, | ||
}, | ||
All: all, // Include all notifications, even those already read. | ||
} | ||
client, err := getClient(ctx) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to get GitHub client: %w", err) | ||
} | ||
notifications, resp, err := client.Activity.ListNotifications(ctx, opts) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to list notifications: %w", err) | ||
} | ||
defer func() { _ = resp.Body.Close() }() | ||
if resp.StatusCode != http.StatusOK { | ||
body, err := io.ReadAll(resp.Body) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to read response body: %w", err) | ||
} | ||
return mcp.NewToolResultError(fmt.Sprintf("failed to list notifications: %s", string(body))), nil | ||
} | ||
r, err := json.Marshal(notifications) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to marshal notifications: %w", err) | ||
} | ||
return mcp.NewToolResultText(string(r)), nil | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
package github | ||
import ( | ||
"context" | ||
"encoding/json" | ||
"net/http" | ||
"testing" | ||
"time" | ||
"github.com/github/github-mcp-server/pkg/translations" | ||
"github.com/google/go-github/v69/github" | ||
"github.com/migueleliasweb/go-github-mock/src/mock" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
func Test_ListNotifications(t *testing.T) { | ||
// Verify tool definition | ||
mockClient := github.NewClient(nil) | ||
tool, _ := ListNotifications(stubGetClientFn(mockClient), translations.NullTranslationHelper) | ||
assert.Equal(t, "list_notifications", tool.Name) | ||
assert.NotEmpty(t, tool.Description) | ||
assert.Contains(t, tool.InputSchema.Properties, "page") | ||
assert.Contains(t, tool.InputSchema.Properties, "per_page") | ||
assert.Contains(t, tool.InputSchema.Properties, "all") | ||
// Setup mock notifications | ||
mockNotifications := []*github.Notification{ | ||
{ | ||
ID: github.Ptr("1"), | ||
Reason: github.Ptr("mention"), | ||
Subject: &github.NotificationSubject{ | ||
Title: github.Ptr("Test Notification 1"), | ||
}, | ||
UpdatedAt: &github.Timestamp{Time: time.Now()}, | ||
URL: github.Ptr("https://example.com/notifications/threads/1"), | ||
}, | ||
{ | ||
ID: github.Ptr("2"), | ||
Reason: github.Ptr("team_mention"), | ||
Subject: &github.NotificationSubject{ | ||
Title: github.Ptr("Test Notification 2"), | ||
}, | ||
UpdatedAt: &github.Timestamp{Time: time.Now()}, | ||
URL: github.Ptr("https://example.com/notifications/threads/1"), | ||
}, | ||
} | ||
tests := []struct { | ||
name string | ||
mockedClient *http.Client | ||
requestArgs map[string]interface{} | ||
expectError bool | ||
expectedResponse []*github.Notification | ||
expectedErrMsg string | ||
}{ | ||
{ | ||
name: "list all notifications", | ||
mockedClient: mock.NewMockedHTTPClient( | ||
mock.WithRequestMatch( | ||
mock.GetNotifications, | ||
mockNotifications, | ||
), | ||
), | ||
requestArgs: map[string]interface{}{ | ||
"all": true, | ||
}, | ||
expectError: false, | ||
expectedResponse: mockNotifications, | ||
}, | ||
{ | ||
name: "list unread notifications", | ||
mockedClient: mock.NewMockedHTTPClient( | ||
mock.WithRequestMatch( | ||
mock.GetNotifications, | ||
mockNotifications[:1], // Only the first notification | ||
), | ||
), | ||
requestArgs: map[string]interface{}{ | ||
"all": false, | ||
}, | ||
expectError: false, | ||
expectedResponse: mockNotifications[:1], | ||
}, | ||
} | ||
for _, tc := range tests { | ||
t.Run(tc.name, func(t *testing.T) { | ||
// Setup client with mock | ||
client := github.NewClient(tc.mockedClient) | ||
_, handler := ListNotifications(stubGetClientFn(client), translations.NullTranslationHelper) | ||
// Create call request | ||
request := createMCPRequest(tc.requestArgs) | ||
// Call handler | ||
result, err := handler(context.Background(), request) | ||
// Verify results | ||
if tc.expectError { | ||
require.Error(t, err) | ||
assert.Contains(t, err.Error(), tc.expectedErrMsg) | ||
return | ||
} | ||
require.NoError(t, err) | ||
textContent := getTextResult(t, result) | ||
// Unmarshal and verify the result | ||
var returnedNotifications []*github.Notification | ||
err = json.Unmarshal([]byte(textContent.Text), &returnedNotifications) | ||
require.NoError(t, err) | ||
assert.Equal(t, len(tc.expectedResponse), len(returnedNotifications)) | ||
for i, notification := range returnedNotifications { | ||
assert.Equal(t, *tc.expectedResponse[i].ID, *notification.ID) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Copilot is somewhat right here with:
| ||
assert.Equal(t, *tc.expectedResponse[i].Reason, *notification.Reason) | ||
assert.Equal(t, *tc.expectedResponse[i].Subject.Title, *notification.Subject.Title) | ||
} | ||
}) | ||
} | ||
} |