@rui.branco/sentry-mcp 1.0.1 → 1.0.3

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.
Files changed (2) hide show
  1. package/index.js +428 -98
  2. package/package.json +6 -2
package/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // Handle setup command
4
4
  if (process.argv[2] === "setup") {
5
5
  require("./setup.js");
6
- return;
6
+ process.exit(0);
7
7
  }
8
8
 
9
9
  const { Server } = require("@modelcontextprotocol/sdk/server/index.js");
@@ -156,7 +156,26 @@ async function fetchSentry(apiPath, instance, options = {}) {
156
156
 
157
157
  // ── Helpers ─────────────────────────────────────────────────────────────────
158
158
 
159
- function formatIssue(issue) {
159
+ // Strip noisy/internal fields that waste context but keep everything useful
160
+ function cleanIssueData(issue) {
161
+ const skip = new Set(["pluginActions", "pluginContexts", "pluginIssues", "activity", "seenBy", "stats"]);
162
+ const cleaned = {};
163
+ for (const [k, v] of Object.entries(issue)) {
164
+ if (!skip.has(k) && v != null && v !== "") cleaned[k] = v;
165
+ }
166
+ return cleaned;
167
+ }
168
+
169
+ function cleanEventData(event) {
170
+ const skip = new Set(["_meta", "groupID", "eventID", "sdk", "dist", "fingerprints"]);
171
+ const cleaned = {};
172
+ for (const [k, v] of Object.entries(event)) {
173
+ if (!skip.has(k) && v != null && v !== "") cleaned[k] = v;
174
+ }
175
+ return cleaned;
176
+ }
177
+
178
+ function formatIssueSummary(issue) {
160
179
  const meta = issue.metadata || {};
161
180
  const lines = [];
162
181
  lines.push(`**${issue.title}**`);
@@ -175,98 +194,6 @@ function formatIssue(issue) {
175
194
  return lines.join("\n");
176
195
  }
177
196
 
178
- function formatEvent(event) {
179
- const lines = [];
180
-
181
- // Stack traces
182
- const exceptions =
183
- event.entries?.find((e) => e.type === "exception")?.data?.values || [];
184
- if (exceptions.length) {
185
- lines.push("## Stack Trace\n");
186
- for (const ex of exceptions) {
187
- if (ex.type || ex.value) lines.push(`**${ex.type || "Exception"}:** ${ex.value || ""}\n`);
188
- const frames = ex.stacktrace?.frames;
189
- if (frames?.length) {
190
- lines.push("```");
191
- for (const f of frames) {
192
- const loc = f.lineNo ? `:${f.lineNo}${f.colNo ? ":" + f.colNo : ""}` : "";
193
- const marker = f.inApp ? "-> " : " ";
194
- lines.push(`${marker}${f.function || "?"} (${f.filename || "?"}${loc})`);
195
- }
196
- lines.push("```\n");
197
- }
198
- }
199
- }
200
-
201
- // Breadcrumbs
202
- const breadcrumbs =
203
- event.entries?.find((e) => e.type === "breadcrumbs")?.data?.values || [];
204
- if (breadcrumbs.length) {
205
- const last = breadcrumbs.slice(-20);
206
- lines.push(`## Breadcrumbs (last ${last.length})\n`);
207
- for (const bc of last) {
208
- const msg = bc.message || bc.data?.url || bc.data?.to || "";
209
- lines.push(`- **${bc.category || "?"}** [${bc.level || "info"}] ${msg}`);
210
- }
211
- lines.push("");
212
- }
213
-
214
- // HTTP Request
215
- const httpReq =
216
- event.entries?.find((e) => e.type === "request")?.data;
217
- if (httpReq) {
218
- lines.push("## HTTP Request\n");
219
- lines.push(`\`${httpReq.method || "GET"} ${httpReq.url || ""}\``);
220
- if (httpReq.headers) {
221
- lines.push("\n```");
222
- for (const [k, v] of httpReq.headers) {
223
- lines.push(`${k}: ${v}`);
224
- }
225
- lines.push("```");
226
- }
227
- lines.push("");
228
- }
229
-
230
- // Tags
231
- if (event.tags?.length) {
232
- lines.push("## Tags\n");
233
- lines.push("| Key | Value |");
234
- lines.push("|-----|-------|");
235
- for (const t of event.tags) {
236
- lines.push(`| ${t.key} | ${t.value} |`);
237
- }
238
- lines.push("");
239
- }
240
-
241
- // Contexts
242
- if (event.contexts) {
243
- lines.push("## Contexts\n");
244
- for (const [name, ctx] of Object.entries(event.contexts)) {
245
- if (name === "trace" || name === "replay" || !ctx || typeof ctx !== "object") continue;
246
- const entries = Object.entries(ctx).filter(([k, v]) => k !== "type" && v != null && v !== "");
247
- if (!entries.length) continue;
248
- const title = name.charAt(0).toUpperCase() + name.slice(1).replace(/_/g, " ");
249
- lines.push(`**${title}:**`);
250
- for (const [k, v] of entries.slice(0, 15)) {
251
- const display = typeof v === "object" ? JSON.stringify(v) : String(v);
252
- lines.push(`- ${k}: ${display}`);
253
- }
254
- lines.push("");
255
- }
256
- }
257
-
258
- // User
259
- if (event.user) {
260
- const u = event.user;
261
- lines.push("## User\n");
262
- lines.push(`${u.username || u.name || u.email || u.id || "unknown"}${u.email ? " (" + u.email + ")" : ""}`);
263
- if (u.ip_address) lines.push(`IP: ${u.ip_address}`);
264
- lines.push("");
265
- }
266
-
267
- return lines.join("\n") || "No event data available.";
268
- }
269
-
270
197
  // ── MCP Server ──────────────────────────────────────────────────────────────
271
198
 
272
199
  const server = new Server(
@@ -456,6 +383,232 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
456
383
  required: ["issueId", "assignee"],
457
384
  },
458
385
  },
386
+ {
387
+ name: "sentry_list_releases",
388
+ description:
389
+ "List releases for a Sentry project. Returns release versions, dates, and artifact counts.",
390
+ inputSchema: {
391
+ type: "object",
392
+ properties: {
393
+ query: {
394
+ type: "string",
395
+ description:
396
+ "Optional search query to filter releases by version (e.g. '26.05').",
397
+ },
398
+ instance: {
399
+ type: "string",
400
+ description:
401
+ "Instance name (for multi-instance setups). Uses default instance if omitted.",
402
+ },
403
+ },
404
+ required: [],
405
+ },
406
+ },
407
+ {
408
+ name: "sentry_get_release_artifacts",
409
+ description:
410
+ "List artifacts (source maps, JS bundles) uploaded for a specific release. Use to verify source maps were uploaded correctly.",
411
+ inputSchema: {
412
+ type: "object",
413
+ properties: {
414
+ version: {
415
+ type: "string",
416
+ description: "Release version string (e.g. '26.05.4b185-TRAIN').",
417
+ },
418
+ instance: {
419
+ type: "string",
420
+ description:
421
+ "Instance name (for multi-instance setups). Uses default instance if omitted.",
422
+ },
423
+ },
424
+ required: ["version"],
425
+ },
426
+ },
427
+ {
428
+ name: "sentry_create_release",
429
+ description:
430
+ "Create a new release in Sentry. Optionally associate commits and set a ref.",
431
+ inputSchema: {
432
+ type: "object",
433
+ properties: {
434
+ version: {
435
+ type: "string",
436
+ description: "Release version string (e.g. '1.0.0', '26.05.4b185-TRAIN').",
437
+ },
438
+ ref: {
439
+ type: "string",
440
+ description: "Optional git ref (commit SHA or tag).",
441
+ },
442
+ projects: {
443
+ type: "array",
444
+ items: { type: "string" },
445
+ description: "Project slugs to associate with this release. Defaults to the instance's projectSlug.",
446
+ },
447
+ instance: {
448
+ type: "string",
449
+ description: "Instance name. Uses default if omitted.",
450
+ },
451
+ },
452
+ required: ["version"],
453
+ },
454
+ },
455
+ {
456
+ name: "sentry_finalize_release",
457
+ description:
458
+ "Finalize a release (mark it as deployed/complete). Sets dateReleased to now.",
459
+ inputSchema: {
460
+ type: "object",
461
+ properties: {
462
+ version: {
463
+ type: "string",
464
+ description: "Release version string.",
465
+ },
466
+ instance: {
467
+ type: "string",
468
+ description: "Instance name. Uses default if omitted.",
469
+ },
470
+ },
471
+ required: ["version"],
472
+ },
473
+ },
474
+ {
475
+ name: "sentry_delete_release",
476
+ description:
477
+ "Delete a release and all its artifacts from Sentry.",
478
+ inputSchema: {
479
+ type: "object",
480
+ properties: {
481
+ version: {
482
+ type: "string",
483
+ description: "Release version string to delete.",
484
+ },
485
+ instance: {
486
+ type: "string",
487
+ description: "Instance name. Uses default if omitted.",
488
+ },
489
+ },
490
+ required: ["version"],
491
+ },
492
+ },
493
+ {
494
+ name: "sentry_create_deploy",
495
+ description:
496
+ "Create a deploy for a release. Records when and where a release was deployed.",
497
+ inputSchema: {
498
+ type: "object",
499
+ properties: {
500
+ version: {
501
+ type: "string",
502
+ description: "Release version string.",
503
+ },
504
+ environment: {
505
+ type: "string",
506
+ description: "Environment name (e.g. 'production', 'staging', 'QA', 'TRAIN').",
507
+ },
508
+ name: {
509
+ type: "string",
510
+ description: "Optional deploy name.",
511
+ },
512
+ instance: {
513
+ type: "string",
514
+ description: "Instance name. Uses default if omitted.",
515
+ },
516
+ },
517
+ required: ["version", "environment"],
518
+ },
519
+ },
520
+ {
521
+ name: "sentry_list_deploys",
522
+ description:
523
+ "List deploys for a release.",
524
+ inputSchema: {
525
+ type: "object",
526
+ properties: {
527
+ version: {
528
+ type: "string",
529
+ description: "Release version string.",
530
+ },
531
+ instance: {
532
+ type: "string",
533
+ description: "Instance name. Uses default if omitted.",
534
+ },
535
+ },
536
+ required: ["version"],
537
+ },
538
+ },
539
+ {
540
+ name: "sentry_delete_artifact",
541
+ description:
542
+ "Delete a specific artifact from a release by its ID.",
543
+ inputSchema: {
544
+ type: "object",
545
+ properties: {
546
+ version: {
547
+ type: "string",
548
+ description: "Release version string.",
549
+ },
550
+ artifactId: {
551
+ type: "string",
552
+ description: "Artifact ID to delete (from sentry_get_release_artifacts).",
553
+ },
554
+ instance: {
555
+ type: "string",
556
+ description: "Instance name. Uses default if omitted.",
557
+ },
558
+ },
559
+ required: ["version", "artifactId"],
560
+ },
561
+ },
562
+ {
563
+ name: "sentry_set_commits",
564
+ description:
565
+ "Associate commits with a release. Provide a repository and commit range.",
566
+ inputSchema: {
567
+ type: "object",
568
+ properties: {
569
+ version: {
570
+ type: "string",
571
+ description: "Release version string.",
572
+ },
573
+ repository: {
574
+ type: "string",
575
+ description: "Repository name (e.g. 'org/repo').",
576
+ },
577
+ commit: {
578
+ type: "string",
579
+ description: "Current commit SHA (HEAD of the release).",
580
+ },
581
+ previousCommit: {
582
+ type: "string",
583
+ description: "Previous commit SHA (start of range). If omitted, uses the previous release's commit.",
584
+ },
585
+ instance: {
586
+ type: "string",
587
+ description: "Instance name. Uses default if omitted.",
588
+ },
589
+ },
590
+ required: ["version", "repository", "commit"],
591
+ },
592
+ },
593
+ {
594
+ name: "sentry_get_release",
595
+ description:
596
+ "Get full details for a specific release including projects, commits, authors, and deploy info.",
597
+ inputSchema: {
598
+ type: "object",
599
+ properties: {
600
+ version: {
601
+ type: "string",
602
+ description: "Release version string.",
603
+ },
604
+ instance: {
605
+ type: "string",
606
+ description: "Instance name. Uses default if omitted.",
607
+ },
608
+ },
609
+ required: ["version"],
610
+ },
611
+ },
459
612
  {
460
613
  name: "sentry_list_instances",
461
614
  description:
@@ -573,7 +726,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
573
726
  content: [{ type: "text", text: `No issues found for query: ${query}` }],
574
727
  };
575
728
  }
576
- const formatted = data.map(formatIssue);
729
+ const formatted = data.map(formatIssueSummary);
577
730
  return {
578
731
  content: [
579
732
  {
@@ -591,7 +744,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
591
744
  inst,
592
745
  );
593
746
  return {
594
- content: [{ type: "text", text: formatIssue(data) }],
747
+ content: [{ type: "text", text: JSON.stringify(cleanIssueData(data), null, 2) }],
595
748
  };
596
749
 
597
750
  } else if (name === "sentry_get_latest_event") {
@@ -602,7 +755,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
602
755
  inst,
603
756
  );
604
757
  return {
605
- content: [{ type: "text", text: formatEvent(data) }],
758
+ content: [{ type: "text", text: JSON.stringify(cleanEventData(data), null, 2) }],
606
759
  };
607
760
 
608
761
  } else if (name === "sentry_resolve_issue") {
@@ -659,7 +812,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
659
812
  content: [{ type: "text", text: `No issues found for query: ${args.query}` }],
660
813
  };
661
814
  }
