@rui.branco/sentry-mcp 1.0.6 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +246 -246
- package/assets/icon.svg +1 -0
- package/icons.js +22 -0
- package/index.js +1399 -1389
- package/package.json +37 -35
- package/setup.js +267 -266
package/index.js
CHANGED
|
@@ -1,1389 +1,1399 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// Handle setup command
|
|
4
|
-
if (process.argv[2] === "setup") {
|
|
5
|
-
require("./setup.js");
|
|
6
|
-
process.exit(0);
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const { Server } = require("@modelcontextprotocol/sdk/server/index.js");
|
|
10
|
-
const {
|
|
11
|
-
StdioServerTransport,
|
|
12
|
-
} = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
13
|
-
const {
|
|
14
|
-
CallToolRequestSchema,
|
|
15
|
-
ListToolsRequestSchema,
|
|
16
|
-
} = require("@modelcontextprotocol/sdk/types.js");
|
|
17
|
-
const fs = require("fs");
|
|
18
|
-
const os = require("os");
|
|
19
|
-
const path = require("path");
|
|
20
|
-
const fetch = require("node-fetch");
|
|
21
|
-
const {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
instances.find((i) => i.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
function
|
|
108
|
-
if (!
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
.
|
|
248
|
-
.replace(
|
|
249
|
-
.
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
);
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
},
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
},
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
required: [],
|
|
773
|
-
},
|
|
774
|
-
},
|
|
775
|
-
{
|
|
776
|
-
name: "
|
|
777
|
-
description:
|
|
778
|
-
"
|
|
779
|
-
inputSchema: {
|
|
780
|
-
type: "object",
|
|
781
|
-
properties: {
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
description:
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
},
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
const
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
if
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
};
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
);
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
const
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
const
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
if (
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
};
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
{
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
const
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
);
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
const
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
{
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
if (
|
|
1327
|
-
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Handle setup command
|
|
4
|
+
if (process.argv[2] === "setup") {
|
|
5
|
+
require("./setup.js");
|
|
6
|
+
process.exit(0);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { Server } = require("@modelcontextprotocol/sdk/server/index.js");
|
|
10
|
+
const {
|
|
11
|
+
StdioServerTransport,
|
|
12
|
+
} = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
13
|
+
const {
|
|
14
|
+
CallToolRequestSchema,
|
|
15
|
+
ListToolsRequestSchema,
|
|
16
|
+
} = require("@modelcontextprotocol/sdk/types.js");
|
|
17
|
+
const fs = require("fs");
|
|
18
|
+
const os = require("os");
|
|
19
|
+
const path = require("path");
|
|
20
|
+
const fetch = require("node-fetch");
|
|
21
|
+
const { ICONS } = require("./icons.js");
|
|
22
|
+
const { spawn } = require("child_process");
|
|
23
|
+
|
|
24
|
+
const PKG_VERSION = require("./package.json").version;
|
|
25
|
+
|
|
26
|
+
// Auto-update: ask the npm registry on every start what the latest published
|
|
27
|
+
// version is, and install it in the background when it isn't the one we're
|
|
28
|
+
// running. The package ships through npm, so there's no git involved: a plain
|
|
29
|
+
// fetch plus an npm install. Runs detached from startup so the server begins
|
|
30
|
+
// serving stdio immediately.
|
|
31
|
+
const NPM_PACKAGE = "@rui.branco/sentry-mcp";
|
|
32
|
+
(async () => {
|
|
33
|
+
try {
|
|
34
|
+
const res = await fetch(
|
|
35
|
+
`https://registry.npmjs.org/${encodeURIComponent(NPM_PACKAGE)}/latest`,
|
|
36
|
+
{ signal: AbortSignal.timeout(5000) },
|
|
37
|
+
);
|
|
38
|
+
const latest = (await res.json()).version;
|
|
39
|
+
if (latest && latest !== PKG_VERSION) {
|
|
40
|
+
// Passed as one shell string rather than an argv array: npm is npm.cmd on
|
|
41
|
+
// Windows, and since Node 18.20.2 spawning a .cmd without a shell throws
|
|
42
|
+
// EINVAL outright (CVE-2024-27980 hardening). shell:true picks cmd.exe
|
|
43
|
+
// there and /bin/sh elsewhere, so this stays portable — unlike the old
|
|
44
|
+
// hardcoded `sh`, which simply doesn't exist on Windows. The command is a
|
|
45
|
+
// fixed literal with no interpolation, so there's nothing to inject.
|
|
46
|
+
const child = spawn(`npm install -g ${NPM_PACKAGE}`, {
|
|
47
|
+
shell: true,
|
|
48
|
+
stdio: "ignore",
|
|
49
|
+
});
|
|
50
|
+
// A spawn failure (npm missing from PATH) arrives as an async 'error'
|
|
51
|
+
// event, which no try/catch around the spawn can see — left unhandled it
|
|
52
|
+
// would take the whole server down at startup. Swallow it here.
|
|
53
|
+
child.on("error", () => {});
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
/* offline, slow registry, or bad response — no-op */
|
|
57
|
+
}
|
|
58
|
+
})();
|
|
59
|
+
|
|
60
|
+
// ── Config ──────────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
const configDir = path.join(os.homedir(), ".config/sentry-mcp");
|
|
63
|
+
const configPath = path.join(configDir, "config.json");
|
|
64
|
+
|
|
65
|
+
function loadConfigFile() {
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
68
|
+
} catch {
|
|
69
|
+
return {};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Load config (supports single-instance and multi-instance formats)
|
|
74
|
+
const rawConfig = loadConfigFile();
|
|
75
|
+
|
|
76
|
+
// Normalize config to multi-instance format
|
|
77
|
+
let instances;
|
|
78
|
+
if (rawConfig.instances) {
|
|
79
|
+
instances = rawConfig.instances.map((inst) => ({
|
|
80
|
+
...inst,
|
|
81
|
+
projects: (inst.projects || []).map((p) => p.toLowerCase()),
|
|
82
|
+
}));
|
|
83
|
+
} else if (rawConfig.token) {
|
|
84
|
+
// Old single-instance format — wrap as array
|
|
85
|
+
instances = [
|
|
86
|
+
{
|
|
87
|
+
name: "default",
|
|
88
|
+
token: rawConfig.token,
|
|
89
|
+
baseUrl: (rawConfig.baseUrl || "https://sentry.io").replace(/\/+$/, ""),
|
|
90
|
+
orgSlug: rawConfig.orgSlug,
|
|
91
|
+
projectSlug: rawConfig.projectSlug || "",
|
|
92
|
+
projects: [],
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
} else {
|
|
96
|
+
instances = [];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Resolve default instance
|
|
100
|
+
const defaultInstance =
|
|
101
|
+
(rawConfig.defaultInstance &&
|
|
102
|
+
instances.find((i) => i.name === rawConfig.defaultInstance)) ||
|
|
103
|
+
instances[0] ||
|
|
104
|
+
null;
|
|
105
|
+
|
|
106
|
+
// Instance resolution helpers
|
|
107
|
+
function getInstanceForProject(projectSlug) {
|
|
108
|
+
if (!projectSlug) return defaultInstance;
|
|
109
|
+
const lower = projectSlug.toLowerCase();
|
|
110
|
+
return (
|
|
111
|
+
instances.find((i) => i.projects.includes(lower)) ||
|
|
112
|
+
instances.find((i) => i.projectSlug === projectSlug) ||
|
|
113
|
+
defaultInstance
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getInstanceByName(name) {
|
|
118
|
+
if (!name) return defaultInstance;
|
|
119
|
+
return instances.find((i) => i.name === name) || defaultInstance;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── Sentry API ──────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
async function fetchSentry(apiPath, instance, options = {}) {
|
|
125
|
+
const inst = instance || defaultInstance;
|
|
126
|
+
if (!inst) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
"No Sentry instance configured. Run: sentry-mcp setup",
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (!inst.token) {
|
|
132
|
+
throw new Error(`Sentry API token not configured for instance "${inst.name}".`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const base = (inst.baseUrl || "https://sentry.io").replace(/\/+$/, "");
|
|
136
|
+
const url = `${base}${apiPath}`;
|
|
137
|
+
const { method = "GET", body } = options;
|
|
138
|
+
|
|
139
|
+
const headers = {
|
|
140
|
+
Authorization: `Bearer ${inst.token}`,
|
|
141
|
+
Accept: "application/json",
|
|
142
|
+
};
|
|
143
|
+
if (body) {
|
|
144
|
+
headers["Content-Type"] = "application/json";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const response = await fetch(url, {
|
|
148
|
+
method,
|
|
149
|
+
headers,
|
|
150
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (!response.ok) {
|
|
154
|
+
const errorBody = await response.text().catch(() => "");
|
|
155
|
+
let errorMsg = `Sentry API error: ${response.status} ${response.statusText}`;
|
|
156
|
+
try {
|
|
157
|
+
const parsed = JSON.parse(errorBody);
|
|
158
|
+
if (parsed.detail) errorMsg = parsed.detail;
|
|
159
|
+
else if (parsed.message) errorMsg = parsed.message;
|
|
160
|
+
} catch {}
|
|
161
|
+
throw new Error(errorMsg);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const text = await response.text();
|
|
165
|
+
return text ? JSON.parse(text) : {};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Same as fetchSentry but returns the raw response body as a Buffer (for
|
|
169
|
+
// binary downloads like event attachments). Keeps fetchSentry's JSON contract intact.
|
|
170
|
+
async function fetchSentryRaw(apiPath, instance, options = {}) {
|
|
171
|
+
const inst = instance || defaultInstance;
|
|
172
|
+
if (!inst) {
|
|
173
|
+
throw new Error("No Sentry instance configured. Run: sentry-mcp setup");
|
|
174
|
+
}
|
|
175
|
+
if (!inst.token) {
|
|
176
|
+
throw new Error(`Sentry API token not configured for instance "${inst.name}".`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const base = (inst.baseUrl || "https://sentry.io").replace(/\/+$/, "");
|
|
180
|
+
const url = `${base}${apiPath}`;
|
|
181
|
+
const { method = "GET" } = options;
|
|
182
|
+
|
|
183
|
+
const response = await fetch(url, {
|
|
184
|
+
method,
|
|
185
|
+
headers: {
|
|
186
|
+
Authorization: `Bearer ${inst.token}`,
|
|
187
|
+
Accept: "*/*",
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
if (!response.ok) {
|
|
192
|
+
const errorBody = await response.text().catch(() => "");
|
|
193
|
+
let errorMsg = `Sentry API error: ${response.status} ${response.statusText}`;
|
|
194
|
+
try {
|
|
195
|
+
const parsed = JSON.parse(errorBody);
|
|
196
|
+
if (parsed.detail) errorMsg = parsed.detail;
|
|
197
|
+
else if (parsed.message) errorMsg = parsed.message;
|
|
198
|
+
} catch {}
|
|
199
|
+
throw new Error(errorMsg);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return response.buffer();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
// Strip noisy/internal fields that waste context but keep everything useful
|
|
208
|
+
function cleanIssueData(issue) {
|
|
209
|
+
const skip = new Set(["pluginActions", "pluginContexts", "pluginIssues", "activity", "seenBy", "stats"]);
|
|
210
|
+
const cleaned = {};
|
|
211
|
+
for (const [k, v] of Object.entries(issue)) {
|
|
212
|
+
if (!skip.has(k) && v != null && v !== "") cleaned[k] = v;
|
|
213
|
+
}
|
|
214
|
+
return cleaned;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function cleanEventData(event) {
|
|
218
|
+
const skip = new Set(["_meta", "groupID", "dist", "fingerprints"]);
|
|
219
|
+
const cleaned = {};
|
|
220
|
+
for (const [k, v] of Object.entries(event)) {
|
|
221
|
+
if (!skip.has(k) && v != null && v !== "") cleaned[k] = v;
|
|
222
|
+
}
|
|
223
|
+
return cleaned;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Attachments are event-scoped. Accept a raw eventId (preferred) or resolve
|
|
227
|
+
// the issue's latest event when only an issueId is given.
|
|
228
|
+
async function resolveEventId(args, inst) {
|
|
229
|
+
if (args.eventId) return args.eventId;
|
|
230
|
+
if (args.issueId) {
|
|
231
|
+
const latest = await fetchSentry(
|
|
232
|
+
`/api/0/organizations/${encodeURIComponent(inst.orgSlug)}/issues/${encodeURIComponent(args.issueId)}/events/latest/`,
|
|
233
|
+
inst,
|
|
234
|
+
);
|
|
235
|
+
const id = latest.eventID || latest.id;
|
|
236
|
+
if (!id) throw new Error(`Could not resolve an event for issue ${args.issueId}.`);
|
|
237
|
+
return id;
|
|
238
|
+
}
|
|
239
|
+
throw new Error("Provide either eventId or issueId.");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Sanitize a server-provided attachment filename so it can never escape the
|
|
243
|
+
// target directory (path traversal) or introduce awkward characters.
|
|
244
|
+
function safeAttachmentName(name, fallback) {
|
|
245
|
+
const cleaned = path
|
|
246
|
+
.basename(String(name || fallback))
|
|
247
|
+
.replace(/[^a-zA-Z0-9._ -]/g, "_")
|
|
248
|
+
.replace(/^\.+/, "_")
|
|
249
|
+
.slice(0, 180);
|
|
250
|
+
return cleaned || fallback;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Reduce an id (eventId / attachmentId) to a single safe path segment so it
|
|
254
|
+
// cannot introduce separators or "../" traversal when building the default path.
|
|
255
|
+
function safePathSegment(value, fallback) {
|
|
256
|
+
const cleaned = path
|
|
257
|
+
.basename(String(value == null ? "" : value))
|
|
258
|
+
.replace(/[^a-zA-Z0-9._-]/g, "_")
|
|
259
|
+
.replace(/^\.+/, "_")
|
|
260
|
+
.slice(0, 120);
|
|
261
|
+
return cleaned || fallback;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function formatIssueSummary(issue) {
|
|
265
|
+
const meta = issue.metadata || {};
|
|
266
|
+
const lines = [];
|
|
267
|
+
lines.push(`**${issue.title}**`);
|
|
268
|
+
lines.push(`- ID: ${issue.id} | Short ID: ${issue.shortId || "N/A"}`);
|
|
269
|
+
lines.push(`- Level: ${issue.level || "error"} | Status: ${issue.status || "unresolved"}`);
|
|
270
|
+
lines.push(`- Events: ${issue.count || 0} | Users: ${issue.userCount || 0}`);
|
|
271
|
+
if (issue.culprit) lines.push(`- Culprit: ${issue.culprit}`);
|
|
272
|
+
if (meta.filename) lines.push(`- File: ${meta.filename}${meta.function ? " in " + meta.function : ""}`);
|
|
273
|
+
if (issue.firstSeen) lines.push(`- First seen: ${issue.firstSeen}`);
|
|
274
|
+
if (issue.lastSeen) lines.push(`- Last seen: ${issue.lastSeen}`);
|
|
275
|
+
if (issue.permalink) lines.push(`- URL: ${issue.permalink}`);
|
|
276
|
+
if (issue.assignedTo) {
|
|
277
|
+
const a = issue.assignedTo;
|
|
278
|
+
lines.push(`- Assigned to: ${a.name || a.email || a.id || "unknown"}`);
|
|
279
|
+
}
|
|
280
|
+
return lines.join("\n");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ── MCP Server ──────────────────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
const server = new Server(
|
|
286
|
+
{ name: "sentry-mcp", version: PKG_VERSION, icons: ICONS },
|
|
287
|
+
{ capabilities: { tools: {} } },
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
291
|
+
return {
|
|
292
|
+
tools: [
|
|
293
|
+
{
|
|
294
|
+
name: "sentry_list_projects",
|
|
295
|
+
description:
|
|
296
|
+
"List all Sentry projects accessible by the configured token. When multiple instances are configured, lists projects from each.",
|
|
297
|
+
inputSchema: {
|
|
298
|
+
type: "object",
|
|
299
|
+
properties: {
|
|
300
|
+
instance: {
|
|
301
|
+
type: "string",
|
|
302
|
+
description:
|
|
303
|
+
"Instance name (for multi-instance setups). Uses default instance if omitted.",
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
required: [],
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
name: "sentry_get_issues",
|
|
311
|
+
description:
|
|
312
|
+
"Get unresolved issues for a Sentry project. Returns a list of issues with their metadata.",
|
|
313
|
+
inputSchema: {
|
|
314
|
+
type: "object",
|
|
315
|
+
properties: {
|
|
316
|
+
query: {
|
|
317
|
+
type: "string",
|
|
318
|
+
description:
|
|
319
|
+
'Sentry search query (default: "is:unresolved"). Supports Sentry search syntax.',
|
|
320
|
+
},
|
|
321
|
+
instance: {
|
|
322
|
+
type: "string",
|
|
323
|
+
description:
|
|
324
|
+
"Instance name (for multi-instance setups). Uses default instance if omitted.",
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
required: [],
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
name: "sentry_get_issue",
|
|
332
|
+
description:
|
|
333
|
+
"Get full details for a specific Sentry issue by its numeric ID.",
|
|
334
|
+
inputSchema: {
|
|
335
|
+
type: "object",
|
|
336
|
+
properties: {
|
|
337
|
+
issueId: {
|
|
338
|
+
type: "string",
|
|
339
|
+
description: "Sentry issue ID (numeric).",
|
|
340
|
+
},
|
|
341
|
+
instance: {
|
|
342
|
+
type: "string",
|
|
343
|
+
description:
|
|
344
|
+
"Instance name (for multi-instance setups). Uses default instance if omitted.",
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
required: ["issueId"],
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
name: "sentry_get_latest_event",
|
|
352
|
+
description:
|
|
353
|
+
"Get the latest event for a Sentry issue — includes full stack trace, breadcrumbs, HTTP request, contexts, and tags.",
|
|
354
|
+
inputSchema: {
|
|
355
|
+
type: "object",
|
|
356
|
+
properties: {
|
|
357
|
+
issueId: {
|
|
358
|
+
type: "string",
|
|
359
|
+
description: "Sentry issue ID (numeric).",
|
|
360
|
+
},
|
|
361
|
+
instance: {
|
|
362
|
+
type: "string",
|
|
363
|
+
description:
|
|
364
|
+
"Instance name (for multi-instance setups). Uses default instance if omitted.",
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
required: ["issueId"],
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
name: "sentry_resolve_issue",
|
|
372
|
+
description: "Mark a Sentry issue as resolved.",
|
|
373
|
+
inputSchema: {
|
|
374
|
+
type: "object",
|
|
375
|
+
properties: {
|
|
376
|
+
issueId: {
|
|
377
|
+
type: "string",
|
|
378
|
+
description: "Sentry issue ID (numeric).",
|
|
379
|
+
},
|
|
380
|
+
instance: {
|
|
381
|
+
type: "string",
|
|
382
|
+
description:
|
|
383
|
+
"Instance name (for multi-instance setups). Uses default instance if omitted.",
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
required: ["issueId"],
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
name: "sentry_unresolve_issue",
|
|
391
|
+
description: "Reopen a resolved Sentry issue.",
|
|
392
|
+
inputSchema: {
|
|
393
|
+
type: "object",
|
|
394
|
+
properties: {
|
|
395
|
+
issueId: {
|
|
396
|
+
type: "string",
|
|
397
|
+
description: "Sentry issue ID (numeric).",
|
|
398
|
+
},
|
|
399
|
+
instance: {
|
|
400
|
+
type: "string",
|
|
401
|
+
description:
|
|
402
|
+
"Instance name (for multi-instance setups). Uses default instance if omitted.",
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
required: ["issueId"],
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
name: "sentry_ignore_issue",
|
|
410
|
+
description: "Ignore/snooze a Sentry issue.",
|
|
411
|
+
inputSchema: {
|
|
412
|
+
type: "object",
|
|
413
|
+
properties: {
|
|
414
|
+
issueId: {
|
|
415
|
+
type: "string",
|
|
416
|
+
description: "Sentry issue ID (numeric).",
|
|
417
|
+
},
|
|
418
|
+
instance: {
|
|
419
|
+
type: "string",
|
|
420
|
+
description:
|
|
421
|
+
"Instance name (for multi-instance setups). Uses default instance if omitted.",
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
required: ["issueId"],
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
name: "sentry_search_issues",
|
|
429
|
+
description:
|
|
430
|
+
"Search Sentry issues with a custom query. Supports full Sentry search syntax.",
|
|
431
|
+
inputSchema: {
|
|
432
|
+
type: "object",
|
|
433
|
+
properties: {
|
|
434
|
+
query: {
|
|
435
|
+
type: "string",
|
|
436
|
+
description:
|
|
437
|
+
'Sentry search query (e.g. "is:unresolved level:error", "TypeError", "assigned:me").',
|
|
438
|
+
},
|
|
439
|
+
instance: {
|
|
440
|
+
type: "string",
|
|
441
|
+
description:
|
|
442
|
+
"Instance name (for multi-instance setups). Uses default instance if omitted.",
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
required: ["query"],
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
name: "sentry_assign_issue",
|
|
450
|
+
description: "Assign a Sentry issue to a user or team.",
|
|
451
|
+
inputSchema: {
|
|
452
|
+
type: "object",
|
|
453
|
+
properties: {
|
|
454
|
+
issueId: {
|
|
455
|
+
type: "string",
|
|
456
|
+
description: "Sentry issue ID (numeric).",
|
|
457
|
+
},
|
|
458
|
+
assignee: {
|
|
459
|
+
type: "string",
|
|
460
|
+
description:
|
|
461
|
+
'Username, email, or team identifier to assign to (e.g. "username:john", "team:backend").',
|
|
462
|
+
},
|
|
463
|
+
instance: {
|
|
464
|
+
type: "string",
|
|
465
|
+
description:
|
|
466
|
+
"Instance name (for multi-instance setups). Uses default instance if omitted.",
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
required: ["issueId", "assignee"],
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
name: "sentry_list_releases",
|
|
474
|
+
description:
|
|
475
|
+
"List releases for a Sentry project. Returns release versions, dates, and artifact counts.",
|
|
476
|
+
inputSchema: {
|
|
477
|
+
type: "object",
|
|
478
|
+
properties: {
|
|
479
|
+
query: {
|
|
480
|
+
type: "string",
|
|
481
|
+
description:
|
|
482
|
+
"Optional search query to filter releases by version (e.g. '26.05').",
|
|
483
|
+
},
|
|
484
|
+
instance: {
|
|
485
|
+
type: "string",
|
|
486
|
+
description:
|
|
487
|
+
"Instance name (for multi-instance setups). Uses default instance if omitted.",
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
required: [],
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
name: "sentry_get_release_artifacts",
|
|
495
|
+
description:
|
|
496
|
+
"List artifacts (source maps, JS bundles) uploaded for a specific release. Use to verify source maps were uploaded correctly.",
|
|
497
|
+
inputSchema: {
|
|
498
|
+
type: "object",
|
|
499
|
+
properties: {
|
|
500
|
+
version: {
|
|
501
|
+
type: "string",
|
|
502
|
+
description: "Release version string (e.g. '26.05.4b185-TRAIN').",
|
|
503
|
+
},
|
|
504
|
+
instance: {
|
|
505
|
+
type: "string",
|
|
506
|
+
description:
|
|
507
|
+
"Instance name (for multi-instance setups). Uses default instance if omitted.",
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
required: ["version"],
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
name: "sentry_create_release",
|
|
515
|
+
description:
|
|
516
|
+
"Create a new release in Sentry. Optionally associate commits and set a ref.",
|
|
517
|
+
inputSchema: {
|
|
518
|
+
type: "object",
|
|
519
|
+
properties: {
|
|
520
|
+
version: {
|
|
521
|
+
type: "string",
|
|
522
|
+
description: "Release version string (e.g. '1.0.0', '26.05.4b185-TRAIN').",
|
|
523
|
+
},
|
|
524
|
+
ref: {
|
|
525
|
+
type: "string",
|
|
526
|
+
description: "Optional git ref (commit SHA or tag).",
|
|
527
|
+
},
|
|
528
|
+
projects: {
|
|
529
|
+
type: "array",
|
|
530
|
+
items: { type: "string" },
|
|
531
|
+
description: "Project slugs to associate with this release. Defaults to the instance's projectSlug.",
|
|
532
|
+
},
|
|
533
|
+
instance: {
|
|
534
|
+
type: "string",
|
|
535
|
+
description: "Instance name. Uses default if omitted.",
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
required: ["version"],
|
|
539
|
+
},
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
name: "sentry_finalize_release",
|
|
543
|
+
description:
|
|
544
|
+
"Finalize a release (mark it as deployed/complete). Sets dateReleased to now.",
|
|
545
|
+
inputSchema: {
|
|
546
|
+
type: "object",
|
|
547
|
+
properties: {
|
|
548
|
+
version: {
|
|
549
|
+
type: "string",
|
|
550
|
+
description: "Release version string.",
|
|
551
|
+
},
|
|
552
|
+
instance: {
|
|
553
|
+
type: "string",
|
|
554
|
+
description: "Instance name. Uses default if omitted.",
|
|
555
|
+
},
|
|
556
|
+
},
|
|
557
|
+
required: ["version"],
|
|
558
|
+
},
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
name: "sentry_delete_release",
|
|
562
|
+
description:
|
|
563
|
+
"Delete a release and all its artifacts from Sentry.",
|
|
564
|
+
inputSchema: {
|
|
565
|
+
type: "object",
|
|
566
|
+
properties: {
|
|
567
|
+
version: {
|
|
568
|
+
type: "string",
|
|
569
|
+
description: "Release version string to delete.",
|
|
570
|
+
},
|
|
571
|
+
instance: {
|
|
572
|
+
type: "string",
|
|
573
|
+
description: "Instance name. Uses default if omitted.",
|
|
574
|
+
},
|
|
575
|
+
},
|
|
576
|
+
required: ["version"],
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
name: "sentry_create_deploy",
|
|
581
|
+
description:
|
|
582
|
+
"Create a deploy for a release. Records when and where a release was deployed.",
|
|
583
|
+
inputSchema: {
|
|
584
|
+
type: "object",
|
|
585
|
+
properties: {
|
|
586
|
+
version: {
|
|
587
|
+
type: "string",
|
|
588
|
+
description: "Release version string.",
|
|
589
|
+
},
|
|
590
|
+
environment: {
|
|
591
|
+
type: "string",
|
|
592
|
+
description: "Environment name (e.g. 'production', 'staging', 'QA', 'TRAIN').",
|
|
593
|
+
},
|
|
594
|
+
name: {
|
|
595
|
+
type: "string",
|
|
596
|
+
description: "Optional deploy name.",
|
|
597
|
+
},
|
|
598
|
+
instance: {
|
|
599
|
+
type: "string",
|
|
600
|
+
description: "Instance name. Uses default if omitted.",
|
|
601
|
+
},
|
|
602
|
+
},
|
|
603
|
+
required: ["version", "environment"],
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
name: "sentry_list_deploys",
|
|
608
|
+
description:
|
|
609
|
+
"List deploys for a release.",
|
|
610
|
+
inputSchema: {
|
|
611
|
+
type: "object",
|
|
612
|
+
properties: {
|
|
613
|
+
version: {
|
|
614
|
+
type: "string",
|
|
615
|
+
description: "Release version string.",
|
|
616
|
+
},
|
|
617
|
+
instance: {
|
|
618
|
+
type: "string",
|
|
619
|
+
description: "Instance name. Uses default if omitted.",
|
|
620
|
+
},
|
|
621
|
+
},
|
|
622
|
+
required: ["version"],
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
name: "sentry_delete_artifact",
|
|
627
|
+
description:
|
|
628
|
+
"Delete a specific artifact from a release by its ID.",
|
|
629
|
+
inputSchema: {
|
|
630
|
+
type: "object",
|
|
631
|
+
properties: {
|
|
632
|
+
version: {
|
|
633
|
+
type: "string",
|
|
634
|
+
description: "Release version string.",
|
|
635
|
+
},
|
|
636
|
+
artifactId: {
|
|
637
|
+
type: "string",
|
|
638
|
+
description: "Artifact ID to delete (from sentry_get_release_artifacts).",
|
|
639
|
+
},
|
|
640
|
+
instance: {
|
|
641
|
+
type: "string",
|
|
642
|
+
description: "Instance name. Uses default if omitted.",
|
|
643
|
+
},
|
|
644
|
+
},
|
|
645
|
+
required: ["version", "artifactId"],
|
|
646
|
+
},
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
name: "sentry_set_commits",
|
|
650
|
+
description:
|
|
651
|
+
"Associate commits with a release. Provide a repository and commit range.",
|
|
652
|
+
inputSchema: {
|
|
653
|
+
type: "object",
|
|
654
|
+
properties: {
|
|
655
|
+
version: {
|
|
656
|
+
type: "string",
|
|
657
|
+
description: "Release version string.",
|
|
658
|
+
},
|
|
659
|
+
repository: {
|
|
660
|
+
type: "string",
|
|
661
|
+
description: "Repository name (e.g. 'org/repo').",
|
|
662
|
+
},
|
|
663
|
+
commit: {
|
|
664
|
+
type: "string",
|
|
665
|
+
description: "Current commit SHA (HEAD of the release).",
|
|
666
|
+
},
|
|
667
|
+
previousCommit: {
|
|
668
|
+
type: "string",
|
|
669
|
+
description: "Previous commit SHA (start of range). If omitted, uses the previous release's commit.",
|
|
670
|
+
},
|
|
671
|
+
instance: {
|
|
672
|
+
type: "string",
|
|
673
|
+
description: "Instance name. Uses default if omitted.",
|
|
674
|
+
},
|
|
675
|
+
},
|
|
676
|
+
required: ["version", "repository", "commit"],
|
|
677
|
+
},
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
name: "sentry_get_release",
|
|
681
|
+
description:
|
|
682
|
+
"Get full details for a specific release including projects, commits, authors, and deploy info.",
|
|
683
|
+
inputSchema: {
|
|
684
|
+
type: "object",
|
|
685
|
+
properties: {
|
|
686
|
+
version: {
|
|
687
|
+
type: "string",
|
|
688
|
+
description: "Release version string.",
|
|
689
|
+
},
|
|
690
|
+
instance: {
|
|
691
|
+
type: "string",
|
|
692
|
+
description: "Instance name. Uses default if omitted.",
|
|
693
|
+
},
|
|
694
|
+
},
|
|
695
|
+
required: ["version"],
|
|
696
|
+
},
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
name: "sentry_list_events",
|
|
700
|
+
description:
|
|
701
|
+
"List events for a Sentry issue. Returns event IDs, timestamps, user info, and message/title for each occurrence.",
|
|
702
|
+
inputSchema: {
|
|
703
|
+
type: "object",
|
|
704
|
+
properties: {
|
|
705
|
+
issueId: {
|
|
706
|
+
type: "string",
|
|
707
|
+
description: "Sentry issue ID (numeric).",
|
|
708
|
+
},
|
|
709
|
+
instance: {
|
|
710
|
+
type: "string",
|
|
711
|
+
description:
|
|
712
|
+
"Instance name (for multi-instance setups). Uses default instance if omitted.",
|
|
713
|
+
},
|
|
714
|
+
},
|
|
715
|
+
required: ["issueId"],
|
|
716
|
+
},
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
name: "sentry_list_event_attachments",
|
|
720
|
+
description:
|
|
721
|
+
"List attachments (minidumps, screenshots, logs, view hierarchies, etc.) stored on a Sentry event. Provide either an eventId (32-char hex) or an issueId (resolves to the issue's latest event). Uses the instance's configured projectSlug.",
|
|
722
|
+
inputSchema: {
|
|
723
|
+
type: "object",
|
|
724
|
+
properties: {
|
|
725
|
+
eventId: {
|
|
726
|
+
type: "string",
|
|
727
|
+
description: "Sentry event ID (32-char hex). Takes precedence over issueId.",
|
|
728
|
+
},
|
|
729
|
+
issueId: {
|
|
730
|
+
type: "string",
|
|
731
|
+
description: "Sentry issue ID (numeric). If eventId is omitted, the issue's latest event is used.",
|
|
732
|
+
},
|
|
733
|
+
instance: {
|
|
734
|
+
type: "string",
|
|
735
|
+
description:
|
|
736
|
+
"Instance name (for multi-instance setups). Uses default instance if omitted.",
|
|
737
|
+
},
|
|
738
|
+
},
|
|
739
|
+
required: [],
|
|
740
|
+
},
|
|
741
|
+
},
|
|
742
|
+
{
|
|
743
|
+
name: "sentry_download_event_attachment",
|
|
744
|
+
description:
|
|
745
|
+
"Download a Sentry event attachment to a file on disk and return the saved path. Provide the attachmentId (from sentry_list_event_attachments) plus either an eventId or issueId. By default saves into a temp directory; pass outputPath to choose a file or destination directory.",
|
|
746
|
+
inputSchema: {
|
|
747
|
+
type: "object",
|
|
748
|
+
properties: {
|
|
749
|
+
attachmentId: {
|
|
750
|
+
type: "string",
|
|
751
|
+
description: "Attachment ID to download (from sentry_list_event_attachments).",
|
|
752
|
+
},
|
|
753
|
+
eventId: {
|
|
754
|
+
type: "string",
|
|
755
|
+
description: "Sentry event ID (32-char hex). Takes precedence over issueId.",
|
|
756
|
+
},
|
|
757
|
+
issueId: {
|
|
758
|
+
type: "string",
|
|
759
|
+
description: "Sentry issue ID (numeric). If eventId is omitted, the issue's latest event is used.",
|
|
760
|
+
},
|
|
761
|
+
outputPath: {
|
|
762
|
+
type: "string",
|
|
763
|
+
description:
|
|
764
|
+
"Optional destination. If it is an existing directory (or ends with a slash), the file is saved inside it using the attachment's name; otherwise it is treated as the full file path. Relative paths resolve from the current working directory. Defaults to a temp directory.",
|
|
765
|
+
},
|
|
766
|
+
instance: {
|
|
767
|
+
type: "string",
|
|
768
|
+
description:
|
|
769
|
+
"Instance name (for multi-instance setups). Uses default instance if omitted.",
|
|
770
|
+
},
|
|
771
|
+
},
|
|
772
|
+
required: ["attachmentId"],
|
|
773
|
+
},
|
|
774
|
+
},
|
|
775
|
+
{
|
|
776
|
+
name: "sentry_list_instances",
|
|
777
|
+
description:
|
|
778
|
+
"List all configured Sentry instances and which is the default.",
|
|
779
|
+
inputSchema: {
|
|
780
|
+
type: "object",
|
|
781
|
+
properties: {},
|
|
782
|
+
required: [],
|
|
783
|
+
},
|
|
784
|
+
},
|
|
785
|
+
{
|
|
786
|
+
name: "sentry_add_instance",
|
|
787
|
+
description:
|
|
788
|
+
"Add or update a Sentry instance at runtime. Persists to config file.",
|
|
789
|
+
inputSchema: {
|
|
790
|
+
type: "object",
|
|
791
|
+
properties: {
|
|
792
|
+
name: {
|
|
793
|
+
type: "string",
|
|
794
|
+
description: "Instance name (e.g. 'work', 'personal').",
|
|
795
|
+
},
|
|
796
|
+
token: {
|
|
797
|
+
type: "string",
|
|
798
|
+
description: "Sentry API auth token (Bearer token).",
|
|
799
|
+
},
|
|
800
|
+
baseUrl: {
|
|
801
|
+
type: "string",
|
|
802
|
+
description:
|
|
803
|
+
"Sentry base URL (e.g. 'https://sentry.io' or 'https://my-org.sentry.io').",
|
|
804
|
+
},
|
|
805
|
+
orgSlug: {
|
|
806
|
+
type: "string",
|
|
807
|
+
description: "Sentry organization slug.",
|
|
808
|
+
},
|
|
809
|
+
projectSlug: {
|
|
810
|
+
type: "string",
|
|
811
|
+
description:
|
|
812
|
+
"Default Sentry project slug for this instance (optional).",
|
|
813
|
+
},
|
|
814
|
+
projects: {
|
|
815
|
+
type: "array",
|
|
816
|
+
items: { type: "string" },
|
|
817
|
+
description:
|
|
818
|
+
"Project slugs to route to this instance (for multi-instance resolution).",
|
|
819
|
+
},
|
|
820
|
+
setDefault: {
|
|
821
|
+
type: "boolean",
|
|
822
|
+
description: "Set this as the default instance.",
|
|
823
|
+
},
|
|
824
|
+
},
|
|
825
|
+
required: ["name", "token", "baseUrl", "orgSlug"],
|
|
826
|
+
},
|
|
827
|
+
},
|
|
828
|
+
{
|
|
829
|
+
name: "sentry_remove_instance",
|
|
830
|
+
description: "Remove a Sentry instance from the configuration.",
|
|
831
|
+
inputSchema: {
|
|
832
|
+
type: "object",
|
|
833
|
+
properties: {
|
|
834
|
+
name: {
|
|
835
|
+
type: "string",
|
|
836
|
+
description: "Instance name to remove.",
|
|
837
|
+
},
|
|
838
|
+
},
|
|
839
|
+
required: ["name"],
|
|
840
|
+
},
|
|
841
|
+
},
|
|
842
|
+
],
|
|
843
|
+
};
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
// ── Tool handler ────────────────────────────────────────────────────────────
|
|
847
|
+
|
|
848
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
849
|
+
const { name, arguments: args } = request.params;
|
|
850
|
+
|
|
851
|
+
try {
|
|
852
|
+
if (name === "sentry_list_projects") {
|
|
853
|
+
const inst = getInstanceByName(args.instance);
|
|
854
|
+
if (!inst) throw new Error("No Sentry instance configured. Run: sentry-mcp setup");
|
|
855
|
+
const data = await fetchSentry(
|
|
856
|
+
`/api/0/organizations/${encodeURIComponent(inst.orgSlug)}/projects/`,
|
|
857
|
+
inst,
|
|
858
|
+
);
|
|
859
|
+
if (!data || !data.length) {
|
|
860
|
+
return {
|
|
861
|
+
content: [{ type: "text", text: "No projects found." }],
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
const lines = data.map(
|
|
865
|
+
(p) =>
|
|
866
|
+
`- **${p.name}** (slug: ${p.slug}, platform: ${p.platform || "N/A"}, id: ${p.id})`,
|
|
867
|
+
);
|
|
868
|
+
return {
|
|
869
|
+
content: [
|
|
870
|
+
{
|
|
871
|
+
type: "text",
|
|
872
|
+
text: `Found ${data.length} project(s) in org "${inst.orgSlug}":\n\n${lines.join("\n")}`,
|
|
873
|
+
},
|
|
874
|
+
],
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
} else if (name === "sentry_get_issues") {
|
|
878
|
+
const inst = getInstanceByName(args.instance);
|
|
879
|
+
if (!inst) throw new Error("No Sentry instance configured. Run: sentry-mcp setup");
|
|
880
|
+
if (!inst.projectSlug) throw new Error("No projectSlug configured for this instance. Use sentry_search_issues with org-level search, or configure a projectSlug.");
|
|
881
|
+
const query = args.query || "is:unresolved";
|
|
882
|
+
const params = new URLSearchParams({ query });
|
|
883
|
+
const data = await fetchSentry(
|
|
884
|
+
`/api/0/projects/${encodeURIComponent(inst.orgSlug)}/${encodeURIComponent(inst.projectSlug)}/issues/?${params}`,
|
|
885
|
+
inst,
|
|
886
|
+
);
|
|
887
|
+
if (!data || !data.length) {
|
|
888
|
+
return {
|
|
889
|
+
content: [{ type: "text", text: `No issues found for query: ${query}` }],
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
const formatted = data.map(formatIssueSummary);
|
|
893
|
+
return {
|
|
894
|
+
content: [
|
|
895
|
+
{
|
|
896
|
+
type: "text",
|
|
897
|
+
text: `Found ${data.length} issue(s) for "${query}":\n\n${formatted.join("\n\n---\n\n")}`,
|
|
898
|
+
},
|
|
899
|
+
],
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
} else if (name === "sentry_get_issue") {
|
|
903
|
+
const inst = getInstanceByName(args.instance);
|
|
904
|
+
if (!inst) throw new Error("No Sentry instance configured. Run: sentry-mcp setup");
|
|
905
|
+
const data = await fetchSentry(
|
|
906
|
+
`/api/0/organizations/${encodeURIComponent(inst.orgSlug)}/issues/${encodeURIComponent(args.issueId)}/`,
|
|
907
|
+
inst,
|
|
908
|
+
);
|
|
909
|
+
return {
|
|
910
|
+
content: [{ type: "text", text: JSON.stringify(cleanIssueData(data), null, 2) }],
|
|
911
|
+
};
|
|
912
|
+
|
|
913
|
+
} else if (name === "sentry_get_latest_event") {
|
|
914
|
+
const inst = getInstanceByName(args.instance);
|
|
915
|
+
if (!inst) throw new Error("No Sentry instance configured. Run: sentry-mcp setup");
|
|
916
|
+
const data = await fetchSentry(
|
|
917
|
+
`/api/0/organizations/${encodeURIComponent(inst.orgSlug)}/issues/${encodeURIComponent(args.issueId)}/events/latest/`,
|
|
918
|
+
inst,
|
|
919
|
+
);
|
|
920
|
+
return {
|
|
921
|
+
content: [{ type: "text", text: JSON.stringify(cleanEventData(data), null, 2) }],
|
|
922
|
+
};
|
|
923
|
+
|
|
924
|
+
} else if (name === "sentry_resolve_issue") {
|
|
925
|
+
const inst = getInstanceByName(args.instance);
|
|
926
|
+
if (!inst) throw new Error("No Sentry instance configured. Run: sentry-mcp setup");
|
|
927
|
+
await fetchSentry(
|
|
928
|
+
`/api/0/organizations/${encodeURIComponent(inst.orgSlug)}/issues/${encodeURIComponent(args.issueId)}/`,
|
|
929
|
+
inst,
|
|
930
|
+
{ method: "PUT", body: { status: "resolved" } },
|
|
931
|
+
);
|
|
932
|
+
return {
|
|
933
|
+
content: [{ type: "text", text: `Issue ${args.issueId} marked as resolved.` }],
|
|
934
|
+
};
|
|
935
|
+
|
|
936
|
+
} else if (name === "sentry_unresolve_issue") {
|
|
937
|
+
const inst = getInstanceByName(args.instance);
|
|
938
|
+
if (!inst) throw new Error("No Sentry instance configured. Run: sentry-mcp setup");
|
|
939
|
+
await fetchSentry(
|
|
940
|
+
`/api/0/organizations/${encodeURIComponent(inst.orgSlug)}/issues/${encodeURIComponent(args.issueId)}/`,
|
|
941
|
+
inst,
|
|
942
|
+
{ method: "PUT", body: { status: "unresolved" } },
|
|
943
|
+
);
|
|
944
|
+
return {
|
|
945
|
+
content: [{ type: "text", text: `Issue ${args.issueId} reopened (unresolved).` }],
|
|
946
|
+
};
|
|
947
|
+
|
|
948
|
+
} else if (name === "sentry_ignore_issue") {
|
|
949
|
+
const inst = getInstanceByName(args.instance);
|
|
950
|
+
if (!inst) throw new Error("No Sentry instance configured. Run: sentry-mcp setup");
|
|
951
|
+
await fetchSentry(
|
|
952
|
+
`/api/0/organizations/${encodeURIComponent(inst.orgSlug)}/issues/${encodeURIComponent(args.issueId)}/`,
|
|
953
|
+
inst,
|
|
954
|
+
{ method: "PUT", body: { status: "ignored" } },
|
|
955
|
+
);
|
|
956
|
+
return {
|
|
957
|
+
content: [{ type: "text", text: `Issue ${args.issueId} ignored.` }],
|
|
958
|
+
};
|
|
959
|
+
|
|
960
|
+
} else if (name === "sentry_search_issues") {
|
|
961
|
+
const inst = getInstanceByName(args.instance);
|
|
962
|
+
if (!inst) throw new Error("No Sentry instance configured. Run: sentry-mcp setup");
|
|
963
|
+
// Use project-scoped if projectSlug is available, otherwise org-scoped
|
|
964
|
+
let apiPath;
|
|
965
|
+
if (inst.projectSlug) {
|
|
966
|
+
const params = new URLSearchParams({ query: args.query });
|
|
967
|
+
apiPath = `/api/0/projects/${encodeURIComponent(inst.orgSlug)}/${encodeURIComponent(inst.projectSlug)}/issues/?${params}`;
|
|
968
|
+
} else {
|
|
969
|
+
const params = new URLSearchParams({ query: args.query });
|
|
970
|
+
apiPath = `/api/0/organizations/${encodeURIComponent(inst.orgSlug)}/issues/?${params}`;
|
|
971
|
+
}
|
|
972
|
+
const data = await fetchSentry(apiPath, inst);
|
|
973
|
+
if (!data || !data.length) {
|
|
974
|
+
return {
|
|
975
|
+
content: [{ type: "text", text: `No issues found for query: ${args.query}` }],
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
const formatted = data.map(formatIssueSummary);
|
|
979
|
+
return {
|
|
980
|
+
content: [
|
|
981
|
+
{
|
|
982
|
+
type: "text",
|
|
983
|
+
text: `Found ${data.length} issue(s) for "${args.query}":\n\n${formatted.join("\n\n---\n\n")}`,
|
|
984
|
+
},
|
|
985
|
+
],
|
|
986
|
+
};
|
|
987
|
+
|
|
988
|
+
} else if (name === "sentry_assign_issue") {
|
|
989
|
+
const inst = getInstanceByName(args.instance);
|
|
990
|
+
if (!inst) throw new Error("No Sentry instance configured. Run: sentry-mcp setup");
|
|
991
|
+
await fetchSentry(
|
|
992
|
+
`/api/0/organizations/${encodeURIComponent(inst.orgSlug)}/issues/${encodeURIComponent(args.issueId)}/`,
|
|
993
|
+
inst,
|
|
994
|
+
{ method: "PUT", body: { assignedTo: args.assignee } },
|
|
995
|
+
);
|
|
996
|
+
return {
|
|
997
|
+
content: [
|
|
998
|
+
{ type: "text", text: `Issue ${args.issueId} assigned to ${args.assignee}.` },
|
|
999
|
+
],
|
|
1000
|
+
};
|
|
1001
|
+
|
|
1002
|
+
} else if (name === "sentry_list_releases") {
|
|
1003
|
+
const inst = getInstanceByName(args.instance);
|
|
1004
|
+
if (!inst) throw new Error("No Sentry instance configured. Run: sentry-mcp setup");
|
|
1005
|
+
if (!inst.projectSlug) throw new Error("No projectSlug configured for this instance.");
|
|
1006
|
+
const params = new URLSearchParams();
|
|
1007
|
+
if (args.query) params.set("query", args.query);
|
|
1008
|
+
const data = await fetchSentry(
|
|
1009
|
+
`/api/0/organizations/${encodeURIComponent(inst.orgSlug)}/releases/?project=${encodeURIComponent(inst.projectSlug)}&${params}`,
|
|
1010
|
+
inst,
|
|
1011
|
+
);
|
|
1012
|
+
if (!data || !data.length) {
|
|
1013
|
+
return {
|
|
1014
|
+
content: [{ type: "text", text: `No releases found.${args.query ? ` Query: ${args.query}` : ""}` }],
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
const lines = data.slice(0, 25).map((r) => {
|
|
1018
|
+
const artifacts = r.newGroups != null ? ` | New issues: ${r.newGroups}` : "";
|
|
1019
|
+
return `- **${r.version}** | Created: ${r.dateCreated || "N/A"}${artifacts}${r.lastEvent ? ` | Last event: ${r.lastEvent}` : ""}`;
|
|
1020
|
+
});
|
|
1021
|
+
return {
|
|
1022
|
+
content: [
|
|
1023
|
+
{
|
|
1024
|
+
type: "text",
|
|
1025
|
+
text: `Found ${data.length} release(s):\n\n${lines.join("\n")}`,
|
|
1026
|
+
},
|
|
1027
|
+
],
|
|
1028
|
+
};
|
|
1029
|
+
|
|
1030
|
+
} else if (name === "sentry_get_release_artifacts") {
|
|
1031
|
+
const inst = getInstanceByName(args.instance);
|
|
1032
|
+
if (!inst) throw new Error("No Sentry instance configured. Run: sentry-mcp setup");
|
|
1033
|
+
const data = await fetchSentry(
|
|
1034
|
+
`/api/0/organizations/${encodeURIComponent(inst.orgSlug)}/releases/${encodeURIComponent(args.version)}/files/`,
|
|
1035
|
+
inst,
|
|
1036
|
+
);
|
|
1037
|
+
if (!data || !data.length) {
|
|
1038
|
+
return {
|
|
1039
|
+
content: [{ type: "text", text: `No artifacts found for release "${args.version}". Source maps may not have been uploaded.` }],
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
const lines = data.map((a) => {
|
|
1043
|
+
const size = a.size ? ` (${(a.size / 1024).toFixed(1)} KB)` : "";
|
|
1044
|
+
return `- ${a.name}${size} [id: ${a.id}]`;
|
|
1045
|
+
});
|
|
1046
|
+
return {
|
|
1047
|
+
content: [
|
|
1048
|
+
{
|
|
1049
|
+
type: "text",
|
|
1050
|
+
text: `Found ${data.length} artifact(s) for release "${args.version}":\n\n${lines.join("\n")}`,
|
|
1051
|
+
},
|
|
1052
|
+
],
|
|
1053
|
+
};
|
|
1054
|
+
|
|
1055
|
+
} else if (name === "sentry_create_release") {
|
|
1056
|
+
const inst = getInstanceByName(args.instance);
|
|
1057
|
+
if (!inst) throw new Error("No Sentry instance configured. Run: sentry-mcp setup");
|
|
1058
|
+
const projects = args.projects || (inst.projectSlug ? [inst.projectSlug] : []);
|
|
1059
|
+
if (!projects.length) throw new Error("No projects specified and no projectSlug configured.");
|
|
1060
|
+
const body = { version: args.version, projects };
|
|
1061
|
+
if (args.ref) body.ref = args.ref;
|
|
1062
|
+
const data = await fetchSentry(
|
|
1063
|
+
`/api/0/organizations/${encodeURIComponent(inst.orgSlug)}/releases/`,
|
|
1064
|
+
inst,
|
|
1065
|
+
{ method: "POST", body },
|
|
1066
|
+
);
|
|
1067
|
+
return {
|
|
1068
|
+
content: [{ type: "text", text: `Release "${data.version}" created for projects: ${projects.join(", ")}` }],
|
|
1069
|
+
};
|
|
1070
|
+
|
|
1071
|
+
} else if (name === "sentry_finalize_release") {
|
|
1072
|
+
const inst = getInstanceByName(args.instance);
|
|
1073
|
+
if (!inst) throw new Error("No Sentry instance configured. Run: sentry-mcp setup");
|
|
1074
|
+
await fetchSentry(
|
|
1075
|
+
`/api/0/organizations/${encodeURIComponent(inst.orgSlug)}/releases/${encodeURIComponent(args.version)}/`,
|
|
1076
|
+
inst,
|
|
1077
|
+
{ method: "PUT", body: { dateReleased: new Date().toISOString() } },
|
|
1078
|
+
);
|
|
1079
|
+
return {
|
|
1080
|
+
content: [{ type: "text", text: `Release "${args.version}" finalized.` }],
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
} else if (name === "sentry_delete_release") {
|
|
1084
|
+
const inst = getInstanceByName(args.instance);
|
|
1085
|
+
if (!inst) throw new Error("No Sentry instance configured. Run: sentry-mcp setup");
|
|
1086
|
+
await fetchSentry(
|
|
1087
|
+
`/api/0/organizations/${encodeURIComponent(inst.orgSlug)}/releases/${encodeURIComponent(args.version)}/`,
|
|
1088
|
+
inst,
|
|
1089
|
+
{ method: "DELETE" },
|
|
1090
|
+
);
|
|
1091
|
+
return {
|
|
1092
|
+
content: [{ type: "text", text: `Release "${args.version}" deleted.` }],
|
|
1093
|
+
};
|
|
1094
|
+
|
|
1095
|
+
} else if (name === "sentry_create_deploy") {
|
|
1096
|
+
const inst = getInstanceByName(args.instance);
|
|
1097
|
+
if (!inst) throw new Error("No Sentry instance configured. Run: sentry-mcp setup");
|
|
1098
|
+
const body = { environment: args.environment };
|
|
1099
|
+
if (args.name) body.name = args.name;
|
|
1100
|
+
const data = await fetchSentry(
|
|
1101
|
+
`/api/0/organizations/${encodeURIComponent(inst.orgSlug)}/releases/${encodeURIComponent(args.version)}/deploys/`,
|
|
1102
|
+
inst,
|
|
1103
|
+
{ method: "POST", body },
|
|
1104
|
+
);
|
|
1105
|
+
return {
|
|
1106
|
+
content: [{ type: "text", text: `Deploy created for release "${args.version}" to environment "${args.environment}".` }],
|
|
1107
|
+
};
|
|
1108
|
+
|
|
1109
|
+
} else if (name === "sentry_list_deploys") {
|
|
1110
|
+
const inst = getInstanceByName(args.instance);
|
|
1111
|
+
if (!inst) throw new Error("No Sentry instance configured. Run: sentry-mcp setup");
|
|
1112
|
+
const data = await fetchSentry(
|
|
1113
|
+
`/api/0/organizations/${encodeURIComponent(inst.orgSlug)}/releases/${encodeURIComponent(args.version)}/deploys/`,
|
|
1114
|
+
inst,
|
|
1115
|
+
);
|
|
1116
|
+
if (!data || !data.length) {
|
|
1117
|
+
return {
|
|
1118
|
+
content: [{ type: "text", text: `No deploys found for release "${args.version}".` }],
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
const lines = data.map((d) => `- **${d.environment}** | ${d.dateFinished || d.dateStarted || "N/A"}${d.name ? ` | ${d.name}` : ""}`);
|
|
1122
|
+
return {
|
|
1123
|
+
content: [{ type: "text", text: `${data.length} deploy(s) for "${args.version}":\n\n${lines.join("\n")}` }],
|
|
1124
|
+
};
|
|
1125
|
+
|
|
1126
|
+
} else if (name === "sentry_delete_artifact") {
|
|
1127
|
+
const inst = getInstanceByName(args.instance);
|
|
1128
|
+
if (!inst) throw new Error("No Sentry instance configured. Run: sentry-mcp setup");
|
|
1129
|
+
await fetchSentry(
|
|
1130
|
+
`/api/0/organizations/${encodeURIComponent(inst.orgSlug)}/releases/${encodeURIComponent(args.version)}/files/${encodeURIComponent(args.artifactId)}/`,
|
|
1131
|
+
inst,
|
|
1132
|
+
{ method: "DELETE" },
|
|
1133
|
+
);
|
|
1134
|
+
return {
|
|
1135
|
+
content: [{ type: "text", text: `Artifact ${args.artifactId} deleted from release "${args.version}".` }],
|
|
1136
|
+
};
|
|
1137
|
+
|
|
1138
|
+
} else if (name === "sentry_set_commits") {
|
|
1139
|
+
const inst = getInstanceByName(args.instance);
|
|
1140
|
+
if (!inst) throw new Error("No Sentry instance configured. Run: sentry-mcp setup");
|
|
1141
|
+
const commitSpec = { repository: args.repository, currentId: args.commit };
|
|
1142
|
+
if (args.previousCommit) commitSpec.previousId = args.previousCommit;
|
|
1143
|
+
await fetchSentry(
|
|
1144
|
+
`/api/0/organizations/${encodeURIComponent(inst.orgSlug)}/releases/${encodeURIComponent(args.version)}/commits/`,
|
|
1145
|
+
inst,
|
|
1146
|
+
{ method: "PUT", body: { commits: [commitSpec] } },
|
|
1147
|
+
);
|
|
1148
|
+
return {
|
|
1149
|
+
content: [{ type: "text", text: `Commits associated with release "${args.version}" (repo: ${args.repository}, HEAD: ${args.commit}).` }],
|
|
1150
|
+
};
|
|
1151
|
+
|
|
1152
|
+
} else if (name === "sentry_get_release") {
|
|
1153
|
+
const inst = getInstanceByName(args.instance);
|
|
1154
|
+
if (!inst) throw new Error("No Sentry instance configured. Run: sentry-mcp setup");
|
|
1155
|
+
const data = await fetchSentry(
|
|
1156
|
+
`/api/0/organizations/${encodeURIComponent(inst.orgSlug)}/releases/${encodeURIComponent(args.version)}/`,
|
|
1157
|
+
inst,
|
|
1158
|
+
);
|
|
1159
|
+
const lines = [];
|
|
1160
|
+
lines.push(`**${data.version}**`);
|
|
1161
|
+
lines.push(`- Status: ${data.status || "open"}`);
|
|
1162
|
+
lines.push(`- Created: ${data.dateCreated || "N/A"}`);
|
|
1163
|
+
if (data.dateReleased) lines.push(`- Released: ${data.dateReleased}`);
|
|
1164
|
+
if (data.firstEvent) lines.push(`- First event: ${data.firstEvent}`);
|
|
1165
|
+
if (data.lastEvent) lines.push(`- Last event: ${data.lastEvent}`);
|
|
1166
|
+
lines.push(`- New issues: ${data.newGroups || 0}`);
|
|
1167
|
+
lines.push(`- Commits: ${data.commitCount || 0}`);
|
|
1168
|
+
lines.push(`- Deploys: ${data.deployCount || 0}`);
|
|
1169
|
+
if (data.projects?.length) {
|
|
1170
|
+
lines.push(`- Projects: ${data.projects.map((p) => p.slug || p.name).join(", ")}`);
|
|
1171
|
+
}
|
|
1172
|
+
if (data.authors?.length) {
|
|
1173
|
+
lines.push(`- Authors: ${data.authors.map((a) => a.name || a.email).join(", ")}`);
|
|
1174
|
+
}
|
|
1175
|
+
return {
|
|
1176
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1177
|
+
};
|
|
1178
|
+
|
|
1179
|
+
} else if (name === "sentry_list_events") {
|
|
1180
|
+
const inst = getInstanceByName(args.instance);
|
|
1181
|
+
if (!inst) throw new Error("No Sentry instance configured. Run: sentry-mcp setup");
|
|
1182
|
+
const data = await fetchSentry(
|
|
1183
|
+
`/api/0/organizations/${encodeURIComponent(inst.orgSlug)}/issues/${encodeURIComponent(args.issueId)}/events/`,
|
|
1184
|
+
inst,
|
|
1185
|
+
);
|
|
1186
|
+
if (!data || !data.length) {
|
|
1187
|
+
return {
|
|
1188
|
+
content: [{ type: "text", text: `No events found for issue ${args.issueId}.` }],
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
const lines = data.map((e) => {
|
|
1192
|
+
const user = e.user ? `${e.user.username || e.user.email || e.user.id || "unknown"}` : "N/A";
|
|
1193
|
+
const title = e.title || e.message || "";
|
|
1194
|
+
return `- **${e.eventID || e.id}** | ${e.dateCreated || "N/A"} | User: ${user} | ${title}`;
|
|
1195
|
+
});
|
|
1196
|
+
return {
|
|
1197
|
+
content: [
|
|
1198
|
+
{
|
|
1199
|
+
type: "text",
|
|
1200
|
+
text: `Found ${data.length} event(s) for issue ${args.issueId}:\n\n${lines.join("\n")}`,
|
|
1201
|
+
},
|
|
1202
|
+
],
|
|
1203
|
+
};
|
|
1204
|
+
|
|
1205
|
+
} else if (name === "sentry_list_event_attachments") {
|
|
1206
|
+
const inst = getInstanceByName(args.instance);
|
|
1207
|
+
if (!inst) throw new Error("No Sentry instance configured. Run: sentry-mcp setup");
|
|
1208
|
+
if (!inst.projectSlug) throw new Error("No projectSlug configured for this instance.");
|
|
1209
|
+
const eventId = await resolveEventId(args, inst);
|
|
1210
|
+
const data = await fetchSentry(
|
|
1211
|
+
`/api/0/projects/${encodeURIComponent(inst.orgSlug)}/${encodeURIComponent(inst.projectSlug)}/events/${encodeURIComponent(eventId)}/attachments/`,
|
|
1212
|
+
inst,
|
|
1213
|
+
);
|
|
1214
|
+
if (!data || !data.length) {
|
|
1215
|
+
return {
|
|
1216
|
+
content: [{ type: "text", text: `No attachments found for event ${eventId}.` }],
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
const lines = data.map((a) => {
|
|
1220
|
+
const size = a.size != null ? ` (${(a.size / 1024).toFixed(1)} KB)` : "";
|
|
1221
|
+
const type = a.type ? `, type: ${a.type}` : "";
|
|
1222
|
+
const mime = a.mimetype ? `, mimetype: ${a.mimetype}` : "";
|
|
1223
|
+
return `- ${a.name}${size} [id: ${a.id}${type}${mime}]`;
|
|
1224
|
+
});
|
|
1225
|
+
return {
|
|
1226
|
+
content: [
|
|
1227
|
+
{
|
|
1228
|
+
type: "text",
|
|
1229
|
+
text: `Found ${data.length} attachment(s) for event ${eventId}:\n\n${lines.join("\n")}\n\nDownload one with sentry_download_event_attachment (attachmentId + eventId/issueId).`,
|
|
1230
|
+
},
|
|
1231
|
+
],
|
|
1232
|
+
};
|
|
1233
|
+
|
|
1234
|
+
} else if (name === "sentry_download_event_attachment") {
|
|
1235
|
+
const inst = getInstanceByName(args.instance);
|
|
1236
|
+
if (!inst) throw new Error("No Sentry instance configured. Run: sentry-mcp setup");
|
|
1237
|
+
if (!inst.projectSlug) throw new Error("No projectSlug configured for this instance.");
|
|
1238
|
+
if (!args.attachmentId) throw new Error("attachmentId is required.");
|
|
1239
|
+
const eventId = await resolveEventId(args, inst);
|
|
1240
|
+
|
|
1241
|
+
// Look up metadata (name, size, mimetype) so we save with the real filename.
|
|
1242
|
+
const list = await fetchSentry(
|
|
1243
|
+
`/api/0/projects/${encodeURIComponent(inst.orgSlug)}/${encodeURIComponent(inst.projectSlug)}/events/${encodeURIComponent(eventId)}/attachments/`,
|
|
1244
|
+
inst,
|
|
1245
|
+
);
|
|
1246
|
+
const meta = (list || []).find((a) => String(a.id) === String(args.attachmentId));
|
|
1247
|
+
const safeName = safeAttachmentName(meta && meta.name, `attachment-${args.attachmentId}`);
|
|
1248
|
+
|
|
1249
|
+
const buffer = await fetchSentryRaw(
|
|
1250
|
+
`/api/0/projects/${encodeURIComponent(inst.orgSlug)}/${encodeURIComponent(inst.projectSlug)}/events/${encodeURIComponent(eventId)}/attachments/${encodeURIComponent(args.attachmentId)}/?download`,
|
|
1251
|
+
inst,
|
|
1252
|
+
);
|
|
1253
|
+
|
|
1254
|
+
// Resolve destination: explicit path (file or directory) or default temp dir.
|
|
1255
|
+
let destPath;
|
|
1256
|
+
if (args.outputPath) {
|
|
1257
|
+
const resolved = path.resolve(process.cwd(), args.outputPath);
|
|
1258
|
+
const looksLikeDir =
|
|
1259
|
+
/[\\/]$/.test(args.outputPath) ||
|
|
1260
|
+
(fs.existsSync(resolved) && fs.statSync(resolved).isDirectory());
|
|
1261
|
+
destPath = looksLikeDir ? path.join(resolved, safeName) : resolved;
|
|
1262
|
+
} else {
|
|
1263
|
+
destPath = path.join(
|
|
1264
|
+
os.tmpdir(),
|
|
1265
|
+
"sentry-mcp",
|
|
1266
|
+
"attachments",
|
|
1267
|
+
safePathSegment(eventId, "event"),
|
|
1268
|
+
`${safePathSegment(args.attachmentId, "attachment")}-${safeName}`,
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
1272
|
+
fs.writeFileSync(destPath, buffer);
|
|
1273
|
+
|
|
1274
|
+
const details = {
|
|
1275
|
+
savedPath: destPath,
|
|
1276
|
+
name: (meta && meta.name) || safeName,
|
|
1277
|
+
attachmentId: args.attachmentId,
|
|
1278
|
+
eventId,
|
|
1279
|
+
size: (meta && meta.size != null ? meta.size : buffer.length),
|
|
1280
|
+
mimetype: meta && meta.mimetype ? meta.mimetype : undefined,
|
|
1281
|
+
sha1: meta && meta.sha1 ? meta.sha1 : undefined,
|
|
1282
|
+
};
|
|
1283
|
+
return {
|
|
1284
|
+
content: [
|
|
1285
|
+
{
|
|
1286
|
+
type: "text",
|
|
1287
|
+
text: `Downloaded attachment "${details.name}" (${details.size} bytes) to:\n${destPath}\n\n${JSON.stringify(details, null, 2)}`,
|
|
1288
|
+
},
|
|
1289
|
+
],
|
|
1290
|
+
};
|
|
1291
|
+
|
|
1292
|
+
} else if (name === "sentry_list_instances") {
|
|
1293
|
+
if (!instances.length) {
|
|
1294
|
+
return {
|
|
1295
|
+
content: [{ type: "text", text: "No Sentry instances configured. Run: sentry-mcp setup" }],
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
const lines = instances.map((inst) => {
|
|
1299
|
+
const isDefault = defaultInstance && inst.name === defaultInstance.name ? " (default)" : "";
|
|
1300
|
+
const projs = inst.projects?.length ? ` [${inst.projects.join(", ")}]` : "";
|
|
1301
|
+
return `- **${inst.name}**${isDefault}: ${inst.baseUrl} (org: ${inst.orgSlug}${inst.projectSlug ? ", project: " + inst.projectSlug : ""})${projs}`;
|
|
1302
|
+
});
|
|
1303
|
+
return {
|
|
1304
|
+
content: [
|
|
1305
|
+
{
|
|
1306
|
+
type: "text",
|
|
1307
|
+
text: `${instances.length} instance(s) configured:\n\n${lines.join("\n")}`,
|
|
1308
|
+
},
|
|
1309
|
+
],
|
|
1310
|
+
};
|
|
1311
|
+
|
|
1312
|
+
} else if (name === "sentry_add_instance") {
|
|
1313
|
+
const config = loadConfigFile();
|
|
1314
|
+
if (!config.instances) config.instances = [];
|
|
1315
|
+
|
|
1316
|
+
const instance = {
|
|
1317
|
+
name: args.name,
|
|
1318
|
+
token: args.token,
|
|
1319
|
+
baseUrl: (args.baseUrl || "https://sentry.io").replace(/\/+$/, ""),
|
|
1320
|
+
orgSlug: args.orgSlug,
|
|
1321
|
+
projectSlug: args.projectSlug || "",
|
|
1322
|
+
projects: (args.projects || []).map((p) => p.toLowerCase()),
|
|
1323
|
+
};
|
|
1324
|
+
|
|
1325
|
+
const idx = config.instances.findIndex((i) => i.name === args.name);
|
|
1326
|
+
if (idx >= 0) {
|
|
1327
|
+
config.instances[idx] = instance;
|
|
1328
|
+
} else {
|
|
1329
|
+
config.instances.push(instance);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
if (args.setDefault || !config.defaultInstance) {
|
|
1333
|
+
config.defaultInstance = args.name;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
if (!fs.existsSync(configDir)) {
|
|
1337
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
1338
|
+
}
|
|
1339
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
1340
|
+
|
|
1341
|
+
// Update runtime state
|
|
1342
|
+
const normalized = { ...instance, projects: instance.projects };
|
|
1343
|
+
const runtimeIdx = instances.findIndex((i) => i.name === args.name);
|
|
1344
|
+
if (runtimeIdx >= 0) {
|
|
1345
|
+
instances[runtimeIdx] = normalized;
|
|
1346
|
+
} else {
|
|
1347
|
+
instances.push(normalized);
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
return {
|
|
1351
|
+
content: [
|
|
1352
|
+
{
|
|
1353
|
+
type: "text",
|
|
1354
|
+
text: `Instance "${args.name}" saved.${args.setDefault ? " Set as default." : ""}`,
|
|
1355
|
+
},
|
|
1356
|
+
],
|
|
1357
|
+
};
|
|
1358
|
+
|
|
1359
|
+
} else if (name === "sentry_remove_instance") {
|
|
1360
|
+
const config = loadConfigFile();
|
|
1361
|
+
if (!config.instances) {
|
|
1362
|
+
return {
|
|
1363
|
+
content: [{ type: "text", text: "No instances configured." }],
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
config.instances = config.instances.filter((i) => i.name !== args.name);
|
|
1368
|
+
if (config.defaultInstance === args.name) {
|
|
1369
|
+
config.defaultInstance = config.instances[0]?.name || null;
|
|
1370
|
+
}
|
|
1371
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
1372
|
+
|
|
1373
|
+
// Update runtime state
|
|
1374
|
+
const runtimeIdx = instances.findIndex((i) => i.name === args.name);
|
|
1375
|
+
if (runtimeIdx >= 0) instances.splice(runtimeIdx, 1);
|
|
1376
|
+
|
|
1377
|
+
return {
|
|
1378
|
+
content: [
|
|
1379
|
+
{ type: "text", text: `Instance "${args.name}" removed.` },
|
|
1380
|
+
],
|
|
1381
|
+
};
|
|
1382
|
+
|
|
1383
|
+
} else {
|
|
1384
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
1385
|
+
}
|
|
1386
|
+
} catch (error) {
|
|
1387
|
+
return {
|
|
1388
|
+
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
1389
|
+
isError: true,
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1394
|
+
async function main() {
|
|
1395
|
+
const transport = new StdioServerTransport();
|
|
1396
|
+
await server.connect(transport);
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
main().catch(console.error);
|