@rustrak/mcp 0.1.0

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 (4) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +158 -0
  3. package/dist/index.js +459 -0
  4. package/package.json +49 -0
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ GNU GENERAL PUBLIC LICENSE
2
+ Version 3, 29 June 2007
3
+
4
+ Copyright (C) 2026 Abian Suarez
5
+
6
+ Everyone is permitted to copy and distribute verbatim copies
7
+ of this license document, but changing it is not allowed.
8
+
9
+ This program is free software: you can redistribute it and/or modify
10
+ it under the terms of the GNU General Public License as published by
11
+ the Free Software Foundation, either version 3 of the License, or
12
+ (at your option) any later version.
13
+
14
+ This program is distributed in the hope that it will be useful,
15
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ GNU General Public License for more details.
18
+
19
+ You should have received a copy of the GNU General Public License
20
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
21
+
22
+ For the full license text, see: https://www.gnu.org/licenses/gpl-3.0.txt
package/README.md ADDED
@@ -0,0 +1,158 @@
1
+ # @rustrak/mcp
2
+
3
+ MCP (Model Context Protocol) server for Rustrak. Lets AI assistants (Claude Desktop, Cursor, etc.) manage your error tracking directly — list projects, inspect issues, view stack traces, resolve errors, and manage tokens without leaving your AI tool.
4
+
5
+ ## Features
6
+
7
+ - **18 tools** covering projects, issues, events, tokens, and alert channels
8
+ - **stdio transport** — runs as a local process, no network port needed
9
+ - **Secure** — API token loaded from env vars, never passed as tool argument
10
+ - **Safe destructive actions** — `delete_issue` and `revoke_token` annotated with `destructiveHint`
11
+ - **Graceful errors** — all API errors returned as `isError: true` content, never thrown
12
+
13
+ ## Quick Start
14
+
15
+ ### 1. Generate an API token
16
+
17
+ In the Rustrak web UI: **Settings → Tokens → Create token**. Save the full token value — it is shown only once.
18
+
19
+ ### 2. Configure in Claude Desktop
20
+
21
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
22
+
23
+ ```json
24
+ {
25
+ "mcpServers": {
26
+ "rustrak": {
27
+ "command": "npx",
28
+ "args": ["-y", "@rustrak/mcp"],
29
+ "env": {
30
+ "RUSTRAK_API_URL": "https://your-rustrak-instance.example.com",
31
+ "RUSTRAK_API_TOKEN": "your-40-char-hex-token"
32
+ }
33
+ }
34
+ }
35
+ }
36
+ ```
37
+
38
+ ### 3. Local monorepo (development)
39
+
40
+ Build first, then configure with a `node` command pointing to the dist:
41
+
42
+ ```json
43
+ {
44
+ "mcpServers": {
45
+ "rustrak": {
46
+ "command": "node",
47
+ "args": ["packages/mcp/dist/index.js"],
48
+ "env": {
49
+ "RUSTRAK_API_URL": "http://localhost:8080",
50
+ "RUSTRAK_API_TOKEN": "your-40-char-hex-token"
51
+ }
52
+ }
53
+ }
54
+ }
55
+ ```
56
+
57
+ Or use the project-level `.mcp.json` at the repo root (Claude Code picks this up automatically).
58
+
59
+ ## Environment Variables
60
+
61
+ | Variable | Required | Default | Description |
62
+ |----------|----------|---------|-------------|
63
+ | `RUSTRAK_API_TOKEN` | ✅ | — | 40-char hex API token. Server exits if missing. |
64
+ | `RUSTRAK_API_URL` | ✅ | — | Base URL of your Rustrak server. Server exits if missing. |
65
+
66
+ ## Available Tools
67
+
68
+ ### Projects
69
+ | Tool | Description |
70
+ |------|-------------|
71
+ | `list_projects` | List all projects |
72
+ | `get_project` | Get project details including DSN |
73
+ | `create_project` | Create a new project |
74
+
75
+ ### Issues
76
+ | Tool | Description |
77
+ |------|-------------|
78
+ | `list_issues` | List issues with filters (open / resolved / muted / all) |
79
+ | `get_issue` | Get a single issue with full details |
80
+ | `resolve_issue` | Mark an issue as resolved |
81
+ | `unresolve_issue` | Re-open a resolved issue |
82
+ | `mute_issue` | Mute an issue (silences alerts) |
83
+ | `delete_issue` | ⚠️ Permanently delete an issue and all its events |
84
+
85
+ ### Events
86
+ | Tool | Description |
87
+ |------|-------------|
88
+ | `list_events` | List raw events for an issue (cursor pagination) |
89
+ | `get_event` | Get a single event with full Sentry envelope data |
90
+
91
+ ### Tokens
92
+ | Tool | Description |
93
+ |------|-------------|
94
+ | `list_tokens` | List API tokens (masked) |
95
+ | `create_token` | Create a new API token (full value shown once) |
96
+ | `revoke_token` | ⚠️ Permanently revoke an API token |
97
+
98
+ ### Alerts
99
+ | Tool | Description |
100
+ |------|-------------|
101
+ | `list_alert_channels` | List notification channels (Slack, email, webhook) |
102
+ | `test_alert_channel` | Send a test notification to a channel |
103
+ | `list_alert_rules` | List alert rules for a project |
104
+
105
+ > ⚠️ Tools marked as destructive will prompt for confirmation in supported clients.
106
+
107
+ ## Example prompts
108
+
109
+ Once connected, you can ask your AI assistant things like:
110
+
111
+ - _"List all unresolved issues in project 1"_
112
+ - _"Show me the stack trace for issue abc-123"_
113
+ - _"Resolve all TypeError issues from the last deployment"_
114
+ - _"Create a token called 'CI pipeline' and give me the value"_
115
+ - _"How many events does issue xyz have?"_
116
+
117
+ ## Development
118
+
119
+ ```bash
120
+ # Build
121
+ pnpm --filter @rustrak/mcp build
122
+
123
+ # Run tests (33 tests)
124
+ pnpm --filter @rustrak/mcp test
125
+
126
+ # Type check
127
+ pnpm --filter @rustrak/mcp typecheck
128
+
129
+ # Watch mode
130
+ pnpm --filter @rustrak/mcp dev
131
+ ```
132
+
133
+ ## Architecture
134
+
135
+ ```
136
+ AI Client (Claude Desktop / Cursor)
137
+ │ stdio (JSON-RPC)
138
+
139
+ ┌─────────────────────┐
140
+ │ @rustrak/mcp │
141
+ │ McpServer │
142
+ │ ├── projects │
143
+ │ ├── issues │
144
+ │ ├── events │
145
+ │ ├── tokens │
146
+ │ └── alerts │
147
+ └──────────┬──────────┘
148
+ │ HTTP (Bearer token)
149
+
150
+ ┌─────────────────────┐
151
+ │ Rustrak Server │
152
+ │ (Rust/Actix-web) │
153
+ └─────────────────────┘
154
+ ```
155
+
156
+ ## License
157
+
158
+ GPL-3.0
package/dist/index.js ADDED
@@ -0,0 +1,459 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { RustrakClient } from "@rustrak/client";
6
+
7
+ // src/config.ts
8
+ function loadConfig() {
9
+ const url = process.env["RUSTRAK_API_URL"];
10
+ if (!url) {
11
+ console.error(
12
+ "[rustrak-mcp] Missing required environment variable: RUSTRAK_API_URL. Set RUSTRAK_API_URL to the base URL of your Rustrak server."
13
+ );
14
+ process.exit(1);
15
+ }
16
+ const token = process.env["RUSTRAK_API_TOKEN"];
17
+ if (!token) {
18
+ console.error(
19
+ "[rustrak-mcp] Missing required environment variable: RUSTRAK_API_TOKEN. Set RUSTRAK_API_TOKEN to a valid Rustrak API token."
20
+ );
21
+ process.exit(1);
22
+ }
23
+ return { RUSTRAK_API_URL: url, RUSTRAK_API_TOKEN: token };
24
+ }
25
+
26
+ // src/server.ts
27
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
28
+
29
+ // src/tools/alerts.ts
30
+ import { z } from "zod";
31
+
32
+ // src/errors.ts
33
+ import {
34
+ AuthenticationError,
35
+ NotFoundError,
36
+ RateLimitError,
37
+ RustrakError
38
+ } from "@rustrak/client";
39
+ function mcpError(text) {
40
+ return { content: [{ type: "text", text }], isError: true };
41
+ }
42
+ function toMcpError(err) {
43
+ if (err instanceof NotFoundError) {
44
+ return mcpError(`Not found: ${err.message}`);
45
+ }
46
+ if (err instanceof RateLimitError) {
47
+ const after = err.retryAfter !== void 0 ? err.retryAfter : "?";
48
+ return mcpError(`Rate limited. Retry after: ${after}s`);
49
+ }
50
+ if (err instanceof AuthenticationError) {
51
+ return mcpError("Authentication failed. Check RUSTRAK_API_TOKEN.");
52
+ }
53
+ if (err instanceof RustrakError) {
54
+ return mcpError(`API error: ${err.message}`);
55
+ }
56
+ return mcpError(`Unexpected error: ${String(err)}`);
57
+ }
58
+
59
+ // src/tools/alerts.ts
60
+ function registerAlertTools(server2, client2) {
61
+ server2.registerTool(
62
+ "list_alert_channels",
63
+ {
64
+ description: "List all configured alert notification channels (Slack, email, webhook, etc.).",
65
+ inputSchema: {}
66
+ },
67
+ async () => {
68
+ try {
69
+ const result = await client2.alertChannels.list();
70
+ return {
71
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
72
+ };
73
+ } catch (err) {
74
+ return toMcpError(err);
75
+ }
76
+ }
77
+ );
78
+ server2.registerTool(
79
+ "test_alert_channel",
80
+ {
81
+ description: "Send a test notification to an alert channel to verify it is configured correctly.",
82
+ inputSchema: {
83
+ channel_id: z.number().int().describe("Alert channel ID to test")
84
+ }
85
+ },
86
+ async ({ channel_id }) => {
87
+ try {
88
+ const result = await client2.alertChannels.test(channel_id);
89
+ return {
90
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
91
+ };
92
+ } catch (err) {
93
+ return toMcpError(err);
94
+ }
95
+ }
96
+ );
97
+ server2.registerTool(
98
+ "list_alert_rules",
99
+ {
100
+ description: "List all alert rules configured for a project.",
101
+ inputSchema: {
102
+ project_id: z.number().int().describe("Project ID")
103
+ }
104
+ },
105
+ async ({ project_id }) => {
106
+ try {
107
+ const result = await client2.alertRules.list(project_id);
108
+ return {
109
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
110
+ };
111
+ } catch (err) {
112
+ return toMcpError(err);
113
+ }
114
+ }
115
+ );
116
+ }
117
+
118
+ // src/tools/events.ts
119
+ import { z as z2 } from "zod";
120
+ function registerEventTools(server2, client2) {
121
+ server2.registerTool(
122
+ "list_events",
123
+ {
124
+ description: "List raw events for a specific issue. Events are individual error occurrences within a grouped issue.",
125
+ inputSchema: {
126
+ project_id: z2.number().int().describe("Project ID"),
127
+ issue_id: z2.string().describe("Issue ID"),
128
+ cursor: z2.string().optional().describe("Pagination cursor"),
129
+ order: z2.enum(["asc", "desc"]).optional().describe("Sort order (default: desc)")
130
+ }
131
+ },
132
+ async ({ project_id, issue_id, cursor, order }) => {
133
+ try {
134
+ const result = await client2.events.list(project_id, issue_id, {
135
+ cursor,
136
+ order
137
+ });
138
+ return {
139
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
140
+ };
141
+ } catch (err) {
142
+ return toMcpError(err);
143
+ }
144
+ }
145
+ );
146
+ server2.registerTool(
147
+ "get_event",
148
+ {
149
+ description: "Get full detail for a single event, including the complete Sentry envelope data.",
150
+ inputSchema: {
151
+ project_id: z2.number().int().describe("Project ID"),
152
+ issue_id: z2.string().describe("Issue ID"),
153
+ event_id: z2.string().describe("Event ID")
154
+ }
155
+ },
156
+ async ({ project_id, issue_id, event_id }) => {
157
+ try {
158
+ const result = await client2.events.get(project_id, issue_id, event_id);
159
+ return {
160
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
161
+ };
162
+ } catch (err) {
163
+ return toMcpError(err);
164
+ }
165
+ }
166
+ );
167
+ }
168
+
169
+ // src/tools/issues.ts
170
+ import { z as z3 } from "zod";
171
+ function registerIssueTools(server2, client2) {
172
+ server2.registerTool(
173
+ "list_issues",
174
+ {
175
+ description: "List issues for a Rustrak project. Returns paginated grouped error occurrences.",
176
+ inputSchema: {
177
+ project_id: z3.number().int().describe("Project ID"),
178
+ page: z3.number().int().min(1).optional(),
179
+ per_page: z3.number().int().min(1).max(100).optional(),
180
+ filter: z3.enum(["open", "resolved", "muted", "all"]).optional().describe("Filter issues by state (default: open)")
181
+ }
182
+ },
183
+ async ({ project_id, page, per_page, filter }) => {
184
+ try {
185
+ const result = await client2.issues.list(project_id, {
186
+ page,
187
+ per_page,
188
+ filter
189
+ });
190
+ return {
191
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
192
+ };
193
+ } catch (err) {
194
+ return toMcpError(err);
195
+ }
196
+ }
197
+ );
198
+ server2.registerTool(
199
+ "get_issue",
200
+ {
201
+ description: "Get a single issue by ID.",
202
+ inputSchema: {
203
+ project_id: z3.number().int().describe("Project ID"),
204
+ issue_id: z3.string().describe("Issue ID")
205
+ }
206
+ },
207
+ async ({ project_id, issue_id }) => {
208
+ try {
209
+ const result = await client2.issues.get(project_id, issue_id);
210
+ return {
211
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
212
+ };
213
+ } catch (err) {
214
+ return toMcpError(err);
215
+ }
216
+ }
217
+ );
218
+ server2.registerTool(
219
+ "resolve_issue",
220
+ {
221
+ description: "Mark an issue as resolved.",
222
+ inputSchema: {
223
+ project_id: z3.number().int().describe("Project ID"),
224
+ issue_id: z3.string().describe("Issue ID")
225
+ }
226
+ },
227
+ async ({ project_id, issue_id }) => {
228
+ try {
229
+ const result = await client2.issues.updateState(project_id, issue_id, {
230
+ is_resolved: true
231
+ });
232
+ return {
233
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
234
+ };
235
+ } catch (err) {
236
+ return toMcpError(err);
237
+ }
238
+ }
239
+ );
240
+ server2.registerTool(
241
+ "unresolve_issue",
242
+ {
243
+ description: "Mark a resolved issue as unresolved (re-open it).",
244
+ inputSchema: {
245
+ project_id: z3.number().int().describe("Project ID"),
246
+ issue_id: z3.string().describe("Issue ID")
247
+ }
248
+ },
249
+ async ({ project_id, issue_id }) => {
250
+ try {
251
+ const result = await client2.issues.updateState(project_id, issue_id, {
252
+ is_resolved: false
253
+ });
254
+ return {
255
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
256
+ };
257
+ } catch (err) {
258
+ return toMcpError(err);
259
+ }
260
+ }
261
+ );
262
+ server2.registerTool(
263
+ "mute_issue",
264
+ {
265
+ description: "Mute an issue so it no longer triggers alerts.",
266
+ inputSchema: {
267
+ project_id: z3.number().int().describe("Project ID"),
268
+ issue_id: z3.string().describe("Issue ID")
269
+ }
270
+ },
271
+ async ({ project_id, issue_id }) => {
272
+ try {
273
+ const result = await client2.issues.updateState(project_id, issue_id, {
274
+ is_muted: true
275
+ });
276
+ return {
277
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
278
+ };
279
+ } catch (err) {
280
+ return toMcpError(err);
281
+ }
282
+ }
283
+ );
284
+ server2.registerTool(
285
+ "delete_issue",
286
+ {
287
+ description: "Permanently delete an issue and all its events.",
288
+ inputSchema: {
289
+ project_id: z3.number().int().describe("Project ID"),
290
+ issue_id: z3.string().describe("Issue ID")
291
+ },
292
+ annotations: { destructiveHint: true }
293
+ },
294
+ async ({ project_id, issue_id }) => {
295
+ try {
296
+ await client2.issues.delete(project_id, issue_id);
297
+ return {
298
+ content: [
299
+ { type: "text", text: `Issue ${issue_id} deleted successfully.` }
300
+ ]
301
+ };
302
+ } catch (err) {
303
+ return toMcpError(err);
304
+ }
305
+ }
306
+ );
307
+ }
308
+
309
+ // src/tools/projects.ts
310
+ import { z as z4 } from "zod";
311
+ function registerProjectTools(server2, client2) {
312
+ server2.registerTool(
313
+ "list_projects",
314
+ {
315
+ description: "List all Rustrak projects you have access to.",
316
+ inputSchema: {
317
+ page: z4.number().int().min(1).optional().describe("Page number"),
318
+ per_page: z4.number().int().min(1).max(100).optional().describe("Items per page")
319
+ }
320
+ },
321
+ async ({ page, per_page }) => {
322
+ try {
323
+ const result = await client2.projects.list({ page, per_page });
324
+ return {
325
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
326
+ };
327
+ } catch (err) {
328
+ return toMcpError(err);
329
+ }
330
+ }
331
+ );
332
+ server2.registerTool(
333
+ "get_project",
334
+ {
335
+ description: "Get details for a single Rustrak project.",
336
+ inputSchema: {
337
+ project_id: z4.number().int().describe("Project ID")
338
+ }
339
+ },
340
+ async ({ project_id }) => {
341
+ try {
342
+ const result = await client2.projects.get(project_id);
343
+ return {
344
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
345
+ };
346
+ } catch (err) {
347
+ return toMcpError(err);
348
+ }
349
+ }
350
+ );
351
+ server2.registerTool(
352
+ "create_project",
353
+ {
354
+ description: "Create a new Rustrak project.",
355
+ inputSchema: {
356
+ name: z4.string().min(1).describe("Project name"),
357
+ slug: z4.string().optional().describe("URL-safe slug (auto-generated if omitted)")
358
+ }
359
+ },
360
+ async ({ name, slug }) => {
361
+ try {
362
+ const result = await client2.projects.create({ name, slug });
363
+ return {
364
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
365
+ };
366
+ } catch (err) {
367
+ return toMcpError(err);
368
+ }
369
+ }
370
+ );
371
+ }
372
+
373
+ // src/tools/tokens.ts
374
+ import { z as z5 } from "zod";
375
+ function registerTokenTools(server2, client2) {
376
+ server2.registerTool(
377
+ "list_tokens",
378
+ {
379
+ description: "List all API tokens. Token values are masked \u2014 only the prefix is shown.",
380
+ inputSchema: {}
381
+ },
382
+ async () => {
383
+ try {
384
+ const result = await client2.tokens.list();
385
+ return {
386
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
387
+ };
388
+ } catch (err) {
389
+ return toMcpError(err);
390
+ }
391
+ }
392
+ );
393
+ server2.registerTool(
394
+ "create_token",
395
+ {
396
+ description: "Create a new API token. The full token value is returned ONCE \u2014 save it immediately.",
397
+ inputSchema: {
398
+ description: z5.string().min(1).describe("Human-readable label for this token")
399
+ }
400
+ },
401
+ async ({ description }) => {
402
+ try {
403
+ const result = await client2.tokens.create({ description });
404
+ return {
405
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
406
+ };
407
+ } catch (err) {
408
+ return toMcpError(err);
409
+ }
410
+ }
411
+ );
412
+ server2.registerTool(
413
+ "revoke_token",
414
+ {
415
+ description: "Permanently revoke an API token. This action cannot be undone.",
416
+ inputSchema: {
417
+ token_id: z5.number().int().describe("Token ID to revoke")
418
+ },
419
+ annotations: { destructiveHint: true }
420
+ },
421
+ async ({ token_id }) => {
422
+ try {
423
+ await client2.tokens.delete(token_id);
424
+ return {
425
+ content: [
426
+ { type: "text", text: `Token ${token_id} revoked successfully.` }
427
+ ]
428
+ };
429
+ } catch (err) {
430
+ return toMcpError(err);
431
+ }
432
+ }
433
+ );
434
+ }
435
+
436
+ // src/server.ts
437
+ function createServer(client2) {
438
+ const server2 = new McpServer({
439
+ name: "rustrak-mcp",
440
+ version: "0.1.0"
441
+ });
442
+ registerProjectTools(server2, client2);
443
+ registerIssueTools(server2, client2);
444
+ registerEventTools(server2, client2);
445
+ registerTokenTools(server2, client2);
446
+ registerAlertTools(server2, client2);
447
+ return server2;
448
+ }
449
+
450
+ // src/index.ts
451
+ var config = loadConfig();
452
+ var client = new RustrakClient({
453
+ baseUrl: config.RUSTRAK_API_URL,
454
+ token: config.RUSTRAK_API_TOKEN
455
+ });
456
+ var server = createServer(client);
457
+ var transport = new StdioServerTransport();
458
+ await server.connect(transport);
459
+ console.error("[rustrak-mcp] Server started");
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@rustrak/mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for Rustrak error tracking — wraps @rustrak/client",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "type": "module",
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ }
16
+ },
17
+ "bin": {
18
+ "rustrak-mcp": "./dist/index.js"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "README.md"
23
+ ],
24
+ "keywords": [
25
+ "rustrak",
26
+ "mcp",
27
+ "model-context-protocol",
28
+ "error-tracking",
29
+ "claude"
30
+ ],
31
+ "license": "GPL-3.0",
32
+ "dependencies": {
33
+ "@modelcontextprotocol/sdk": "1.29.0",
34
+ "zod": "4.4.3",
35
+ "@rustrak/client": "0.1.1"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "25.8.0",
39
+ "tsup": "8.5.1",
40
+ "typescript": "6.0.3",
41
+ "vitest": "4.1.6"
42
+ },
43
+ "scripts": {
44
+ "build": "tsup",
45
+ "dev": "tsup --watch",
46
+ "test": "vitest run",
47
+ "typecheck": "tsc --noEmit"
48
+ }
49
+ }