Skip to content

Commit 9878a8a

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

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", "List current notifications for the authenticated GitHub user, check these from time to time, especially if you know the user is involved in discussions, issues or pull requests.")),
2828
mcp.WithToolAnnotation(mcp.ToolAnnotation{
2929
Title: t("TOOL_LIST_NOTIFICATIONS_USER_TITLE", "List notifications"),
3030
ReadOnlyHint: toBoolPtr(true),

0 commit comments

Comments
 (0)