@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.
Files changed (6) hide show
  1. package/README.md +246 -246
  2. package/assets/icon.svg +1 -0
  3. package/icons.js +22 -0
  4. package/index.js +1399 -1389
  5. package/package.json +37 -35
  6. package/setup.js +267 -266
package/package.json CHANGED
@@ -1,35 +1,37 @@
1
- {
2
- "name": "@rui.branco/sentry-mcp",
3
- "version": "1.0.6",
4
- "description": "Sentry MCP server for any MCP-compatible AI client (Claude Code, Codex, Antigravity, Cursor, etc.) - fetch issues, search errors, resolve/ignore issues, get stack traces and breadcrumbs",
5
- "main": "index.js",
6
- "bin": {
7
- "sentry-mcp": "./index.js"
8
- },
9
- "files": [
10
- "index.js",
11
- "setup.js",
12
- "README.md"
13
- ],
14
- "scripts": {
15
- "setup": "node setup.js",
16
- "test": "vitest run"
17
- },
18
- "keywords": [
19
- "mcp",
20
- "model-context-protocol",
21
- "sentry",
22
- "claude",
23
- "claude-code",
24
- "error-tracking"
25
- ],
26
- "author": "Rui Branco",
27
- "license": "MIT",
28
- "dependencies": {
29
- "@modelcontextprotocol/sdk": "^1.25.3",
30
- "node-fetch": "^2.7.0"
31
- },
32
- "devDependencies": {
33
- "vitest": "^4.1.6"
34
- }
35
- }
1
+ {
2
+ "name": "@rui.branco/sentry-mcp",
3
+ "version": "1.0.8",
4
+ "description": "Sentry MCP server for any MCP-compatible AI client (Claude Code, Codex, Antigravity, Cursor, etc.) - fetch issues, search errors, resolve/ignore issues, get stack traces and breadcrumbs",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "sentry-mcp": "./index.js"
8
+ },
9
+ "files": [
10
+ "index.js",
11
+ "setup.js",
12
+ "icons.js",
13
+ "assets/icon.svg",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "setup": "node setup.js",
18
+ "test": "vitest run"
19
+ },
20
+ "keywords": [
21
+ "mcp",
22
+ "model-context-protocol",
23
+ "sentry",
24
+ "claude",
25
+ "claude-code",
26
+ "error-tracking"
27
+ ],
28
+ "author": "Rui Branco",
29
+ "license": "MIT",
30
+ "dependencies": {
31
+ "@modelcontextprotocol/sdk": "^1.25.3",
32
+ "node-fetch": "^2.7.0"
33
+ },
34
+ "devDependencies": {
35
+ "vitest": "^4.1.6"
36
+ }
37
+ }
package/setup.js CHANGED
@@ -1,266 +1,267 @@
1
- #!/usr/bin/env node
2
-
3
- const readline = require("readline");
4
- const fs = require("fs");
5
- const path = require("path");
6
-
7
- const configDir = path.join(process.env.HOME, ".config/sentry-mcp");
8
- const configPath = path.join(configDir, "config.json");
9
-
10
- // Check for command line arguments
11
- let args = process.argv.slice(2);
12
- // Skip "setup" arg if called via index.js
13
- if (args[0] === "setup") args = args.slice(1);
14
-
15
- // Load existing config if present
16
- function loadConfig() {
17
- try {
18
- if (fs.existsSync(configPath)) {
19
- return JSON.parse(fs.readFileSync(configPath, "utf8"));
20
- }
21
- } catch {}
22
- return null;
23
- }
24
-
25
- function saveConfig(config) {
26
- if (!fs.existsSync(configDir)) {
27
- fs.mkdirSync(configDir, { recursive: true });
28
- }
29
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
30
- }
31
-
32
- // Non-interactive: setup add <name> <token> <baseUrl> <orgSlug> [projectSlug] [projects...]
33
- if (args[0] === "add" && args.length >= 4) {
34
- const [, name, token, baseUrl, orgSlug, projectSlug, ...projectArgs] = args;
35
- const projects = projectArgs.length > 0
36
- ? projectArgs.join(",").split(",").map((p) => p.trim().toLowerCase()).filter(Boolean)
37
- : [];
38
-
39
- const config = loadConfig() || {};
40
-
41
- // Migrate old format if needed
42
- if (config.token && !config.instances) {
43
- config.instances = [{
44
- name: "default",
45
- token: config.token,
46
- baseUrl: config.baseUrl,
47
- orgSlug: config.orgSlug,
48
- projectSlug: config.projectSlug || "",
49
- projects: [],
50
- }];
51
- config.defaultInstance = "default";
52
- delete config.token;
53
- delete config.baseUrl;
54
- delete config.orgSlug;
55
- delete config.projectSlug;
56
- }
57
-
58
- if (!config.instances) config.instances = [];
59
-
60
- // Replace or add instance
61
- const existing = config.instances.findIndex((i) => i.name === name);
62
- const instance = {
63
- name,
64
- token,
65
- baseUrl: baseUrl.replace(/\/+$/, ""),
66
- orgSlug,
67
- projectSlug: projectSlug || "",
68
- projects,
69
- };
70
- if (existing >= 0) {
71
- config.instances[existing] = instance;
72
- } else {
73
- config.instances.push(instance);
74
- }
75
-
76
- if (!config.defaultInstance) config.defaultInstance = name;
77
-
78
- saveConfig(config);
79
- console.log(`Instance "${name}" saved to ${configPath}`);
80
- if (projects.length > 0) {
81
- console.log(`Projects: ${projects.join(", ")}`);
82
- }
83
- process.exit(0);
84
- }
85
-
86
- // Non-interactive: setup remove <name>
87
- if (args[0] === "remove" && args.length >= 2) {
88
- const name = args[1];
89
- const config = loadConfig();
90
-
91
- if (!config || !config.instances) {
92
- console.error("No multi-instance config found.");
93
- process.exit(1);
94
- }
95
-
96
- config.instances = config.instances.filter((i) => i.name !== name);
97
- if (config.defaultInstance === name) {
98
- config.defaultInstance = config.instances[0]?.name || null;
99
- }
100
-
101
- saveConfig(config);
102
- console.log(`Instance "${name}" removed.`);
103
- process.exit(0);
104
- }
105
-
106
- // Non-interactive: setup <token> <baseUrl> <orgSlug> [projectSlug]
107
- if (args.length >= 3 && args[0] !== "add" && args[0] !== "remove") {
108
- const [token, baseUrl, orgSlug, projectSlug] = args;
109
- saveConfig({
110
- token,
111
- baseUrl: baseUrl.replace(/\/+$/, ""),
112
- orgSlug,
113
- projectSlug: projectSlug || "",
114
- });
115
- console.log(`Config saved to ${configPath}`);
116
- process.exit(0);
117
- }
118
-
119
- // Interactive mode
120
- const rl = readline.createInterface({
121
- input: process.stdin,
122
- output: process.stdout,
123
- });
124
-
125
- function ask(question) {
126
- return new Promise((resolve) => {
127
- rl.question(question, resolve);
128
- });
129
- }
130
-
131
- async function setup() {
132
- const existing = loadConfig();
133
- const hasInstances = existing?.instances?.length > 0;
134
- const isOldFormat = existing?.token && !existing?.instances;
135
-
136
- console.log("\n=== Sentry MCP Setup ===\n");
137
-
138
- if (hasInstances || isOldFormat) {
139
- if (isOldFormat) {
140
- console.log(`Existing single-instance config found (${existing.baseUrl})\n`);
141
- } else {
142
- console.log("Existing instances:");
143
- for (const inst of existing.instances) {
144
- const isDefault = inst.name === existing.defaultInstance ? " (default)" : "";
145
- const projs = inst.projects?.length > 0 ? ` [${inst.projects.join(", ")}]` : "";
146
- console.log(` - ${inst.name}${isDefault}: ${inst.baseUrl} (org: ${inst.orgSlug})${projs}`);
147
- }
148
- console.log();
149
- }
150
-
151
- const action = await ask("Add new instance, or fresh setup? (add/fresh): ");
152
- if (action.trim().toLowerCase() === "fresh") {
153
- // Fall through to single setup
154
- } else {
155
- return await addInstance(existing);
156
- }
157
- }
158
-
159
- console.log("To get your Sentry API token:");
160
- console.log("1. Go to https://sentry.io/settings/account/api/auth-tokens/");
161
- console.log(" (or your self-hosted Sentry Settings > API > Auth Tokens)");
162
- console.log("2. Create a new token with scopes: event:read, project:read, org:read, member:read");
163
- console.log("3. Copy the token\n");
164
-
165
- const token = await ask("Sentry API token: ");
166
- const baseUrl = await ask("Sentry base URL (e.g., https://sentry.io or https://my-org.sentry.io): ");
167
- const orgSlug = await ask("Organization slug: ");
168
- const projectSlug = await ask("Default project slug (optional, press Enter to skip): ");
169
-
170
- saveConfig({
171
- token: token.trim(),
172
- baseUrl: baseUrl.trim().replace(/\/+$/, ""),
173
- orgSlug: orgSlug.trim(),
174
- projectSlug: projectSlug.trim() || "",
175
- });
176
- console.log(`\nConfig saved to ${configPath}`);
177
-
178
- printSetupComplete();
179
- rl.close();
180
- }
181
-
182
- async function addInstance(config) {
183
- // Migrate old format if needed
184
- if (config.token && !config.instances) {
185
- const oldName = await ask("Name for your existing instance (e.g., work): ");
186
- const oldProjects = await ask("Project slugs for existing instance (comma-separated): ");
187
- config = {
188
- instances: [{
189
- name: oldName.trim() || "default",
190
- token: config.token,
191
- baseUrl: config.baseUrl,
192
- orgSlug: config.orgSlug,
193
- projectSlug: config.projectSlug || "",
194
- projects: oldProjects.split(",").map((p) => p.trim().toLowerCase()).filter(Boolean),
195
- }],
196
- defaultInstance: oldName.trim() || "default",
197
- };
198
- }
199
-
200
- if (!config.instances) config.instances = [];
201
-
202
- console.log("\n--- Add New Instance ---\n");
203
- console.log("To get your Sentry API token:");
204
- console.log("1. Go to https://sentry.io/settings/account/api/auth-tokens/");
205
- console.log(" (or your self-hosted Sentry Settings > API > Auth Tokens)");
206
- console.log("2. Create a new token with scopes: event:read, project:read, org:read, member:read");
207
- console.log("3. Copy the token\n");
208
-
209
- const name = await ask("Instance name (e.g., personal): ");
210
- const token = await ask("Sentry API token: ");
211
- const baseUrl = await ask("Sentry base URL (e.g., https://sentry.io or https://my-org.sentry.io): ");
212
- const orgSlug = await ask("Organization slug: ");
213
- const projectSlug = await ask("Default project slug (optional, press Enter to skip): ");
214
- const projectsInput = await ask("Project slugs to route to this instance (comma-separated, optional): ");
215
- const projects = projectsInput.split(",").map((p) => p.trim().toLowerCase()).filter(Boolean);
216
-
217
- const instance = {
218
- name: name.trim(),
219
- token: token.trim(),
220
- baseUrl: baseUrl.trim().replace(/\/+$/, ""),
221
- orgSlug: orgSlug.trim(),
222
- projectSlug: projectSlug.trim() || "",
223
- projects,
224
- };
225
-
226
- // Replace or add
227
- const idx = config.instances.findIndex((i) => i.name === instance.name);
228
- if (idx >= 0) {
229
- config.instances[idx] = instance;
230
- } else {
231
- config.instances.push(instance);
232
- }
233
-
234
- if (!config.defaultInstance) config.defaultInstance = instance.name;
235
-
236
- const setDefault = await ask(`Set "${instance.name}" as default? (y/N): `);
237
- if (setDefault.trim().toLowerCase() === "y") {
238
- config.defaultInstance = instance.name;
239
- }
240
-
241
- saveConfig(config);
242
- console.log(`\nInstance "${instance.name}" saved to ${configPath}`);
243
-
244
- console.log("\nAll instances:");
245
- for (const inst of config.instances) {
246
- const isDefault = inst.name === config.defaultInstance ? " (default)" : "";
247
- const projs = inst.projects?.length > 0 ? ` [${inst.projects.join(", ")}]` : "";
248
- console.log(` - ${inst.name}${isDefault}: ${inst.baseUrl} (org: ${inst.orgSlug})${projs}`);
249
- }
250
-
251
- printSetupComplete();
252
- rl.close();
253
- }
254
-
255
- function printSetupComplete() {
256
- console.log("\n=== Setup Complete ===");
257
- console.log("\nIf you haven't already, add to Claude Code with:\n");
258
- console.log(" claude mcp add --transport stdio sentry -- npx -y @rui.branco/sentry-mcp");
259
- console.log("\nThen restart Claude Code and run /mcp to verify.");
260
- }
261
-
262
- setup().catch((e) => {
263
- console.error("Setup failed:", e.message);
264
- rl.close();
265
- process.exit(1);
266
- });
1
+ #!/usr/bin/env node
2
+
3
+ const readline = require("readline");
4
+ const fs = require("fs");
5
+ const os = require("os");
6
+ const path = require("path");
7
+
8
+ const configDir = path.join(os.homedir(), ".config/sentry-mcp");
9
+ const configPath = path.join(configDir, "config.json");
10
+
11
+ // Check for command line arguments
12
+ let args = process.argv.slice(2);
13
+ // Skip "setup" arg if called via index.js
14
+ if (args[0] === "setup") args = args.slice(1);
15
+
16
+ // Load existing config if present
17
+ function loadConfig() {
18
+ try {
19
+ if (fs.existsSync(configPath)) {
20
+ return JSON.parse(fs.readFileSync(configPath, "utf8"));
21
+ }
22
+ } catch {}
23
+ return null;
24
+ }
25
+
26
+ function saveConfig(config) {
27
+ if (!fs.existsSync(configDir)) {
28
+ fs.mkdirSync(configDir, { recursive: true });
29
+ }
30
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
31
+ }
32
+
33
+ // Non-interactive: setup add <name> <token> <baseUrl> <orgSlug> [projectSlug] [projects...]
34
+ if (args[0] === "add" && args.length >= 4) {
35
+ const [, name, token, baseUrl, orgSlug, projectSlug, ...projectArgs] = args;
36
+ const projects = projectArgs.length > 0
37
+ ? projectArgs.join(",").split(",").map((p) => p.trim().toLowerCase()).filter(Boolean)
38
+ : [];
39
+
40
+ const config = loadConfig() || {};
41
+
42
+ // Migrate old format if needed
43
+ if (config.token && !config.instances) {
44
+ config.instances = [{
45
+ name: "default",
46
+ token: config.token,
47
+ baseUrl: config.baseUrl,
48
+ orgSlug: config.orgSlug,
49
+ projectSlug: config.projectSlug || "",
50
+ projects: [],
51
+ }];
52
+ config.defaultInstance = "default";
53
+ delete config.token;
54
+ delete config.baseUrl;
55
+ delete config.orgSlug;
56
+ delete config.projectSlug;
57
+ }
58
+
59
+ if (!config.instances) config.instances = [];
60
+
61
+ // Replace or add instance
62
+ const existing = config.instances.findIndex((i) => i.name === name);
63
+ const instance = {
64
+ name,
65
+ token,
66
+ baseUrl: baseUrl.replace(/\/+$/, ""),
67
+ orgSlug,
68
+ projectSlug: projectSlug || "",
69
+ projects,
70
+ };
71
+ if (existing >= 0) {
72
+ config.instances[existing] = instance;
73
+ } else {
74
+ config.instances.push(instance);
75
+ }
76
+
77
+ if (!config.defaultInstance) config.defaultInstance = name;
78
+
79
+ saveConfig(config);
80
+ console.log(`Instance "${name}" saved to ${configPath}`);
81
+ if (projects.length > 0) {
82
+ console.log(`Projects: ${projects.join(", ")}`);
83
+ }
84
+ process.exit(0);
85
+ }
86
+
87
+ // Non-interactive: setup remove <name>
88
+ if (args[0] === "remove" && args.length >= 2) {
89
+ const name = args[1];
90
+ const config = loadConfig();
91
+
92
+ if (!config || !config.instances) {
93
+ console.error("No multi-instance config found.");
94
+ process.exit(1);
95
+ }
96
+
97
+ config.instances = config.instances.filter((i) => i.name !== name);
98
+ if (config.defaultInstance === name) {
99
+ config.defaultInstance = config.instances[0]?.name || null;
100
+ }
101
+
102
+ saveConfig(config);
103
+ console.log(`Instance "${name}" removed.`);
104
+ process.exit(0);
105
+ }
106
+
107
+ // Non-interactive: setup <token> <baseUrl> <orgSlug> [projectSlug]
108
+ if (args.length >= 3 && args[0] !== "add" && args[0] !== "remove") {
109
+ const [token, baseUrl, orgSlug, projectSlug] = args;
110
+ saveConfig({
111
+ token,
112
+ baseUrl: baseUrl.replace(/\/+$/, ""),
113
+ orgSlug,
114
+ projectSlug: projectSlug || "",
115
+ });
116
+ console.log(`Config saved to ${configPath}`);
117
+ process.exit(0);
118
+ }
119
+
120
+ // Interactive mode
121
+ const rl = readline.createInterface({
122
+ input: process.stdin,
123
+ output: process.stdout,
124
+ });
125
+
126
+ function ask(question) {
127
+ return new Promise((resolve) => {
128
+ rl.question(question, resolve);
129
+ });
130
+ }
131
+
132
+ async function setup() {
133
+ const existing = loadConfig();
134
+ const hasInstances = existing?.instances?.length > 0;
135
+ const isOldFormat = existing?.token && !existing?.instances;
136
+
137
+ console.log("\n=== Sentry MCP Setup ===\n");
138
+
139
+ if (hasInstances || isOldFormat) {
140
+ if (isOldFormat) {
141
+ console.log(`Existing single-instance config found (${existing.baseUrl})\n`);
142
+ } else {
143
+ console.log("Existing instances:");
144
+ for (const inst of existing.instances) {
145
+ const isDefault = inst.name === existing.defaultInstance ? " (default)" : "";
146
+ const projs = inst.projects?.length > 0 ? ` [${inst.projects.join(", ")}]` : "";
147
+ console.log(` - ${inst.name}${isDefault}: ${inst.baseUrl} (org: ${inst.orgSlug})${projs}`);
148
+ }
149
+ console.log();
150
+ }
151
+
152
+ const action = await ask("Add new instance, or fresh setup? (add/fresh): ");
153
+ if (action.trim().toLowerCase() === "fresh") {
154
+ // Fall through to single setup
155
+ } else {
156
+ return await addInstance(existing);
157
+ }
158
+ }
159
+
160
+ console.log("To get your Sentry API token:");
161
+ console.log("1. Go to https://sentry.io/settings/account/api/auth-tokens/");
162
+ console.log(" (or your self-hosted Sentry Settings > API > Auth Tokens)");
163
+ console.log("2. Create a new token with scopes: event:read, project:read, org:read, member:read");
164
+ console.log("3. Copy the token\n");
165
+
166
+ const token = await ask("Sentry API token: ");
167
+ const baseUrl = await ask("Sentry base URL (e.g., https://sentry.io or https://my-org.sentry.io): ");
168
+ const orgSlug = await ask("Organization slug: ");
169
+ const projectSlug = await ask("Default project slug (optional, press Enter to skip): ");
170
+
171
+ saveConfig({
172
+ token: token.trim(),
173
+ baseUrl: baseUrl.trim().replace(/\/+$/, ""),
174
+ orgSlug: orgSlug.trim(),
175
+ projectSlug: projectSlug.trim() || "",
176
+ });
177
+ console.log(`\nConfig saved to ${configPath}`);
178
+
179
+ printSetupComplete();
180
+ rl.close();
181
+ }
182
+
183
+ async function addInstance(config) {
184
+ // Migrate old format if needed
185
+ if (config.token && !config.instances) {
186
+ const oldName = await ask("Name for your existing instance (e.g., work): ");
187
+ const oldProjects = await ask("Project slugs for existing instance (comma-separated): ");
188
+ config = {
189
+ instances: [{
190
+ name: oldName.trim() || "default",
191
+ token: config.token,
192
+ baseUrl: config.baseUrl,
193
+ orgSlug: config.orgSlug,
194
+ projectSlug: config.projectSlug || "",
195
+ projects: oldProjects.split(",").map((p) => p.trim().toLowerCase()).filter(Boolean),
196
+ }],
197
+ defaultInstance: oldName.trim() || "default",
198
+ };
199
+ }
200
+
201
+ if (!config.instances) config.instances = [];
202
+
203
+ console.log("\n--- Add New Instance ---\n");
204
+ console.log("To get your Sentry API token:");
205
+ console.log("1. Go to https://sentry.io/settings/account/api/auth-tokens/");
206
+ console.log(" (or your self-hosted Sentry Settings > API > Auth Tokens)");
207
+ console.log("2. Create a new token with scopes: event:read, project:read, org:read, member:read");
208
+ console.log("3. Copy the token\n");
209
+
210
+ const name = await ask("Instance name (e.g., personal): ");
211
+ const token = await ask("Sentry API token: ");
212
+ const baseUrl = await ask("Sentry base URL (e.g., https://sentry.io or https://my-org.sentry.io): ");
213
+ const orgSlug = await ask("Organization slug: ");
214
+ const projectSlug = await ask("Default project slug (optional, press Enter to skip): ");
215
+ const projectsInput = await ask("Project slugs to route to this instance (comma-separated, optional): ");
216
+ const projects = projectsInput.split(",").map((p) => p.trim().toLowerCase()).filter(Boolean);
217
+
218
+ const instance = {
219
+ name: name.trim(),
220
+ token: token.trim(),
221
+ baseUrl: baseUrl.trim().replace(/\/+$/, ""),
222
+ orgSlug: orgSlug.trim(),
223
+ projectSlug: projectSlug.trim() || "",
224
+ projects,
225
+ };
226
+
227
+ // Replace or add
228
+ const idx = config.instances.findIndex((i) => i.name === instance.name);
229
+ if (idx >= 0) {
230
+ config.instances[idx] = instance;
231
+ } else {
232
+ config.instances.push(instance);
233
+ }
234
+
235
+ if (!config.defaultInstance) config.defaultInstance = instance.name;
236
+
237
+ const setDefault = await ask(`Set "${instance.name}" as default? (y/N): `);
238
+ if (setDefault.trim().toLowerCase() === "y") {
239
+ config.defaultInstance = instance.name;
240
+ }
241
+
242
+ saveConfig(config);
243
+ console.log(`\nInstance "${instance.name}" saved to ${configPath}`);
244
+
245
+ console.log("\nAll instances:");
246
+ for (const inst of config.instances) {
247
+ const isDefault = inst.name === config.defaultInstance ? " (default)" : "";
248
+ const projs = inst.projects?.length > 0 ? ` [${inst.projects.join(", ")}]` : "";
249
+ console.log(` - ${inst.name}${isDefault}: ${inst.baseUrl} (org: ${inst.orgSlug})${projs}`);
250
+ }
251
+
252
+ printSetupComplete();
253
+ rl.close();
254
+ }
255
+
256
+ function printSetupComplete() {
257
+ console.log("\n=== Setup Complete ===");
258
+ console.log("\nIf you haven't already, add to Claude Code with:\n");
259
+ console.log(" claude mcp add --transport stdio sentry -- npx -y @rui.branco/sentry-mcp");
260
+ console.log("\nThen restart Claude Code and run /mcp to verify.");
261
+ }
262
+
263
+ setup().catch((e) => {
264
+ console.error("Setup failed:", e.message);
265
+ rl.close();
266
+ process.exit(1);
267
+ });