rosentry-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.
- package/LICENSE +21 -0
- package/README.md +186 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +535 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 RoSentry
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# rosentry-mcp
|
|
2
|
+
|
|
3
|
+
MCP (Model Context Protocol) server for [RoSentry](https://rosentry.dev) - AI-powered error monitoring for Roblox games.
|
|
4
|
+
|
|
5
|
+
This package allows Claude and other MCP-compatible AI assistants to query, analyze, and manage errors from your Roblox games directly in conversation.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Query Errors** - Fetch recent errors with filters (level, user, place)
|
|
10
|
+
- **Error Groups** - View deduplicated/grouped errors with occurrence counts
|
|
11
|
+
- **Trends Analysis** - See error frequency over time (1h, 24h, 7d, 30d)
|
|
12
|
+
- **Full-Text Search** - Search across error messages and stack traces
|
|
13
|
+
- **User Impact** - See which Roblox users are affected by specific errors
|
|
14
|
+
- **Status Management** - Resolve, ignore, or reopen error groups
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g rosentry-mcp
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or use with npx (no install required):
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx rosentry-mcp
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Configuration
|
|
29
|
+
|
|
30
|
+
### Environment Variables
|
|
31
|
+
|
|
32
|
+
| Variable | Required | Description |
|
|
33
|
+
|----------|----------|-------------|
|
|
34
|
+
| `ROSENTRY_SUPABASE_URL` | Yes | Your Supabase project URL |
|
|
35
|
+
| `ROSENTRY_SUPABASE_KEY` | Yes | Supabase service role key |
|
|
36
|
+
| `ROSENTRY_API_KEY` | Yes | Your RoSentry project API key (`rs_...`) |
|
|
37
|
+
|
|
38
|
+
### Claude Desktop Setup
|
|
39
|
+
|
|
40
|
+
Add to your Claude Desktop config file:
|
|
41
|
+
|
|
42
|
+
**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
43
|
+
**Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"mcpServers": {
|
|
48
|
+
"rosentry": {
|
|
49
|
+
"command": "npx",
|
|
50
|
+
"args": ["rosentry-mcp"],
|
|
51
|
+
"env": {
|
|
52
|
+
"ROSENTRY_SUPABASE_URL": "https://your-project.supabase.co",
|
|
53
|
+
"ROSENTRY_SUPABASE_KEY": "your-service-role-key",
|
|
54
|
+
"ROSENTRY_API_KEY": "rs_your_api_key"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Or if installed globally:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"mcpServers": {
|
|
66
|
+
"rosentry": {
|
|
67
|
+
"command": "rosentry-mcp",
|
|
68
|
+
"env": {
|
|
69
|
+
"ROSENTRY_SUPABASE_URL": "https://your-project.supabase.co",
|
|
70
|
+
"ROSENTRY_SUPABASE_KEY": "your-service-role-key",
|
|
71
|
+
"ROSENTRY_API_KEY": "rs_your_api_key"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Available Tools
|
|
79
|
+
|
|
80
|
+
### `get_recent_errors`
|
|
81
|
+
Fetch recent error events with full details including stack traces and context.
|
|
82
|
+
|
|
83
|
+
| Parameter | Type | Description |
|
|
84
|
+
|-----------|------|-------------|
|
|
85
|
+
| `limit` | number | Max errors to return (default: 20, max: 100) |
|
|
86
|
+
| `level` | string | Filter by level: `error`, `warn`, `info`, `debug` |
|
|
87
|
+
| `user_id` | number | Filter by Roblox UserId |
|
|
88
|
+
| `place_id` | number | Filter by Roblox PlaceId |
|
|
89
|
+
| `search` | string | Search term for error messages |
|
|
90
|
+
|
|
91
|
+
### `get_error_groups`
|
|
92
|
+
Fetch grouped/deduplicated errors showing occurrence counts and status.
|
|
93
|
+
|
|
94
|
+
| Parameter | Type | Description |
|
|
95
|
+
|-----------|------|-------------|
|
|
96
|
+
| `limit` | number | Max groups to return (default: 20, max: 50) |
|
|
97
|
+
| `status` | string | Filter by status: `open`, `resolved`, `ignored` |
|
|
98
|
+
| `search` | string | Search term for error messages |
|
|
99
|
+
|
|
100
|
+
### `get_error_group_details`
|
|
101
|
+
Get detailed info about a specific error group including individual events.
|
|
102
|
+
|
|
103
|
+
| Parameter | Type | Description |
|
|
104
|
+
|-----------|------|-------------|
|
|
105
|
+
| `group_id` | string | **Required.** UUID of the error group |
|
|
106
|
+
| `include_events` | boolean | Include individual events (default: true) |
|
|
107
|
+
| `events_limit` | number | Max events to include (default: 10) |
|
|
108
|
+
|
|
109
|
+
### `get_error_trends`
|
|
110
|
+
Get error frequency trends over time.
|
|
111
|
+
|
|
112
|
+
| Parameter | Type | Description |
|
|
113
|
+
|-----------|------|-------------|
|
|
114
|
+
| `timeframe` | string | Time range: `1h`, `24h`, `7d`, `30d` (default: 24h) |
|
|
115
|
+
| `group_by` | string | Grouping: `hour` or `day` |
|
|
116
|
+
|
|
117
|
+
### `search_errors`
|
|
118
|
+
Full-text search across error messages and stack traces.
|
|
119
|
+
|
|
120
|
+
| Parameter | Type | Description |
|
|
121
|
+
|-----------|------|-------------|
|
|
122
|
+
| `query` | string | **Required.** Search query |
|
|
123
|
+
| `limit` | number | Max results (default: 20) |
|
|
124
|
+
|
|
125
|
+
### `get_affected_users`
|
|
126
|
+
Get Roblox users affected by a specific error group.
|
|
127
|
+
|
|
128
|
+
| Parameter | Type | Description |
|
|
129
|
+
|-----------|------|-------------|
|
|
130
|
+
| `group_id` | string | **Required.** UUID of the error group |
|
|
131
|
+
| `limit` | number | Max users to return (default: 50) |
|
|
132
|
+
|
|
133
|
+
### `get_stats`
|
|
134
|
+
Get summary statistics for your project.
|
|
135
|
+
|
|
136
|
+
| Parameter | Type | Description |
|
|
137
|
+
|-----------|------|-------------|
|
|
138
|
+
| `timeframe` | string | Time range: `1h`, `24h`, `7d`, `30d` (default: 24h) |
|
|
139
|
+
|
|
140
|
+
Returns: Total errors, errors by level, open groups count, affected users, top 5 errors.
|
|
141
|
+
|
|
142
|
+
### `update_error_status`
|
|
143
|
+
Update the status of an error group.
|
|
144
|
+
|
|
145
|
+
| Parameter | Type | Description |
|
|
146
|
+
|-----------|------|-------------|
|
|
147
|
+
| `group_id` | string | **Required.** UUID of the error group |
|
|
148
|
+
| `status` | string | **Required.** New status: `open`, `resolved`, `ignored` |
|
|
149
|
+
|
|
150
|
+
## Example Usage with Claude
|
|
151
|
+
|
|
152
|
+
Once configured, you can ask Claude things like:
|
|
153
|
+
|
|
154
|
+
- *"What errors happened in the last hour?"*
|
|
155
|
+
- *"Show me the top errors affecting users"*
|
|
156
|
+
- *"Search for errors related to 'DataStore'"*
|
|
157
|
+
- *"How many users are affected by error group abc-123?"*
|
|
158
|
+
- *"Mark that error as resolved"*
|
|
159
|
+
- *"What's the error trend for the past week?"*
|
|
160
|
+
|
|
161
|
+
## Development
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
# Clone the repo
|
|
165
|
+
git clone https://github.com/rosentry/rosentry.git
|
|
166
|
+
cd rosentry/mcp
|
|
167
|
+
|
|
168
|
+
# Install dependencies
|
|
169
|
+
npm install
|
|
170
|
+
|
|
171
|
+
# Run in development
|
|
172
|
+
npm run dev
|
|
173
|
+
|
|
174
|
+
# Build for production
|
|
175
|
+
npm run build
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Related
|
|
179
|
+
|
|
180
|
+
- [RoSentry](https://rosentry.dev) - Error monitoring platform for Roblox
|
|
181
|
+
- [RoSentry Lua SDK](https://github.com/rosentry/rosentry/tree/main/sdk) - Roblox Lua SDK
|
|
182
|
+
- [Model Context Protocol](https://modelcontextprotocol.io) - MCP specification
|
|
183
|
+
|
|
184
|
+
## License
|
|
185
|
+
|
|
186
|
+
MIT - see [LICENSE](./LICENSE)
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { createClient } from "@supabase/supabase-js";
|
|
6
|
+
// Initialize Supabase client
|
|
7
|
+
const supabaseUrl = process.env.ROSENTRY_SUPABASE_URL;
|
|
8
|
+
const supabaseKey = process.env.ROSENTRY_SUPABASE_KEY;
|
|
9
|
+
const projectApiKey = process.env.ROSENTRY_API_KEY;
|
|
10
|
+
let supabase = null;
|
|
11
|
+
let projectId = null;
|
|
12
|
+
async function initializeClient() {
|
|
13
|
+
if (!supabaseUrl || !supabaseKey) {
|
|
14
|
+
throw new Error("Missing ROSENTRY_SUPABASE_URL or ROSENTRY_SUPABASE_KEY environment variables");
|
|
15
|
+
}
|
|
16
|
+
supabase = createClient(supabaseUrl, supabaseKey);
|
|
17
|
+
// If API key is provided, get the project ID
|
|
18
|
+
if (projectApiKey) {
|
|
19
|
+
const { data: project } = await supabase
|
|
20
|
+
.from("projects")
|
|
21
|
+
.select("id")
|
|
22
|
+
.eq("api_key", projectApiKey)
|
|
23
|
+
.single();
|
|
24
|
+
if (project) {
|
|
25
|
+
projectId = project.id;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// Tool definitions
|
|
30
|
+
const tools = [
|
|
31
|
+
{
|
|
32
|
+
name: "get_recent_errors",
|
|
33
|
+
description: "Fetch recent errors from RoSentry. Returns individual error events with full details including stack traces, user context, and metadata.",
|
|
34
|
+
inputSchema: {
|
|
35
|
+
type: "object",
|
|
36
|
+
properties: {
|
|
37
|
+
limit: {
|
|
38
|
+
type: "number",
|
|
39
|
+
description: "Maximum number of errors to return (default: 20, max: 100)",
|
|
40
|
+
},
|
|
41
|
+
level: {
|
|
42
|
+
type: "string",
|
|
43
|
+
enum: ["error", "warn", "info", "debug"],
|
|
44
|
+
description: "Filter by error level",
|
|
45
|
+
},
|
|
46
|
+
user_id: {
|
|
47
|
+
type: "number",
|
|
48
|
+
description: "Filter by Roblox UserId",
|
|
49
|
+
},
|
|
50
|
+
place_id: {
|
|
51
|
+
type: "number",
|
|
52
|
+
description: "Filter by Roblox PlaceId",
|
|
53
|
+
},
|
|
54
|
+
search: {
|
|
55
|
+
type: "string",
|
|
56
|
+
description: "Search term to filter error messages",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "get_error_groups",
|
|
63
|
+
description: "Fetch grouped/deduplicated errors from RoSentry. Error groups combine similar errors by fingerprint, showing occurrence count and status.",
|
|
64
|
+
inputSchema: {
|
|
65
|
+
type: "object",
|
|
66
|
+
properties: {
|
|
67
|
+
limit: {
|
|
68
|
+
type: "number",
|
|
69
|
+
description: "Maximum number of groups to return (default: 20, max: 50)",
|
|
70
|
+
},
|
|
71
|
+
status: {
|
|
72
|
+
type: "string",
|
|
73
|
+
enum: ["open", "resolved", "ignored"],
|
|
74
|
+
description: "Filter by group status",
|
|
75
|
+
},
|
|
76
|
+
search: {
|
|
77
|
+
type: "string",
|
|
78
|
+
description: "Search term to filter error messages",
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: "get_error_group_details",
|
|
85
|
+
description: "Get detailed information about a specific error group, including all individual error events in that group.",
|
|
86
|
+
inputSchema: {
|
|
87
|
+
type: "object",
|
|
88
|
+
properties: {
|
|
89
|
+
group_id: {
|
|
90
|
+
type: "string",
|
|
91
|
+
description: "The UUID of the error group",
|
|
92
|
+
},
|
|
93
|
+
include_events: {
|
|
94
|
+
type: "boolean",
|
|
95
|
+
description: "Include individual error events (default: true)",
|
|
96
|
+
},
|
|
97
|
+
events_limit: {
|
|
98
|
+
type: "number",
|
|
99
|
+
description: "Maximum number of events to include (default: 10)",
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
required: ["group_id"],
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: "get_error_trends",
|
|
107
|
+
description: "Get error frequency trends over time. Useful for understanding when errors spike or identifying patterns.",
|
|
108
|
+
inputSchema: {
|
|
109
|
+
type: "object",
|
|
110
|
+
properties: {
|
|
111
|
+
timeframe: {
|
|
112
|
+
type: "string",
|
|
113
|
+
enum: ["1h", "24h", "7d", "30d"],
|
|
114
|
+
description: "Time range for trends (default: 24h)",
|
|
115
|
+
},
|
|
116
|
+
group_by: {
|
|
117
|
+
type: "string",
|
|
118
|
+
enum: ["hour", "day"],
|
|
119
|
+
description: "How to group the data points",
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: "search_errors",
|
|
126
|
+
description: "Full-text search across all error messages and stack traces. Returns matching errors with context.",
|
|
127
|
+
inputSchema: {
|
|
128
|
+
type: "object",
|
|
129
|
+
properties: {
|
|
130
|
+
query: {
|
|
131
|
+
type: "string",
|
|
132
|
+
description: "Search query string",
|
|
133
|
+
},
|
|
134
|
+
limit: {
|
|
135
|
+
type: "number",
|
|
136
|
+
description: "Maximum results to return (default: 20)",
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
required: ["query"],
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: "get_affected_users",
|
|
144
|
+
description: "Get list of Roblox users affected by a specific error group. Helpful for understanding impact.",
|
|
145
|
+
inputSchema: {
|
|
146
|
+
type: "object",
|
|
147
|
+
properties: {
|
|
148
|
+
group_id: {
|
|
149
|
+
type: "string",
|
|
150
|
+
description: "The UUID of the error group",
|
|
151
|
+
},
|
|
152
|
+
limit: {
|
|
153
|
+
type: "number",
|
|
154
|
+
description: "Maximum users to return (default: 50)",
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
required: ["group_id"],
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: "get_stats",
|
|
162
|
+
description: "Get summary statistics for error monitoring - total errors, errors by level, affected users, and top errors.",
|
|
163
|
+
inputSchema: {
|
|
164
|
+
type: "object",
|
|
165
|
+
properties: {
|
|
166
|
+
timeframe: {
|
|
167
|
+
type: "string",
|
|
168
|
+
enum: ["1h", "24h", "7d", "30d"],
|
|
169
|
+
description: "Time range for stats (default: 24h)",
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: "update_error_status",
|
|
176
|
+
description: "Update the status of an error group (resolve, ignore, or reopen).",
|
|
177
|
+
inputSchema: {
|
|
178
|
+
type: "object",
|
|
179
|
+
properties: {
|
|
180
|
+
group_id: {
|
|
181
|
+
type: "string",
|
|
182
|
+
description: "The UUID of the error group",
|
|
183
|
+
},
|
|
184
|
+
status: {
|
|
185
|
+
type: "string",
|
|
186
|
+
enum: ["open", "resolved", "ignored"],
|
|
187
|
+
description: "New status for the error group",
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
required: ["group_id", "status"],
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
];
|
|
194
|
+
// Tool handlers
|
|
195
|
+
async function handleGetRecentErrors(args) {
|
|
196
|
+
if (!supabase || !projectId) {
|
|
197
|
+
return { error: "Not connected to RoSentry" };
|
|
198
|
+
}
|
|
199
|
+
const limit = Math.min(args.limit ?? 20, 100);
|
|
200
|
+
let query = supabase
|
|
201
|
+
.from("errors")
|
|
202
|
+
.select("*")
|
|
203
|
+
.eq("project_id", projectId)
|
|
204
|
+
.order("timestamp", { ascending: false })
|
|
205
|
+
.limit(limit);
|
|
206
|
+
if (args.level)
|
|
207
|
+
query = query.eq("level", args.level);
|
|
208
|
+
if (args.user_id)
|
|
209
|
+
query = query.eq("user_id", args.user_id);
|
|
210
|
+
if (args.place_id)
|
|
211
|
+
query = query.eq("place_id", args.place_id);
|
|
212
|
+
if (args.search)
|
|
213
|
+
query = query.ilike("message", `%${args.search}%`);
|
|
214
|
+
const { data, error } = await query;
|
|
215
|
+
if (error)
|
|
216
|
+
return { error: error.message };
|
|
217
|
+
return { errors: data, count: data?.length ?? 0 };
|
|
218
|
+
}
|
|
219
|
+
async function handleGetErrorGroups(args) {
|
|
220
|
+
if (!supabase || !projectId) {
|
|
221
|
+
return { error: "Not connected to RoSentry" };
|
|
222
|
+
}
|
|
223
|
+
const limit = Math.min(args.limit ?? 20, 50);
|
|
224
|
+
let query = supabase
|
|
225
|
+
.from("error_groups")
|
|
226
|
+
.select("*")
|
|
227
|
+
.eq("project_id", projectId)
|
|
228
|
+
.order("last_seen", { ascending: false })
|
|
229
|
+
.limit(limit);
|
|
230
|
+
if (args.status)
|
|
231
|
+
query = query.eq("status", args.status);
|
|
232
|
+
if (args.search)
|
|
233
|
+
query = query.ilike("message", `%${args.search}%`);
|
|
234
|
+
const { data, error } = await query;
|
|
235
|
+
if (error)
|
|
236
|
+
return { error: error.message };
|
|
237
|
+
return { groups: data, count: data?.length ?? 0 };
|
|
238
|
+
}
|
|
239
|
+
async function handleGetErrorGroupDetails(args) {
|
|
240
|
+
if (!supabase || !projectId) {
|
|
241
|
+
return { error: "Not connected to RoSentry" };
|
|
242
|
+
}
|
|
243
|
+
// Get group
|
|
244
|
+
const { data: group, error: groupError } = await supabase
|
|
245
|
+
.from("error_groups")
|
|
246
|
+
.select("*")
|
|
247
|
+
.eq("id", args.group_id)
|
|
248
|
+
.eq("project_id", projectId)
|
|
249
|
+
.single();
|
|
250
|
+
if (groupError)
|
|
251
|
+
return { error: groupError.message };
|
|
252
|
+
if (!group)
|
|
253
|
+
return { error: "Error group not found" };
|
|
254
|
+
const result = { group };
|
|
255
|
+
// Get events if requested
|
|
256
|
+
if (args.include_events !== false) {
|
|
257
|
+
const { data: events } = await supabase
|
|
258
|
+
.from("errors")
|
|
259
|
+
.select("*")
|
|
260
|
+
.eq("error_group_id", args.group_id)
|
|
261
|
+
.order("timestamp", { ascending: false })
|
|
262
|
+
.limit(args.events_limit ?? 10);
|
|
263
|
+
result.events = events ?? [];
|
|
264
|
+
}
|
|
265
|
+
return result;
|
|
266
|
+
}
|
|
267
|
+
async function handleGetErrorTrends(args) {
|
|
268
|
+
if (!supabase || !projectId) {
|
|
269
|
+
return { error: "Not connected to RoSentry" };
|
|
270
|
+
}
|
|
271
|
+
const timeframe = args.timeframe ?? "24h";
|
|
272
|
+
const now = new Date();
|
|
273
|
+
let since;
|
|
274
|
+
switch (timeframe) {
|
|
275
|
+
case "1h":
|
|
276
|
+
since = new Date(now.getTime() - 60 * 60 * 1000);
|
|
277
|
+
break;
|
|
278
|
+
case "7d":
|
|
279
|
+
since = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
280
|
+
break;
|
|
281
|
+
case "30d":
|
|
282
|
+
since = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
283
|
+
break;
|
|
284
|
+
default:
|
|
285
|
+
since = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
286
|
+
}
|
|
287
|
+
const { data: errors } = await supabase
|
|
288
|
+
.from("errors")
|
|
289
|
+
.select("timestamp, level")
|
|
290
|
+
.eq("project_id", projectId)
|
|
291
|
+
.gte("timestamp", since.toISOString())
|
|
292
|
+
.order("timestamp", { ascending: true });
|
|
293
|
+
if (!errors)
|
|
294
|
+
return { trends: [], timeframe };
|
|
295
|
+
// Group by time bucket
|
|
296
|
+
const groupBy = args.group_by ?? (timeframe === "1h" ? "hour" : "day");
|
|
297
|
+
const buckets = {};
|
|
298
|
+
for (const error of errors) {
|
|
299
|
+
const date = new Date(error.timestamp);
|
|
300
|
+
let key;
|
|
301
|
+
if (groupBy === "hour") {
|
|
302
|
+
key = `${date.toISOString().slice(0, 13)}:00`;
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
key = date.toISOString().slice(0, 10);
|
|
306
|
+
}
|
|
307
|
+
if (!buckets[key]) {
|
|
308
|
+
buckets[key] = { count: 0, errors: 0, warnings: 0 };
|
|
309
|
+
}
|
|
310
|
+
buckets[key].count++;
|
|
311
|
+
if (error.level === "error")
|
|
312
|
+
buckets[key].errors++;
|
|
313
|
+
if (error.level === "warn")
|
|
314
|
+
buckets[key].warnings++;
|
|
315
|
+
}
|
|
316
|
+
const trends = Object.entries(buckets).map(([time, data]) => ({
|
|
317
|
+
time,
|
|
318
|
+
...data,
|
|
319
|
+
}));
|
|
320
|
+
return { trends, timeframe, group_by: groupBy };
|
|
321
|
+
}
|
|
322
|
+
async function handleSearchErrors(args) {
|
|
323
|
+
if (!supabase || !projectId) {
|
|
324
|
+
return { error: "Not connected to RoSentry" };
|
|
325
|
+
}
|
|
326
|
+
const limit = Math.min(args.limit ?? 20, 100);
|
|
327
|
+
// Search in messages and stack traces
|
|
328
|
+
const { data: messageMatches } = await supabase
|
|
329
|
+
.from("errors")
|
|
330
|
+
.select("*")
|
|
331
|
+
.eq("project_id", projectId)
|
|
332
|
+
.ilike("message", `%${args.query}%`)
|
|
333
|
+
.order("timestamp", { ascending: false })
|
|
334
|
+
.limit(limit);
|
|
335
|
+
const { data: stackMatches } = await supabase
|
|
336
|
+
.from("errors")
|
|
337
|
+
.select("*")
|
|
338
|
+
.eq("project_id", projectId)
|
|
339
|
+
.ilike("stack_trace", `%${args.query}%`)
|
|
340
|
+
.order("timestamp", { ascending: false })
|
|
341
|
+
.limit(limit);
|
|
342
|
+
// Combine and dedupe
|
|
343
|
+
const seen = new Set();
|
|
344
|
+
const results = [];
|
|
345
|
+
for (const error of [...(messageMatches ?? []), ...(stackMatches ?? [])]) {
|
|
346
|
+
if (!seen.has(error.id)) {
|
|
347
|
+
seen.add(error.id);
|
|
348
|
+
results.push(error);
|
|
349
|
+
}
|
|
350
|
+
if (results.length >= limit)
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
return { results, count: results.length, query: args.query };
|
|
354
|
+
}
|
|
355
|
+
async function handleGetAffectedUsers(args) {
|
|
356
|
+
if (!supabase || !projectId) {
|
|
357
|
+
return { error: "Not connected to RoSentry" };
|
|
358
|
+
}
|
|
359
|
+
const limit = Math.min(args.limit ?? 50, 200);
|
|
360
|
+
const { data: errors } = await supabase
|
|
361
|
+
.from("errors")
|
|
362
|
+
.select("user_id, timestamp")
|
|
363
|
+
.eq("error_group_id", args.group_id)
|
|
364
|
+
.not("user_id", "is", null)
|
|
365
|
+
.order("timestamp", { ascending: false });
|
|
366
|
+
if (!errors)
|
|
367
|
+
return { users: [], count: 0 };
|
|
368
|
+
// Dedupe users and count occurrences
|
|
369
|
+
const userStats = {};
|
|
370
|
+
for (const error of errors) {
|
|
371
|
+
if (error.user_id) {
|
|
372
|
+
if (!userStats[error.user_id]) {
|
|
373
|
+
userStats[error.user_id] = { count: 0, last_seen: error.timestamp };
|
|
374
|
+
}
|
|
375
|
+
userStats[error.user_id].count++;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const users = Object.entries(userStats)
|
|
379
|
+
.map(([userId, stats]) => ({
|
|
380
|
+
user_id: parseInt(userId),
|
|
381
|
+
occurrence_count: stats.count,
|
|
382
|
+
last_seen: stats.last_seen,
|
|
383
|
+
}))
|
|
384
|
+
.sort((a, b) => b.occurrence_count - a.occurrence_count)
|
|
385
|
+
.slice(0, limit);
|
|
386
|
+
return { users, total_affected: Object.keys(userStats).length };
|
|
387
|
+
}
|
|
388
|
+
async function handleGetStats(args) {
|
|
389
|
+
if (!supabase || !projectId) {
|
|
390
|
+
return { error: "Not connected to RoSentry" };
|
|
391
|
+
}
|
|
392
|
+
const timeframe = args.timeframe ?? "24h";
|
|
393
|
+
const now = new Date();
|
|
394
|
+
let since;
|
|
395
|
+
switch (timeframe) {
|
|
396
|
+
case "1h":
|
|
397
|
+
since = new Date(now.getTime() - 60 * 60 * 1000);
|
|
398
|
+
break;
|
|
399
|
+
case "7d":
|
|
400
|
+
since = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
401
|
+
break;
|
|
402
|
+
case "30d":
|
|
403
|
+
since = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
404
|
+
break;
|
|
405
|
+
default:
|
|
406
|
+
since = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
407
|
+
}
|
|
408
|
+
// Get total errors
|
|
409
|
+
const { count: totalErrors } = await supabase
|
|
410
|
+
.from("errors")
|
|
411
|
+
.select("*", { count: "exact", head: true })
|
|
412
|
+
.eq("project_id", projectId)
|
|
413
|
+
.gte("timestamp", since.toISOString());
|
|
414
|
+
// Get errors by level
|
|
415
|
+
const { data: levelData } = await supabase
|
|
416
|
+
.from("errors")
|
|
417
|
+
.select("level")
|
|
418
|
+
.eq("project_id", projectId)
|
|
419
|
+
.gte("timestamp", since.toISOString());
|
|
420
|
+
const byLevel = (levelData ?? []).reduce((acc, { level }) => {
|
|
421
|
+
acc[level] = (acc[level] ?? 0) + 1;
|
|
422
|
+
return acc;
|
|
423
|
+
}, {});
|
|
424
|
+
// Get open groups
|
|
425
|
+
const { count: openGroups } = await supabase
|
|
426
|
+
.from("error_groups")
|
|
427
|
+
.select("*", { count: "exact", head: true })
|
|
428
|
+
.eq("project_id", projectId)
|
|
429
|
+
.eq("status", "open");
|
|
430
|
+
// Get unique users
|
|
431
|
+
const { data: userData } = await supabase
|
|
432
|
+
.from("errors")
|
|
433
|
+
.select("user_id")
|
|
434
|
+
.eq("project_id", projectId)
|
|
435
|
+
.gte("timestamp", since.toISOString())
|
|
436
|
+
.not("user_id", "is", null);
|
|
437
|
+
const uniqueUsers = new Set(userData?.map((e) => e.user_id)).size;
|
|
438
|
+
// Get top errors
|
|
439
|
+
const { data: topErrors } = await supabase
|
|
440
|
+
.from("error_groups")
|
|
441
|
+
.select("id, message, count, status")
|
|
442
|
+
.eq("project_id", projectId)
|
|
443
|
+
.eq("status", "open")
|
|
444
|
+
.order("count", { ascending: false })
|
|
445
|
+
.limit(5);
|
|
446
|
+
return {
|
|
447
|
+
timeframe,
|
|
448
|
+
total_errors: totalErrors ?? 0,
|
|
449
|
+
by_level: byLevel,
|
|
450
|
+
open_groups: openGroups ?? 0,
|
|
451
|
+
affected_users: uniqueUsers,
|
|
452
|
+
top_errors: topErrors ?? [],
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
async function handleUpdateErrorStatus(args) {
|
|
456
|
+
if (!supabase || !projectId) {
|
|
457
|
+
return { error: "Not connected to RoSentry" };
|
|
458
|
+
}
|
|
459
|
+
const { error } = await supabase
|
|
460
|
+
.from("error_groups")
|
|
461
|
+
.update({ status: args.status, updated_at: new Date().toISOString() })
|
|
462
|
+
.eq("id", args.group_id)
|
|
463
|
+
.eq("project_id", projectId);
|
|
464
|
+
if (error)
|
|
465
|
+
return { error: error.message };
|
|
466
|
+
return { success: true, group_id: args.group_id, new_status: args.status };
|
|
467
|
+
}
|
|
468
|
+
// Main server setup
|
|
469
|
+
async function main() {
|
|
470
|
+
await initializeClient();
|
|
471
|
+
const server = new Server({
|
|
472
|
+
name: "rosentry-mcp",
|
|
473
|
+
version: "0.1.0",
|
|
474
|
+
}, {
|
|
475
|
+
capabilities: {
|
|
476
|
+
tools: {},
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
// List tools handler
|
|
480
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
481
|
+
tools,
|
|
482
|
+
}));
|
|
483
|
+
// Call tool handler
|
|
484
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
485
|
+
const { name, arguments: args } = request.params;
|
|
486
|
+
try {
|
|
487
|
+
let result;
|
|
488
|
+
switch (name) {
|
|
489
|
+
case "get_recent_errors":
|
|
490
|
+
result = await handleGetRecentErrors(args);
|
|
491
|
+
break;
|
|
492
|
+
case "get_error_groups":
|
|
493
|
+
result = await handleGetErrorGroups(args);
|
|
494
|
+
break;
|
|
495
|
+
case "get_error_group_details":
|
|
496
|
+
result = await handleGetErrorGroupDetails(args);
|
|
497
|
+
break;
|
|
498
|
+
case "get_error_trends":
|
|
499
|
+
result = await handleGetErrorTrends(args);
|
|
500
|
+
break;
|
|
501
|
+
case "search_errors":
|
|
502
|
+
result = await handleSearchErrors(args);
|
|
503
|
+
break;
|
|
504
|
+
case "get_affected_users":
|
|
505
|
+
result = await handleGetAffectedUsers(args);
|
|
506
|
+
break;
|
|
507
|
+
case "get_stats":
|
|
508
|
+
result = await handleGetStats(args);
|
|
509
|
+
break;
|
|
510
|
+
case "update_error_status":
|
|
511
|
+
result = await handleUpdateErrorStatus(args);
|
|
512
|
+
break;
|
|
513
|
+
default:
|
|
514
|
+
return {
|
|
515
|
+
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
516
|
+
isError: true,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
return {
|
|
520
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
catch (error) {
|
|
524
|
+
return {
|
|
525
|
+
content: [{ type: "text", text: `Error: ${error}` }],
|
|
526
|
+
isError: true,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
// Connect via stdio
|
|
531
|
+
const transport = new StdioServerTransport();
|
|
532
|
+
await server.connect(transport);
|
|
533
|
+
console.error("RoSentry MCP server running");
|
|
534
|
+
}
|
|
535
|
+
main().catch(console.error);
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rosentry-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP (Model Context Protocol) server for RoSentry - AI-powered error monitoring for Roblox",
|
|
5
|
+
"author": "RoSentry",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"bin": {
|
|
11
|
+
"rosentry-mcp": "dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"dev": "tsx src/index.ts",
|
|
21
|
+
"start": "node dist/index.js",
|
|
22
|
+
"prepublishOnly": "npm run build"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"mcp",
|
|
26
|
+
"model-context-protocol",
|
|
27
|
+
"rosentry",
|
|
28
|
+
"roblox",
|
|
29
|
+
"error-monitoring",
|
|
30
|
+
"sentry",
|
|
31
|
+
"claude",
|
|
32
|
+
"ai",
|
|
33
|
+
"debugging"
|
|
34
|
+
],
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/rosentry/rosentry.git",
|
|
38
|
+
"directory": "mcp"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://rosentry.dev",
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/rosentry/rosentry/issues"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=18.0.0"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
49
|
+
"@supabase/supabase-js": "^2.50.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^20.0.0",
|
|
53
|
+
"tsx": "^4.7.0",
|
|
54
|
+
"typescript": "^5.0.0"
|
|
55
|
+
}
|
|
56
|
+
}
|