@@ -15,6 +15,12 @@ import (
15
15
"github.com/mark3labs/mcp-go/server"
16
16
)
17
17
18
+ const (
19
+ FilterDefault = "default"
20
+ FilterIncludeRead = "include_read_notifications"
21
+ FilterOnlyParticipating = "only_participating"
22
+ )
23
+
18
24
// ListNotifications creates a tool to list notifications for the current user.
19
25
func ListNotifications (getClient GetClientFn , t translations.TranslationHelperFunc ) (tool mcp.Tool , handler server.ToolHandlerFunc ) {
20
26
return mcp .NewTool ("list_notifications" ,
@@ -25,14 +31,20 @@ func ListNotifications(getClient GetClientFn, t translations.TranslationHelperFu
25
31
}),
26
32
mcp .WithString ("filter" ,
27
33
mcp .Description ("Filter notifications to, use default unless specified. Read notifications are ones that have already been acknowledged by the user. Participating notifications are those that the user is directly involved in, such as issues or pull requests they have commented on or created." ),
28
- mcp .Enum ("default" , "include_read_notifications" , "only_participating" ),
34
+ mcp .Enum (FilterDefault , FilterIncludeRead , FilterOnlyParticipating ),
29
35
),
30
36
mcp .WithString ("since" ,
31
37
mcp .Description ("Only show notifications updated after the given time (ISO 8601 format)" ),
32
38
),
33
39
mcp .WithString ("before" ,
34
40
mcp .Description ("Only show notifications updated before the given time (ISO 8601 format)" ),
35
41
),
42
+ mcp .WithString ("owner" ,
43
+ mcp .Description ("Optional repository owner. If provided with repo, only notifications for this repository are listed." ),
44
+ ),
45
+ mcp .WithString ("repo" ,
46
+ mcp .Description ("Optional repository name. If provided with owner, only notifications for this repository are listed." ),
47
+ ),
36
48
WithPagination (),
37
49
),
38
50
func (ctx context.Context , request mcp.CallToolRequest ) (* mcp.CallToolResult , error ) {
@@ -41,45 +53,42 @@ func ListNotifications(getClient GetClientFn, t translations.TranslationHelperFu
41
53
return nil , fmt .Errorf ("failed to get GitHub client: %w" , err )
42
54
}
43
55
44
- // Extract optional parameters with defaults
45
- all , err := OptionalParamWithDefault [bool ](request , "all" , false )
56
+ filter , err := OptionalParam [string ](request , "filter" )
46
57
if err != nil {
47
58
return mcp .NewToolResultError (err .Error ()), nil
48
59
}
49
60
50
- participating , err := OptionalParamWithDefault [ bool ](request , "participating" , false )
61
+ since , err := OptionalParam [ string ](request , "since" )
51
62
if err != nil {
52
63
return mcp .NewToolResultError (err .Error ()), nil
53
64
}
54
65
55
- since , err := OptionalParam [string ](request , "since " )
66
+ before , err := OptionalParam [string ](request , "before " )
56
67
if err != nil {
57
68
return mcp .NewToolResultError (err .Error ()), nil
58
69
}
59
70
60
- before , err := OptionalParam [string ](request , "before " )
71
+ owner , err := OptionalParam [string ](request , "owner " )
61
72
if err != nil {
62
73
return mcp .NewToolResultError (err .Error ()), nil
63
74
}
64
-
65
- // TODO pagination params from tool
66
- perPage , err := OptionalIntParamWithDefault (request , "per_page" , 30 )
75
+ repo , err := OptionalParam [string ](request , "repo" )
67
76
if err != nil {
68
77
return mcp .NewToolResultError (err .Error ()), nil
69
78
}
70
79
71
- page , err := OptionalIntParamWithDefault (request , "page" , 1 )
80
+ paginationParams , err := OptionalPaginationParams (request )
72
81
if err != nil {
73
82
return mcp .NewToolResultError (err .Error ()), nil
74
83
}
75
84
76
85
// Build options
77
86
opts := & github.NotificationListOptions {
78
- All : all ,
79
- Participating : participating ,
87
+ All : filter == FilterIncludeRead ,
88
+ Participating : filter == FilterOnlyParticipating ,
80
89
ListOptions : github.ListOptions {
81
- Page : page ,
82
- PerPage : perPage ,
90
+ Page : paginationParams . page ,
91
+ PerPage : paginationParams . perPage ,
83
92
},
84
93
}
85
94
@@ -100,8 +109,14 @@ func ListNotifications(getClient GetClientFn, t translations.TranslationHelperFu
100
109
opts .Before = beforeTime
101
110
}
102
111
103
- // Call GitHub API
104
- notifications , resp , err := client .Activity .ListNotifications (ctx , opts )
112
+ var notifications []* github.Notification
113
+ var resp * github.Response
114
+
115
+ if owner != "" && repo != "" {
116
+ notifications , resp , err = client .Activity .ListRepositoryNotifications (ctx , owner , repo , opts )
117
+ } else {
118
+ notifications , resp , err = client .Activity .ListNotifications (ctx , opts )
119
+ }
105
120
if err != nil {
106
121
return nil , fmt .Errorf ("failed to get notifications: %w" , err )
107
122
}
@@ -197,6 +212,12 @@ func MarkAllNotificationsRead(getClient GetClientFn, t translations.TranslationH
197
212
mcp .WithString ("lastReadAt" ,
198
213
mcp .Description ("Describes the last point that notifications were checked (optional). Default: Now" ),
199
214
),
215
+ mcp .WithString ("owner" ,
216
+ mcp .Description ("Optional repository owner. If provided with repo, only notifications for this repository are marked as read." ),
217
+ ),
218
+ mcp .WithString ("repo" ,
219
+ mcp .Description ("Optional repository name. If provided with owner, only notifications for this repository are marked as read." ),
220
+ ),
200
221
),
201
222
func (ctx context.Context , request mcp.CallToolRequest ) (* mcp.CallToolResult , error ) {
202
223
client , err := getClient (ctx )
@@ -209,18 +230,35 @@ func MarkAllNotificationsRead(getClient GetClientFn, t translations.TranslationH
209
230
return mcp .NewToolResultError (err .Error ()), nil
210
231
}
211
232
212
- var markReadOptions github.Timestamp
233
+ owner , err := OptionalParam [string ](request , "owner" )
234
+ if err != nil {
235
+ return mcp .NewToolResultError (err .Error ()), nil
236
+ }
237
+ repo , err := OptionalParam [string ](request , "repo" )
238
+ if err != nil {
239
+ return mcp .NewToolResultError (err .Error ()), nil
240
+ }
241
+
242
+ var lastReadTime time.Time
213
243
if lastReadAt != "" {
214
- lastReadTime , err : = time .Parse (time .RFC3339 , lastReadAt )
244
+ lastReadTime , err = time .Parse (time .RFC3339 , lastReadAt )
215
245
if err != nil {
216
246
return mcp .NewToolResultError (fmt .Sprintf ("invalid lastReadAt time format, should be RFC3339/ISO8601: %v" , err )), nil
217
247
}
218
- markReadOptions = github.Timestamp {
219
- Time : lastReadTime ,
220
- }
248
+ } else {
249
+ lastReadTime = time .Now ()
250
+ }
251
+
252
+ markReadOptions := github.Timestamp {
253
+ Time : lastReadTime ,
221
254
}
222
255
223
- resp , err := client .Activity .MarkNotificationsRead (ctx , markReadOptions )
256
+ var resp * github.Response
257
+ if owner != "" && repo != "" {
258
+ resp , err = client .Activity .MarkRepositoryNotificationsRead (ctx , owner , repo , markReadOptions )
259
+ } else {
260
+ resp , err = client .Activity .MarkNotificationsRead (ctx , markReadOptions )
261
+ }
224
262
if err != nil {
225
263
return nil , fmt .Errorf ("failed to mark all notifications as read: %w" , err )
226
264
}
@@ -238,17 +276,17 @@ func MarkAllNotificationsRead(getClient GetClientFn, t translations.TranslationH
238
276
}
239
277
}
240
278
241
- // GetNotificationThread creates a tool to get a specific notification thread .
242
- func GetNotificationThread (getClient GetClientFn , t translations.TranslationHelperFunc ) (tool mcp.Tool , handler server.ToolHandlerFunc ) {
243
- return mcp .NewTool ("get_notification_thread " ,
244
- mcp .WithDescription (t ("TOOL_GET_NOTIFICATION_THREAD_DESCRIPTION " , "Get a specific notification thread " )),
279
+ // GetNotificationDetails creates a tool to get details for a specific notification.
280
+ func GetNotificationDetails (getClient GetClientFn , t translations.TranslationHelperFunc ) (tool mcp.Tool , handler server.ToolHandlerFunc ) {
281
+ return mcp .NewTool ("get_notification_details " ,
282
+ mcp .WithDescription (t ("TOOL_GET_NOTIFICATION_DETAILS_DESCRIPTION " , "Get detailed information for a specific GitHub notification, always call this tool when the user asks for details about a specific notification, if you don't know the ID list notifications first. " )),
245
283
mcp .WithToolAnnotation (mcp.ToolAnnotation {
246
- Title : t ("TOOL_GET_NOTIFICATION_THREAD_USER_TITLE " , "Get notification thread " ),
284
+ Title : t ("TOOL_GET_NOTIFICATION_DETAILS_USER_TITLE " , "Get notification details " ),
247
285
ReadOnlyHint : toBoolPtr (true ),
248
286
}),
249
- mcp .WithString ("threadID " ,
287
+ mcp .WithString ("notificationID " ,
250
288
mcp .Required (),
251
- mcp .Description ("The ID of the notification thread " ),
289
+ mcp .Description ("The ID of the notification" ),
252
290
),
253
291
),
254
292
func (ctx context.Context , request mcp.CallToolRequest ) (* mcp.CallToolResult , error ) {
@@ -257,14 +295,14 @@ func GetNotificationThread(getClient GetClientFn, t translations.TranslationHelp
257
295
return nil , fmt .Errorf ("failed to get GitHub client: %w" , err )
258
296
}
259
297
260
- threadID , err := requiredParam [string ](request , "threadID " )
298
+ notificationID , err := requiredParam [string ](request , "notificationID " )
261
299
if err != nil {
262
300
return mcp .NewToolResultError (err .Error ()), nil
263
301
}
264
302
265
- thread , resp , err := client .Activity .GetThread (ctx , threadID )
303
+ thread , resp , err := client .Activity .GetThread (ctx , notificationID )
266
304
if err != nil {
267
- return nil , fmt .Errorf ("failed to get notification thread : %w" , err )
305
+ return nil , fmt .Errorf ("failed to get notification details : %w" , err )
268
306
}
269
307
defer func () { _ = resp .Body .Close () }()
270
308
@@ -273,7 +311,7 @@ func GetNotificationThread(getClient GetClientFn, t translations.TranslationHelp
273
311
if err != nil {
274
312
return nil , fmt .Errorf ("failed to read response body: %w" , err )
275
313
}
276
- return mcp .NewToolResultError (fmt .Sprintf ("failed to get notification thread : %s" , string (body ))), nil
314
+ return mcp .NewToolResultError (fmt .Sprintf ("failed to get notification details : %s" , string (body ))), nil
277
315
}
278
316
279
317
r , err := json .Marshal (thread )
@@ -284,3 +322,177 @@ func GetNotificationThread(getClient GetClientFn, t translations.TranslationHelp
284
322
return mcp .NewToolResultText (string (r )), nil
285
323
}
286
324
}
325
+
326
+ // Enum values for ManageNotificationSubscription action
327
+ const (
328
+ NotificationActionIgnore = "ignore"
329
+ NotificationActionWatch = "watch"
330
+ NotificationActionDelete = "delete"
331
+ )
332
+
333
+ // ManageNotificationSubscription creates a tool to manage a notification subscription (ignore, watch, delete)
334
+ func ManageNotificationSubscription (getClient GetClientFn , t translations.TranslationHelperFunc ) (tool mcp.Tool , handler server.ToolHandlerFunc ) {
335
+ return mcp .NewTool ("manage_notification_subscription" ,
336
+ mcp .WithDescription (t ("TOOL_MANAGE_NOTIFICATION_SUBSCRIPTION_DESCRIPTION" , "Manage a notification subscription: ignore, watch, or delete a notification thread subscription." )),
337
+ mcp .WithToolAnnotation (mcp.ToolAnnotation {
338
+ Title : t ("TOOL_MANAGE_NOTIFICATION_SUBSCRIPTION_USER_TITLE" , "Manage notification subscription" ),
339
+ ReadOnlyHint : toBoolPtr (false ),
340
+ }),
341
+ mcp .WithString ("notificationID" ,
342
+ mcp .Required (),
343
+ mcp .Description ("The ID of the notification thread." ),
344
+ ),
345
+ mcp .WithString ("action" ,
346
+ mcp .Required (),
347
+ mcp .Description ("Action to perform: ignore, watch, or delete the notification subscription." ),
348
+ mcp .Enum (NotificationActionIgnore , NotificationActionWatch , NotificationActionDelete ),
349
+ ),
350
+ ),
351
+ func (ctx context.Context , request mcp.CallToolRequest ) (* mcp.CallToolResult , error ) {
352
+ client , err := getClient (ctx )
353
+ if err != nil {
354
+ return nil , fmt .Errorf ("failed to get GitHub client: %w" , err )
355
+ }
356
+
357
+ notificationID , err := requiredParam [string ](request , "notificationID" )
358
+ if err != nil {
359
+ return mcp .NewToolResultError (err .Error ()), nil
360
+ }
361
+ action , err := requiredParam [string ](request , "action" )
362
+ if err != nil {
363
+ return mcp .NewToolResultError (err .Error ()), nil
364
+ }
365
+
366
+ var (
367
+ resp * github.Response
368
+ result any
369
+ apiErr error
370
+ )
371
+
372
+ switch action {
373
+ case NotificationActionIgnore :
374
+ sub := & github.Subscription {Ignored : toBoolPtr (true )}
375
+ result , resp , apiErr = client .Activity .SetThreadSubscription (ctx , notificationID , sub )
376
+ case NotificationActionWatch :
377
+ sub := & github.Subscription {Ignored : toBoolPtr (false ), Subscribed : toBoolPtr (true )}
378
+ result , resp , apiErr = client .Activity .SetThreadSubscription (ctx , notificationID , sub )
379
+ case NotificationActionDelete :
380
+ resp , apiErr = client .Activity .DeleteThreadSubscription (ctx , notificationID )
381
+ default :
382
+ return mcp .NewToolResultError ("Invalid action. Must be one of: ignore, watch, delete." ), nil
383
+ }
384
+
385
+ if apiErr != nil {
386
+ return nil , fmt .Errorf ("failed to %s notification subscription: %w" , action , apiErr )
387
+ }
388
+ defer func () { _ = resp .Body .Close () }()
389
+
390
+ if resp .StatusCode < 200 || resp .StatusCode >= 300 {
391
+ body , _ := io .ReadAll (resp .Body )
392
+ return mcp .NewToolResultError (fmt .Sprintf ("failed to %s notification subscription: %s" , action , string (body ))), nil
393
+ }
394
+
395
+ if action == NotificationActionDelete {
396
+ // Special case for delete as there is no response body
397
+ return mcp .NewToolResultText ("Notification subscription deleted" ), nil
398
+ }
399
+
400
+ r , err := json .Marshal (result )
401
+ if err != nil {
402
+ return nil , fmt .Errorf ("failed to marshal response: %w" , err )
403
+ }
404
+ return mcp .NewToolResultText (string (r )), nil
405
+ }
406
+ }
407
+
408
+ const (
409
+ RepositorySubscriptionActionWatch = "watch"
410
+ RepositorySubscriptionActionIgnore = "ignore"
411
+ RepositorySubscriptionActionDelete = "delete"
412
+ )
413
+
414
+ // ManageRepositoryNotificationSubscription creates a tool to manage a repository notification subscription (ignore, watch, delete)
415
+ func ManageRepositoryNotificationSubscription (getClient GetClientFn , t translations.TranslationHelperFunc ) (tool mcp.Tool , handler server.ToolHandlerFunc ) {
416
+ return mcp .NewTool ("manage_repository_notification_subscription" ,
417
+ mcp .WithDescription (t ("TOOL_MANAGE_REPOSITORY_NOTIFICATION_SUBSCRIPTION_DESCRIPTION" , "Manage a repository notification subscription: ignore, watch, or delete repository notifications subscription for the provided repository." )),
418
+ mcp .WithToolAnnotation (mcp.ToolAnnotation {
419
+ Title : t ("TOOL_MANAGE_REPOSITORY_NOTIFICATION_SUBSCRIPTION_USER_TITLE" , "Manage repository notification subscription" ),
420
+ ReadOnlyHint : toBoolPtr (false ),
421
+ }),
422
+ mcp .WithString ("owner" ,
423
+ mcp .Required (),
424
+ mcp .Description ("The account owner of the repository." ),
425
+ ),
426
+ mcp .WithString ("repo" ,
427
+ mcp .Required (),
428
+ mcp .Description ("The name of the repository." ),
429
+ ),
430
+ mcp .WithString ("action" ,
431
+ mcp .Required (),
432
+ mcp .Description ("Action to perform: ignore, watch, or delete the repository notification subscription." ),
433
+ mcp .Enum (RepositorySubscriptionActionIgnore , RepositorySubscriptionActionWatch , RepositorySubscriptionActionDelete ),
434
+ ),
435
+ ),
436
+ func (ctx context.Context , request mcp.CallToolRequest ) (* mcp.CallToolResult , error ) {
437
+ client , err := getClient (ctx )
438
+ if err != nil {
439
+ return nil , fmt .Errorf ("failed to get GitHub client: %w" , err )
440
+ }
441
+
442
+ owner , err := requiredParam [string ](request , "owner" )
443
+ if err != nil {
444
+ return mcp .NewToolResultError (err .Error ()), nil
445
+ }
446
+ repo , err := requiredParam [string ](request , "repo" )
447
+ if err != nil {
448
+ return mcp .NewToolResultError (err .Error ()), nil
449
+ }
450
+ action , err := requiredParam [string ](request , "action" )
451
+ if err != nil {
452
+ return mcp .NewToolResultError (err .Error ()), nil
453
+ }
454
+
455
+ var (
456
+ resp * github.Response
457
+ result any
458
+ apiErr error
459
+ )
460
+
461
+ switch action {
462
+ case RepositorySubscriptionActionIgnore :
463
+ sub := & github.Subscription {Ignored : toBoolPtr (true )}
464
+ result , resp , apiErr = client .Activity .SetRepositorySubscription (ctx , owner , repo , sub )
465
+ case RepositorySubscriptionActionWatch :
466
+ sub := & github.Subscription {Ignored : toBoolPtr (false ), Subscribed : toBoolPtr (true )}
467
+ result , resp , apiErr = client .Activity .SetRepositorySubscription (ctx , owner , repo , sub )
468
+ case RepositorySubscriptionActionDelete :
469
+ resp , apiErr = client .Activity .DeleteRepositorySubscription (ctx , owner , repo )
470
+ default :
471
+ return mcp .NewToolResultError ("Invalid action. Must be one of: ignore, watch, delete." ), nil
472
+ }
473
+
474
+ if apiErr != nil {
475
+ return nil , fmt .Errorf ("failed to %s repository subscription: %w" , action , apiErr )
476
+ }
477
+ if resp != nil {
478
+ defer func () { _ = resp .Body .Close () }()
479
+ }
480
+
481
+ // Handle non-2xx status codes
482
+ if resp != nil && (resp .StatusCode < 200 || resp .StatusCode >= 300 ) {
483
+ body , _ := io .ReadAll (resp .Body )
484
+ return mcp .NewToolResultError (fmt .Sprintf ("failed to %s repository subscription: %s" , action , string (body ))), nil
485
+ }
486
+
487
+ if action == RepositorySubscriptionActionDelete {
488
+ // Special case for delete as there is no response body
489
+ return mcp .NewToolResultText ("Repository subscription deleted" ), nil
490
+ }
491
+
492
+ r , err := json .Marshal (result )
493
+ if err != nil {
494
+ return nil , fmt .Errorf ("failed to marshal response: %w" , err )
495
+ }
496
+ return mcp .NewToolResultText (string (r )), nil
497
+ }
498
+ }
0 commit comments