Skip to content

Commit ea51752

Browse files
add in e2e and also try to get the LLM to call notifications off its own back
1 parent d49b7a6 commit ea51752

File tree

2 files changed

+249
-1
lines changed

2 files changed

+249
-1
lines changed

e2e/e2e_test.go

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1525,3 +1525,251 @@ func TestPullRequestReviewDeletion(t *testing.T) {
15251525
require.NoError(t, err, "expected to unmarshal text content successfully")
15261526
require.Len(t, noReviews, 0, "expected to find no reviews")
15271527
}
1528+
1529+
func TestE2E_ListNotifications(t *testing.T) {
1530+
t.Parallel()
1531+
client := setupMCPClient(t)
1532+
ctx := context.Background()
1533+
1534+
request := mcp.CallToolRequest{}
1535+
request.Params.Name = "list_notifications"
1536+
request.Params.Arguments = map[string]any{}
1537+
1538+
resp, err := client.CallTool(ctx, request)
1539+
require.NoError(t, err, "expected to call 'list_notifications' tool successfully")
1540+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
1541+
require.Len(t, resp.Content, 1, "expected content to have one item")
1542+
1543+
var notifications []struct {
1544+
ID string `json:"id"`
1545+
}
1546+
textContent, ok := resp.Content[0].(mcp.TextContent)
1547+
require.True(t, ok, "expected content to be of type TextContent")
1548+
err = json.Unmarshal([]byte(textContent.Text), &notifications)
1549+
require.NoError(t, err, "expected to unmarshal text content successfully")
1550+
}
1551+
1552+
func TestE2E_ManageNotificationSubscription(t *testing.T) {
1553+
t.Parallel()
1554+
client := setupMCPClient(t)
1555+
ctx := context.Background()
1556+
1557+
// List notifications to get a valid notificationID
1558+
listReq := mcp.CallToolRequest{}
1559+
listReq.Params.Name = "list_notifications"
1560+
listReq.Params.Arguments = map[string]any{}
1561+
resp, err := client.CallTool(ctx, listReq)
1562+
require.NoError(t, err)
1563+
require.False(t, resp.IsError)
1564+
if len(resp.Content) == 0 {
1565+
return t.Skip("No notifications available to test subscription management")
1566+
}
1567+
textContent, ok := resp.Content[0].(mcp.TextContent)
1568+
require.True(t, ok)
1569+
var notifications []struct {
1570+
ID string `json:"id"`
1571+
}
1572+
err = json.Unmarshal([]byte(textContent.Text), &notifications)
1573+
require.NoError(t, err)
1574+
require.NotEmpty(t, notifications)
1575+
notificationID := notifications[0].ID
1576+
1577+
// Ignore notification
1578+
ignoreReq := mcp.CallToolRequest{}
1579+
ignoreReq.Params.Name = "manage_notification_subscription"
1580+
ignoreReq.Params.Arguments = map[string]any{
1581+
"notificationID": notificationID,
1582+
"action": "ignore",
1583+
}
1584+
resp, err = client.CallTool(ctx, ignoreReq)
1585+
require.NoError(t, err)
1586+
require.False(t, resp.IsError)
1587+
textContent, ok = resp.Content[0].(mcp.TextContent)
1588+
require.True(t, ok)
1589+
require.Contains(t, textContent.Text, "ignored")
1590+
1591+
// Watch notification
1592+
watchReq := mcp.CallToolRequest{}
1593+
watchReq.Params.Name = "manage_notification_subscription"
1594+
watchReq.Params.Arguments = map[string]any{
1595+
"notificationID": notificationID,
1596+
"action": "watch",
1597+
}
1598+
resp, err = client.CallTool(ctx, watchReq)
1599+
require.NoError(t, err)
1600+
require.False(t, resp.IsError)
1601+
textContent, ok = resp.Content[0].(mcp.TextContent)
1602+
require.True(t, ok)
1603+
require.Contains(t, textContent.Text, "subscribed")
1604+
1605+
// Delete notification subscription
1606+
deleteReq := mcp.CallToolRequest{}
1607+
deleteReq.Params.Name = "manage_notification_subscription"
1608+
deleteReq.Params.Arguments = map[string]any{
1609+
"notificationID": notificationID,
1610+
"action": "delete",
1611+
}
1612+
resp, err = client.CallTool(ctx, deleteReq)
1613+
require.NoError(t, err)
1614+
require.False(t, resp.IsError)
1615+
textContent, ok = resp.Content[0].(mcp.TextContent)
1616+
require.True(t, ok)
1617+
require.Contains(t, textContent.Text, "deleted")
1618+
}
1619+
1620+
func TestE2E_ManageRepositoryNotificationSubscription(t *testing.T) {
1621+
t.Parallel()
1622+
client := setupMCPClient(t)
1623+
ctx := context.Background()
1624+
1625+
// Use a well-known repo for the test (e.g., the user's own repo)
1626+
owner := "github"
1627+
repo := "github-mcp-server"
1628+
1629+
// Ignore repo notifications
1630+
ignoreReq := mcp.CallToolRequest{}
1631+
ignoreReq.Params.Name = "manage_repository_notification_subscription"
1632+
ignoreReq.Params.Arguments = map[string]any{
1633+
"owner": owner,
1634+
"repo": repo,
1635+
"action": "ignore",
1636+
}
1637+
resp, err := client.CallTool(ctx, ignoreReq)
1638+
require.NoError(t, err)
1639+
require.False(t, resp.IsError)
1640+
textContent, ok := resp.Content[0].(mcp.TextContent)
1641+
require.True(t, ok)
1642+
require.Contains(t, textContent.Text, "ignored")
1643+
1644+
// Watch repo notifications
1645+
watchReq := mcp.CallToolRequest{}
1646+
watchReq.Params.Name = "manage_repository_notification_subscription"
1647+
watchReq.Params.Arguments = map[string]any{
1648+
"owner": owner,
1649+
"repo": repo,
1650+
"action": "watch",
1651+
}
1652+
resp, err = client.CallTool(ctx, watchReq)
1653+
require.NoError(t, err)
1654+
require.False(t, resp.IsError)
1655+
textContent, ok = resp.Content[0].(mcp.TextContent)
1656+
require.True(t, ok)
1657+
require.Contains(t, textContent.Text, "subscribed")
1658+
1659+
// Delete repo notification subscription
1660+
deleteReq := mcp.CallToolRequest{}
1661+
deleteReq.Params.Name = "manage_repository_notification_subscription"
1662+
deleteReq.Params.Arguments = map[string]any{
1663+
"owner": owner,
1664+
"repo": repo,
1665+
"action": "delete",
1666+
}
1667+
resp, err = client.CallTool(ctx, deleteReq)
1668+
require.NoError(t, err)
1669+
require.False(t, resp.IsError)
1670+
textContent, ok = resp.Content[0].(mcp.TextContent)
1671+
require.True(t, ok)
1672+
require.Contains(t, textContent.Text, "deleted")
1673+
}
1674+
1675+
func TestE2E_DismissNotification(t *testing.T) {
1676+
t.Parallel()
1677+
client := setupMCPClient(t)
1678+
ctx := context.Background()
1679+
1680+
// List notifications to get a valid threadID
1681+
listReq := mcp.CallToolRequest{}
1682+
listReq.Params.Name = "list_notifications"
1683+
listReq.Params.Arguments = map[string]any{}
1684+
resp, err := client.CallTool(ctx, listReq)
1685+
require.NoError(t, err)
1686+
require.False(t, resp.IsError)
1687+
if len(resp.Content) == 0 {
1688+
return t.Skip("No notifications available to test dismissal")
1689+
}
1690+
textContent, ok := resp.Content[0].(mcp.TextContent)
1691+
require.True(t, ok)
1692+
var notifications []struct {
1693+
ID string `json:"id"`
1694+
}
1695+
err = json.Unmarshal([]byte(textContent.Text), &notifications)
1696+
require.NoError(t, err)
1697+
require.NotEmpty(t, notifications)
1698+
threadID := notifications[0].ID
1699+
1700+
// Dismiss notification (mark as read)
1701+
dismissReq := mcp.CallToolRequest{}
1702+
dismissReq.Params.Name = "dismiss_notification"
1703+
dismissReq.Params.Arguments = map[string]any{
1704+
"threadID": threadID,
1705+
"state": "read",
1706+
}
1707+
resp, err = client.CallTool(ctx, dismissReq)
1708+
require.NoError(t, err)
1709+
require.False(t, resp.IsError)
1710+
textContent, ok = resp.Content[0].(mcp.TextContent)
1711+
require.True(t, ok)
1712+
require.Contains(t, textContent.Text, "read")
1713+
}
1714+
1715+
func TestE2E_MarkAllNotificationsRead(t *testing.T) {
1716+
t.Parallel()
1717+
client := setupMCPClient(t)
1718+
ctx := context.Background()
1719+
1720+
// Limit to notifications updated within the last hour
1721+
oneHourAgo := nowMinusOneHourRFC3339()
1722+
markAllReq := mcp.CallToolRequest{}
1723+
markAllReq.Params.Name = "mark_all_notifications_read"
1724+
markAllReq.Params.Arguments = map[string]any{
1725+
"since": oneHourAgo,
1726+
}
1727+
resp, err := client.CallTool(ctx, markAllReq)
1728+
require.NoError(t, err)
1729+
require.False(t, resp.IsError)
1730+
textContent, ok := resp.Content[0].(mcp.TextContent)
1731+
require.True(t, ok)
1732+
require.Contains(t, textContent.Text, "All notifications marked as read")
1733+
1734+
}
1735+
1736+
// nowMinusOneHourRFC3339 returns the RFC3339 timestamp for one hour ago from now (UTC)
1737+
func nowMinusOneHourRFC3339() string {
1738+
return time.Now().UTC().Add(-1 * time.Hour).Format(time.RFC3339)
1739+
}
1740+
1741+
func TestE2E_GetNotificationDetails(t *testing.T) {
1742+
t.Parallel()
1743+
client := setupMCPClient(t)
1744+
ctx := context.Background()
1745+
1746+
// List notifications to get a valid notificationID
1747+
listReq := mcp.CallToolRequest{}
1748+
listReq.Params.Name = "list_notifications"
1749+
listReq.Params.Arguments = map[string]any{}
1750+
resp, err := client.CallTool(ctx, listReq)
1751+
require.NoError(t, err)
1752+
require.False(t, resp.IsError)
1753+
textContent, ok := resp.Content[0].(mcp.TextContent)
1754+
require.True(t, ok)
1755+
var notifications []struct {
1756+
ID string `json:"id"`
1757+
}
1758+
err = json.Unmarshal([]byte(textContent.Text), &notifications)
1759+
require.NoError(t, err)
1760+
require.NotEmpty(t, notifications)
1761+
notificationID := notifications[0].ID
1762+
1763+
// Get notification details
1764+
detailsReq := mcp.CallToolRequest{}
1765+
detailsReq.Params.Name = "get_notification_details"
1766+
detailsReq.Params.Arguments = map[string]any{
1767+
"notificationID": notificationID,
1768+
}
1769+
resp, err = client.CallTool(ctx, detailsReq)
1770+
require.NoError(t, err)
1771+
require.False(t, resp.IsError)
1772+
textContent, ok = resp.Content[0].(mcp.TextContent)
1773+
require.True(t, ok)
1774+
require.Contains(t, textContent.Text, notificationID)
1775+
}

pkg/github/notifications.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const (
2424
// ListNotifications creates a tool to list notifications for the current user.
2525
func ListNotifications(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
2626
return mcp.NewTool("list_notifications",
27-
mcp.WithDescription(t("TOOL_LIST_NOTIFICATIONS_DESCRIPTION", "List current notifications for the authenticated GitHub user")),
27+
mcp.WithDescription(t("TOOL_LIST_NOTIFICATIONS_DESCRIPTION", "Lists all GitHub notifications for the authenticated user, including unread notifications, mentions, review requests, assignments, and updates on issues or pull requests. Use this tool whenever the user asks what to work on next, requests a summary of their GitHub activity, wants to see pending reviews, or needs to check for new updates or tasks. This tool is the primary way to discover actionable items, reminders, and outstanding work on GitHub. Always call this tool when asked what to work on next, what is pending, or what needs attention in GitHub.")),
2828
mcp.WithToolAnnotation(mcp.ToolAnnotation{
2929
Title: t("TOOL_LIST_NOTIFICATIONS_USER_TITLE", "List notifications"),
3030
ReadOnlyHint: toBoolPtr(true),

0 commit comments

Comments
 (0)