Skip to content

feat: add GitHub notifications tools for managing user notifications #225

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 our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

sridharavinash
Copy link

@sridharavinash sridharavinash commented Apr 11, 2025

GitHub Notifications Tooling

This PR adds support for managing GitHub notifications

Features Added

New Tools

  • get_notifications: Retrieve a list of notifications for the authenticated GitHub user with filtering options for read/unread status, participation, and time ranges
  • mark_notification_read: Mark a specific notification thread as read
  • mark_notification_done: Mark a specific notification thread as done
  • mark_all_notifications_read: Mark all notifications as read with an optional timestamp
  • get_notification_thread: Fetch details of a specific notification thread

Technical Implementation

  • Implements proper error handling and status code validation
  • Provides RFC3339/ISO8601 timestamp parsing for time-related parameters
  • Integrates with the existing GitHub API client infrastructure
  • Respects read-only mode settings for mutating operations

@sridharavinash sridharavinash marked this pull request as ready for review April 14, 2025 18:16
@Copilot Copilot AI review requested due to automatic review settings April 14, 2025 18:16
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces new GitHub notifications management tooling to allow users to retrieve notifications, mark them as read (individually or in bulk), and fetch detailed thread information.

  • Added notifications tools to the server initialization in pkg/github/server.go.
  • Implemented functions in pkg/github/notifications.go for handling notification retrieval and state updates with appropriate error handling and time parsing.

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
pkg/github/server.go Added notifications tools and helper functions for optional params.
pkg/github/notifications.go Implements API integrations for GitHub notifications management.
Comments suppressed due to low confidence (2)

pkg/github/server.go:262

  • The condition unconditionally returns the default value when v is false, which means an explicit false value from the user is ignored. Consider checking for the presence of the parameter instead of evaluating its boolean value directly.
if !v { return d, nil }

pkg/github/notifications.go:130

  • [nitpick] The parameter name 'getclient' is inconsistent with other functions that use 'getClient'. Consider renaming it to 'getClient' for consistency.