662
- const formatted = data.map(formatIssue);
815
+ const formatted = data.map(formatIssueSummary);
663
816
  return {
664
817
  content: [
665
818
  {
@@ -683,6 +836,183 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
683
836
  ],
684
837
  };
685
838
 
839
+ } else if (name === "sentry_list_releases") {
840
+ const inst = getInstanceByName(args.instance);
841
+ if (!inst) throw new Error("No Sentry instance configured. Run: sentry-mcp setup");
842
+ if (!inst.projectSlug) throw new Error("No projectSlug configured for this instance.");
843
+ const params = new URLSearchParams();
844
+ if (args.query) params.set("query", args.query);
845
+ const data = await fetchSentry(
846
+ `/api/0/organizations/${encodeURIComponent(inst.orgSlug)}/releases/?project=${encodeURIComponent(inst.projectSlug)}&${params}`,
847
+ inst,
848
+ );
849
+ if (!data || !data.length) {
850
+ return {
851
+ content: [{ type: "text", text: `No releases found.${args.query ? ` Query: ${args.query}` : ""}` }],
852
+ };
853
+ }
854
+ const lines = data.slice(0, 25).map((r) => {
855
+ const artifacts = r.newGroups != null ? ` | New issues: ${r.newGroups}` : "";
856
+ return `- **${r.version}** | Created: ${r.dateCreated || "N/A"}${artifacts}${r.lastEvent ? ` | Last event: ${r.lastEvent}` : ""}`;
857
+ });
858
+ return {
859
+ content: [
860
+ {
861
+ type: "text",
862
+ text: `Found ${data.length} release(s):\n\n${lines.join("\n")}`,
863
+ },
864
+ ],
865
+ };
866
+
867
+ } else if (name === "sentry_get_release_artifacts") {
868
+ const inst = getInstanceByName(args.instance);
869
+ if (!inst) throw new Error("No Sentry instance configured. Run: sentry-mcp setup");
870
+ const data = await fetchSentry(
871
+ `/api/0/organizations/${encodeURIComponent(inst.orgSlug)}/releases/${encodeURIComponent(args.version)}/files/`,
872
+ inst,
873
+ );
874
+ if (!data || !data.length) {
875
+ return {
876
+ content: [{ type: "text", text: `No artifacts found for release "${args.version}". Source maps may not have been uploaded.` }],
877
+ };
878
+ }
879
+ const lines = data.map((a) => {
880
+ const size = a.size ? ` (${(a.size / 1024).toFixed(1)} KB)` : "";
881
+ return `- ${a.name}${size} [id: ${a.id}]`;
882
+ });
883
+ return {
884
+ content: [
885
+ {
886
+ type: "text",
887
+ text: `Found ${data.length} artifact(s) for release "${args.version}":\n\n${lines.join("\n")}`,
888
+ },
889
+ ],
890
+ };
891
+
892
+ } else if (name === "sentry_create_release") {
893
+ const inst = getInstanceByName(args.instance);
894
+ if (!inst) throw new Error("No Sentry instance configured. Run: sentry-mcp setup");
895
+ const projects = args.projects || (inst.projectSlug ? [inst.projectSlug] : []);
896
+ if (!projects.length) throw new Error("No projects specified and no projectSlug configured.");
897
+ const body = { version: args.version, projects };
898
+ if (args.ref) body.ref = args.ref;
899
+ const data = await fetchSentry(
900
+ `/api/0/organizations/${encodeURIComponent(inst.orgSlug)}/releases/`,
901
+ inst,
902
+ { method: "POST", body },
903
+ );
904
+ return {
905
+ content: [{ type: "text", text: `Release "${data.version}" created for projects: ${projects.join(", ")}` }],
906
+ };
907
+
908
+ } else if (name === "sentry_finalize_release") {
909
+ const inst = getInstanceByName(args.instance);
910
+ if (!inst) throw new Error("No Sentry instance configured. Run: sentry-mcp setup");
911
+ await fetchSentry(
912
+ `/api/0/organizations/${encodeURIComponent(inst.orgSlug)}/releases/${encodeURIComponent(args.version)}/`,
913
+ inst,
914
+ { method: "PUT", body: { dateReleased: new Date().toISOString() } },
915
+ );
916
+ return {
917
+ content: [{ type: "text", text: `Release "${args.version}" finalized.` }],
918
+ };
919
+
920
+ } else if (name === "sentry_delete_release") {
921
+ const inst = getInstanceByName(args.instance);
922
+ if (!inst) throw new Error("No Sentry instance configured. Run: sentry-mcp setup");
923
+ await fetchSentry(
924
+ `/api/0/organizations/${encodeURIComponent(inst.orgSlug)}/releases/${encodeURIComponent(args.version)}/`,
925
+ inst,
926
+ { method: "DELETE" },
927
+ );
928
+ return {
929
+ content: [{ type: "text", text: `Release "${args.version}" deleted.` }],
930
+ };
931
+
932
+ } else if (name === "sentry_create_deploy") {
933
+ const inst = getInstanceByName(args.instance);
934
+ if (!inst) throw new Error("No Sentry instance configured. Run: sentry-mcp setup");
935
+ const body = { environment: args.environment };
936
+ if (args.name) body.name = args.name;
937
+ const data = await fetchSentry(
938
+ `/api/0/organizations/${encodeURIComponent(inst.orgSlug)}/releases/${encodeURIComponent(args.version)}/deploys/`,
939
+ inst,
940
+ { method: "POST", body },
941
+ );
942
+ return {
943
+ content: [{ type: "text", text: `Deploy created for release "${args.version}" to environment "${args.environment}".` }],
944
+ };
945
+
946
+ } else if (name === "sentry_list_deploys") {
947
+ const inst = getInstanceByName(args.instance);
948
+ if (!inst) throw new Error("No Sentry instance configured. Run: sentry-mcp setup");
949
+ const data = await fetchSentry(
950
+ `/api/0/organizations/${encodeURIComponent(inst.orgSlug)}/releases/${encodeURIComponent(args.version)}/deploys/`,
951
+ inst,
952
+ );
953
+ if (!data || !data.length) {
954
+ return {
955
+ content: [{ type: "text", text: `No deploys found for release "${args.version}".` }],
956
+ };
957
+ }
958
+ const lines = data.map((d) => `- **${d.environment}** | ${d.dateFinished || d.dateStarted || "N/A"}${d.name ? ` | ${d.name}` : ""}`);
959
+ return {
960
+ content: [{ type: "text", text: `${data.length} deploy(s) for "${args.version}":\n\n${lines.join("\n")}` }],
961
+ };
962
+
963
+ } else if (name === "sentry_delete_artifact") {
964
+ const inst = getInstanceByName(args.instance);
965
+ if (!inst) throw new Error("No Sentry instance configured. Run: sentry-mcp setup");
966
+ await fetchSentry(
967
+ `/api/0/organizations/${encodeURIComponent(inst.orgSlug)}/releases/${encodeURIComponent(args.version)}/files/${encodeURIComponent(args.artifactId)}/`,
968
+ inst,
969
+ { method: "DELETE" },
970
+ );
971
+ return {
972
+ content: [{ type: "text", text: `Artifact ${args.artifactId} deleted from release "${args.version}".` }],
973
+ };
974
+
975
+ } else if (name === "sentry_set_commits") {
976
+ const inst = getInstanceByName(args.instance);
977
+ if (!inst) throw new Error("No Sentry instance configured. Run: sentry-mcp setup");
978
+ const commitSpec = { repository: args.repository, currentId: args.commit };
979
+ if (args.previousCommit) commitSpec.previousId = args.previousCommit;
980
+ await fetchSentry(
981
+ `/api/0/organizations/${encodeURIComponent(inst.orgSlug)}/releases/${encodeURIComponent(args.version)}/commits/`,
982
+ inst,
983
+ { method: "PUT", body: { commits: [commitSpec] } },
984
+ );
985
+ return {
986
+ content: [{ type: "text", text: `Commits associated with release "${args.version}" (repo: ${args.repository}, HEAD: ${args.commit}).` }],
987
+ };
988
+
989
+ } else if (name === "sentry_get_release") {
990
+ const inst = getInstanceByName(args.instance);
991
+ if (!inst) throw new Error("No Sentry instance configured. Run: sentry-mcp setup");
992
+ const data = await fetchSentry(
993
+ `/api/0/organizations/${encodeURIComponent(inst.orgSlug)}/releases/${encodeURIComponent(args.version)}/`,
994
+ inst,
995
+ );
996
+ const lines = [];
997
+ lines.push(`**${data.version}**`);
998
+ lines.push(`- Status: ${data.status || "open"}`);
999
+ lines.push(`- Created: ${data.dateCreated || "N/A"}`);
1000
+ if (data.dateReleased) lines.push(`- Released: ${data.dateReleased}`);
1001
+ if (data.firstEvent) lines.push(`- First event: ${data.firstEvent}`);
1002
+ if (data.lastEvent) lines.push(`- Last event: ${data.lastEvent}`);
1003
+ lines.push(`- New issues: ${data.newGroups || 0}`);
1004
+ lines.push(`- Commits: ${data.commitCount || 0}`);
1005
+ lines.push(`- Deploys: ${data.deployCount || 0}`);
1006
+ if (data.projects?.length) {
1007
+ lines.push(`- Projects: ${data.projects.map((p) => p.slug || p.name).join(", ")}`);
1008
+ }
1009
+ if (data.authors?.length) {
1010
+ lines.push(`- Authors: ${data.authors.map((a) => a.name || a.email).join(", ")}`);
1011
+ }
1012
+ return {
1013
+ content: [{ type: "text", text: lines.join("\n") }],
1014
+ };
1015
+
686
1016
  } else if (name === "sentry_list_instances") {
687
1017
  if (!instances.length) {
688
1018
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rui.branco/sentry-mcp",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Sentry MCP server for Claude Code - fetch issues, search errors, resolve/ignore issues, get stack traces and breadcrumbs",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -12,7 +12,8 @@
12
12
  "README.md"
13
13
  ],
14
14
  "scripts": {
15
- "setup": "node setup.js"
15
+ "setup": "node setup.js",
16
+ "test": "vitest run"
16
17
  },
17
18
  "keywords": [
18
19
  "mcp",
@@ -27,5 +28,8 @@
27
28
  "dependencies": {
28
29
  "@modelcontextprotocol/sdk": "^1.25.3",
29
30
  "node-fetch": "^2.7.0"
31
+ },
32
+ "devDependencies": {
33
+ "vitest": "^4.1.6"
30
34
  }
31
35
  }