func MarkNotificationRead(getclient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {

@SamMorrowDrums SamMorrowDrums force-pushed the notifications-tooling branch from 4624a97 to d49b7a6 Compare May 20, 2025 10:22
@SamMorrowDrums SamMorrowDrums force-pushed the notifications-tooling branch from 9878a8a to ea51752 Compare May 20, 2025 11:08
@SamMorrowDrums SamMorrowDrums requested a review from Copilot May 20, 2025 15:06
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces a new “notifications” toolset for managing GitHub notifications, including unit and end-to-end tests, plus documentation for opting into state-mutating tests.

  • Registers the notifications toolset in pkg/github/tools.go
  • Adds unit tests for each notification tool (pkg/github/notifications_test.go)
  • Implements e2e tests and a README section to skip global-state mutations (e2e/e2e_test.go, e2e/README.md)

Reviewed Changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

File Description
pkg/github/tools.go Register new “notifications” toolset
pkg/github/notifications_test.go Add unit tests for list, get, dismiss, mark-all, and subscription tools
e2e/e2e_test.go Add e2e tests for notification tools and skip logic
e2e/README.md Document environment flag to skip mutation tests
Comments suppressed due to low confidence (5)

pkg/github/notifications_test.go:57

  • Consider adding a unit test for the "watch" action in ManageNotificationSubscription, since currently only "ignore" and "delete" are covered.
func Test_ManageNotificationSubscription(t *testing.T) {

pkg/github/notifications_test.go:114

  • The "watch" action isn’t tested in Test_ManageRepositoryNotificationSubscription; consider adding it to ensure full coverage of subscription states.
func Test_ManageRepositoryNotificationSubscription(t *testing.T) {

pkg/github/notifications_test.go:180

  • Add a success-case test for the "done" state in Test_DismissNotification to cover both supported states.
// Success case: mark as read

pkg/github/notifications_test.go:224

  • Include a unit test for passing the since timestamp into MarkAllNotificationsRead to validate the optional filter parameter.
request := createMCPRequest(map[string]interface{}{})

e2e/e2e_test.go:1552

  • Requiring exactly one notification is brittle—list_notifications can return multiple items. Consider using require.NotEmpty or asserting a minimum length.
require.Len(t, resp.Content, 1, "expected content to have one item")

toolsets.NewServerTool(GetNotificationDetails(getClient, t)),
).
AddWriteTools(
toolsets.NewServerTool(DismissNotification(getClient, t)),
Copy link
Preview

Copilot AI May 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The name DismissNotification may be unclear since it handles multiple states (read/done). Consider renaming to MarkNotificationState or similar for better API clarity.

Suggested change
toolsets.NewServerTool(DismissNotification(getClient, t)),
toolsets.NewServerTool(MarkNotificationState(getClient, t)),

Copilot uses AI. Check for mistakes.

// Validate with REST client
sub, resp2, err := restClient.Activity.GetThreadSubscription(ctx, notificationID)
// According to GitHub API, a deleted subscription returns 404
require.Error(t, err)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concern

This test is failing for me on this line. Even with a pause in the debugger to take a moment, there is a subscription successfully returned:

*github.Subscription {Subscribed: *false, Ignored: *true, Reason: *"", CreatedAt: *github.com/google/go-github/v69/github.Timestamp {Time: (*time.Time)(0x1400022e810)}, URL: *"https://api.github.com/notifications/threads/16530071814/subscription", RepositoryURL: *string nil, ThreadURL: *"https://api.github.com/notifications/threads/16530071814"}

This is despite the DeleteThreadSubscription call in the tool returning 204 as expected:

*github.Response {Response: *net/http.Response {Status: "204 No Content", StatusCode: 204, Proto: "HTTP/2.0", ProtoMajor: 2, ProtoMinor: 0, Header: net/http.Header [...], Body: io.ReadCloser(net/http.http2noBodyReader) *(*io.ReadCloser)(0x1400015fbd0), ContentLength: 0, TransferEncoding: []string len: 0, cap: 0, nil, Close: false, Uncompressed: false, Trailer: net/http.Header nil, Request: *(*"net/http.Request")(0x140001f7e00), TLS: *(*"crypto/tls.ConnectionState")(0x140000dc840)}, NextPage: 0, PrevPage: 0, FirstPage: 0, LastPage: 0, NextPageToken: "", Cursor: "", Before: "", After: "", Rate: github.com/google/go-github/v69/github.Rate {Limit: 15000, Remaining: 14982, Used: 18, Reset: (*"github.com/google/go-github/v69/github.Timestamp")(0x1400014c440), Resource: "core"}, TokenExpiration: github.com/google/go-github/v69/github.Timestamp {Time: (*time.Time)(0x1400014c468)}}

Where do you see the deleted subscription should return 404 in the API docs: https://docs.github.com/en/rest/activity/notifications?apiVersion=2022-11-28#get-a-thread-subscription-for-the-authenticated-user, it seems to me that we have a "subscription" but the Subscribed field is false?

// Validate with REST client
sub, resp2, err := restClient.Activity.GetRepositorySubscription(ctx, owner, repo)
// According to GitHub API, a deleted subscription returns 404
require.Error(t, err)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have the same issue here as above.

}
err = json.Unmarshal([]byte(textContent.Text), &notifications)
require.NoError(t, err)
require.NotEmpty(t, notifications)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this probably needs:

	if len(notifications) == 0 {
		t.Skip("No notifications available to test dismissal")
	}

The test failed for me in a surprising way because I had no notifications.

@@ -1525,3 +1536,298 @@ func TestPullRequestReviewDeletion(t *testing.T) {
require.NoError(t, err, "expected to unmarshal text content successfully")
require.Len(t, noReviews, 0, "expected to find no reviews")
}

func TestE2E_ListNotifications(t *testing.T) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question

Is there a reason you prefixed all of these TestE2E? They are in the e2e package so it seems a little redundant and out of sync with the rest of the existing tests.

"github.com/stretchr/testify/require"
)

func Test_ListNotifications(t *testing.T) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request

Please use the same testing format as the rest of the other tool handler tests, or if you feel strongly, document why this is a different pattern that you are proceeding with here.

var resp *github.Response

if owner != "" && repo != "" {
notifications, resp, err = client.Activity.ListRepositoryNotifications(ctx, owner, repo, opts)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request

I think we should probably have at least a unit test for this branch which is currently uncovered.

if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("invalid threadID format: %v", err)), nil
}
resp, err = client.Activity.MarkThreadDone(ctx, threadIDInt)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request

I think we should probably have at least a unit test for this branch which is currently uncovered.


var resp *github.Response
if owner != "" && repo != "" {
resp, err = client.Activity.MarkRepositoryNotificationsRead(ctx, owner, repo, markReadOptions)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request

I think we should probably have at least a unit test for this branch which is currently uncovered.

result, resp, apiErr = client.Activity.SetThreadSubscription(ctx, notificationID, sub)
case NotificationActionWatch:
sub := &github.Subscription{Ignored: toBoolPtr(false), Subscribed: toBoolPtr(true)}
result, resp, apiErr = client.Activity.SetThreadSubscription(ctx, notificationID, sub)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request

I think we should probably have at least a unit test for this branch which is currently uncovered.

result, resp, apiErr = client.Activity.SetRepositorySubscription(ctx, owner, repo, sub)
case RepositorySubscriptionActionWatch:
sub := &github.Subscription{Ignored: toBoolPtr(false), Subscribed: toBoolPtr(true)}
result, resp, apiErr = client.Activity.SetRepositorySubscription(ctx, owner, repo, sub)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request

I think we should probably have at least a unit test for this branch which is currently uncovered.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants