iflow-mcp_mcp-100_mcp-sentry 0.6.8__py3-none-any.whl
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.
- iflow_mcp_mcp_100_mcp_sentry-0.6.8.dist-info/METADATA +229 -0
- iflow_mcp_mcp_100_mcp_sentry-0.6.8.dist-info/RECORD +8 -0
- iflow_mcp_mcp_100_mcp_sentry-0.6.8.dist-info/WHEEL +5 -0
- iflow_mcp_mcp_100_mcp_sentry-0.6.8.dist-info/entry_points.txt +2 -0
- iflow_mcp_mcp_100_mcp_sentry-0.6.8.dist-info/top_level.txt +1 -0
- mcp_sentry/__init__.py +11 -0
- mcp_sentry/__main__.py +4 -0
- mcp_sentry/server.py +382 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: iflow-mcp_mcp-100_mcp-sentry
|
|
3
|
+
Version: 0.6.8
|
|
4
|
+
Summary: MCP server for retrieving issues from sentry.io
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: mcp>=1.0.0
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# mcp-sentry: A Sentry MCP server
|
|
11
|
+
|
|
12
|
+
[](https://smithery.ai/server/@qianniuspace/mcp-sentry)
|
|
13
|
+
|
|
14
|
+
## Overview
|
|
15
|
+
|
|
16
|
+
A Model Context Protocol server for retrieving and analyzing issues from Sentry.io. This server provides tools to inspect error reports, stacktraces, and other debugging information from your Sentry account.
|
|
17
|
+
|
|
18
|
+
### Tools
|
|
19
|
+
|
|
20
|
+
1. `get_sentry_issue`
|
|
21
|
+
- Retrieve and analyze a Sentry issue by ID or URL
|
|
22
|
+
- Input:
|
|
23
|
+
- `issue_id_or_url` (string): Sentry issue ID or URL to analyze
|
|
24
|
+
- Returns: Issue details including:
|
|
25
|
+
- Title
|
|
26
|
+
- Issue ID
|
|
27
|
+
- Status
|
|
28
|
+
- Level
|
|
29
|
+
- First seen timestamp
|
|
30
|
+
- Last seen timestamp
|
|
31
|
+
- Event count
|
|
32
|
+
- Full stacktrace
|
|
33
|
+
2. `get_list_issues`
|
|
34
|
+
- Retrieve and analyze Sentry issues by project slug
|
|
35
|
+
- Input:
|
|
36
|
+
- `project_slug` (string): Sentry project slug to analyze
|
|
37
|
+
- `organization_slug` (string): Sentry organization slug to analyze
|
|
38
|
+
- Returns: List of issues with details including:
|
|
39
|
+
- Title
|
|
40
|
+
- Issue ID
|
|
41
|
+
- Status
|
|
42
|
+
- Level
|
|
43
|
+
- First seen timestamp
|
|
44
|
+
- Last seen timestamp
|
|
45
|
+
- Event count
|
|
46
|
+
- Basic issue information
|
|
47
|
+
|
|
48
|
+
### Prompts
|
|
49
|
+
|
|
50
|
+
1. `sentry-issue`
|
|
51
|
+
- Retrieve issue details from Sentry
|
|
52
|
+
- Input:
|
|
53
|
+
- `issue_id_or_url` (string): Sentry issue ID or URL
|
|
54
|
+
- Returns: Formatted issue details as conversation context
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
### Installing via Smithery
|
|
59
|
+
|
|
60
|
+
To install mcp-sentry for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@qianniuspace/mcp-sentry):
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npx -y @smithery/cli install @qianniuspace/mcp-sentry --client claude
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Using uv (recommended)
|
|
67
|
+
|
|
68
|
+
When using [`uv`](https://docs.astral.sh/uv/) no specific installation is needed. We will
|
|
69
|
+
use [`uvx`](https://docs.astral.sh/uv/guides/tools/) to directly run *mcp-sentry*.
|
|
70
|
+
|
|
71
|
+
### Using PIP
|
|
72
|
+
|
|
73
|
+
Alternatively you can install `mcp-sentry` via pip:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
pip install mcp-sentry
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
or use uv
|
|
80
|
+
```
|
|
81
|
+
uv pip install -e .
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
After installation, you can run it as a script using:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
python -m mcp_sentry
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Configuration
|
|
91
|
+
|
|
92
|
+
### Usage with Claude Desktop
|
|
93
|
+
|
|
94
|
+
Add this to your `claude_desktop_config.json`:
|
|
95
|
+
|
|
96
|
+
<details>
|
|
97
|
+
<summary>Using uvx</summary>
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
"mcpServers": {
|
|
101
|
+
"sentry": {
|
|
102
|
+
"command": "uvx",
|
|
103
|
+
"args": ["mcp-sentry", "--auth-token", "YOUR_SENTRY_TOKEN","--project-slug" ,"YOUR_PROJECT_SLUG", "--organization-slug","YOUR_ORGANIZATION_SLUG"]
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
</details>
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
<details>
|
|
111
|
+
<summary>Using docker</summary>
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
"mcpServers": {
|
|
115
|
+
"sentry": {
|
|
116
|
+
"command": "docker",
|
|
117
|
+
"args": ["run", "-i", "--rm", "mcp/sentry", "--auth-token", "YOUR_SENTRY_TOKEN","--project-slug" ,"YOUR_PROJECT_SLUG", "--organization-slug","YOUR_ORGANIZATION_SLUG"]
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
</details>
|
|
122
|
+
|
|
123
|
+
<details>
|
|
124
|
+
|
|
125
|
+
<summary>Using pip installation</summary>
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
"mcpServers": {
|
|
129
|
+
"sentry": {
|
|
130
|
+
"command": "python",
|
|
131
|
+
"args": ["-m", "mcp_sentry", "--auth-token", "YOUR_SENTRY_TOKEN","--project-slug" ,"YOUR_PROJECT_SLUG", "--organization-slug","YOUR_ORGANIZATION_SLUG"]
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
</details>
|
|
136
|
+
|
|
137
|
+
### Usage with [Zed](https://github.com/zed-industries/zed)
|
|
138
|
+
|
|
139
|
+
Add to your Zed settings.json:
|
|
140
|
+
|
|
141
|
+
<details>
|
|
142
|
+
<summary>Using uvx</summary>
|
|
143
|
+
|
|
144
|
+
For Example Curson 
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
"context_servers": [
|
|
148
|
+
"mcp-sentry": {
|
|
149
|
+
"command": {
|
|
150
|
+
"path": "uvx",
|
|
151
|
+
"args": ["mcp-sentry", "--auth-token", "YOUR_SENTRY_TOKEN","--project-slug" ,"YOUR_PROJECT_SLUG", "--organization-slug","YOUR_ORGANIZATION_SLUG"]
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
],
|
|
155
|
+
```
|
|
156
|
+
</details>
|
|
157
|
+
|
|
158
|
+
<details>
|
|
159
|
+
<summary>Using pip installation</summary>
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
"context_servers": {
|
|
163
|
+
"mcp-sentry": {
|
|
164
|
+
"command": "python",
|
|
165
|
+
"args": ["-m", "mcp_sentry", "--auth-token", "YOUR_SENTRY_TOKEN","--project-slug" ,"YOUR_PROJECT_SLUG", "--organization-slug","YOUR_ORGANIZATION_SLUG"]
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
```
|
|
169
|
+
</details>
|
|
170
|
+
|
|
171
|
+
<details>
|
|
172
|
+
<summary>Using pip installation with custom path</summary>
|
|
173
|
+
|
|
174
|
+
```json
|
|
175
|
+
"context_servers": {
|
|
176
|
+
"sentry": {
|
|
177
|
+
"command": "python",
|
|
178
|
+
"args": [
|
|
179
|
+
"-m",
|
|
180
|
+
"mcp_sentry",
|
|
181
|
+
"--auth-token",
|
|
182
|
+
"YOUR_SENTRY_TOKEN",
|
|
183
|
+
"--project-slug",
|
|
184
|
+
"YOUR_PROJECT_SLUG",
|
|
185
|
+
"--organization-slug",
|
|
186
|
+
"YOUR_ORGANIZATION_SLUG"
|
|
187
|
+
],
|
|
188
|
+
"env": {
|
|
189
|
+
"PYTHONPATH": "path/to/mcp-sentry/src"
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
</details>
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
## Debugging
|
|
205
|
+
|
|
206
|
+
You can use the MCP inspector to debug the server. For uvx installations:
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
npx @modelcontextprotocol/inspector uvx mcp-sentry --auth-token YOUR_SENTRY_TOKEN --project-slug YOUR_PROJECT_SLUG --organization-slug YOUR_ORGANIZATION_SLUG
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Or if you've installed the package in a specific directory or are developing on it:
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
cd path/to/servers/src/sentry
|
|
216
|
+
npx @modelcontextprotocol/inspector uv run mcp-sentry --auth-token YOUR_SENTRY_TOKEN --project-slug YOUR_PROJECT_SLUG --organization-slug YOUR_ORGANIZATION_SLUG
|
|
217
|
+
```
|
|
218
|
+
or in term
|
|
219
|
+
```
|
|
220
|
+
npx @modelcontextprotocol/inspector uv --directory /Volumes/ExtremeSSD/MCP/mcp-sentry/src run mcp_sentry --auth-token YOUR_SENTRY_TOKEN
|
|
221
|
+
--project-slug YOUR_PROJECT_SLUG --organization-slug YOUR_ORGANIZATION_SLUG
|
|
222
|
+
```
|
|
223
|
+

|
|
224
|
+
|
|
225
|
+
## Fork From
|
|
226
|
+
- [https://github.com/modelcontextprotocol/servers/tree/main/src/sentr](https://github.com/modelcontextprotocol/servers/tree/main/src/sentry)
|
|
227
|
+
## License
|
|
228
|
+
|
|
229
|
+
This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
mcp_sentry/__init__.py,sha256=v2UpkufK6E6__uq_OETVdq-g9lYQqtNyAVL4nFhTt_k,215
|
|
2
|
+
mcp_sentry/__main__.py,sha256=Rm8eUEcid2134EYX38mk5QlFHaxNlbkaYb4teFzywz8,74
|
|
3
|
+
mcp_sentry/server.py,sha256=nM8CHylypUhBQilKgq9pKguMJ_ocSquqxN1G9yA9J5Y,13371
|
|
4
|
+
iflow_mcp_mcp_100_mcp_sentry-0.6.8.dist-info/METADATA,sha256=kz0Diaa-CEHVIuK6zmRH8SGJvBEsNda8Z6sadc3HKsU,5721
|
|
5
|
+
iflow_mcp_mcp_100_mcp_sentry-0.6.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
+
iflow_mcp_mcp_100_mcp_sentry-0.6.8.dist-info/entry_points.txt,sha256=xPJx0xk9fEbYCU93cJRsKyNSV9UEn1JdO2JBWzZCU14,47
|
|
7
|
+
iflow_mcp_mcp_100_mcp_sentry-0.6.8.dist-info/top_level.txt,sha256=ohhxR0Qw654eZ0gW6u9wO7CTPxaLzF1HVmHh_cPXNF4,11
|
|
8
|
+
iflow_mcp_mcp_100_mcp_sentry-0.6.8.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mcp_sentry
|
mcp_sentry/__init__.py
ADDED
mcp_sentry/__main__.py
ADDED
mcp_sentry/server.py
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from urllib.parse import urlparse
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
import httpx
|
|
7
|
+
import mcp.types as types
|
|
8
|
+
from mcp.server import NotificationOptions, Server
|
|
9
|
+
from mcp.server.models import InitializationOptions
|
|
10
|
+
from mcp.shared.exceptions import McpError
|
|
11
|
+
import mcp.server.stdio
|
|
12
|
+
|
|
13
|
+
SENTRY_API_BASE = "https://sentry.io/api/0/"
|
|
14
|
+
MISSING_AUTH_TOKEN_MESSAGE = (
|
|
15
|
+
"""Sentry authentication token not found. Please specify your Sentry auth token."""
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class SentryIssueData:
|
|
21
|
+
title: str
|
|
22
|
+
issue_id: str
|
|
23
|
+
status: str
|
|
24
|
+
level: str
|
|
25
|
+
first_seen: str
|
|
26
|
+
last_seen: str
|
|
27
|
+
count: int
|
|
28
|
+
stacktrace: str
|
|
29
|
+
|
|
30
|
+
def to_text(self) -> str:
|
|
31
|
+
return f"""
|
|
32
|
+
Sentry Issue: {self.title}
|
|
33
|
+
Issue ID: {self.issue_id}
|
|
34
|
+
Status: {self.status}
|
|
35
|
+
Level: {self.level}
|
|
36
|
+
First Seen: {self.first_seen}
|
|
37
|
+
Last Seen: {self.last_seen}
|
|
38
|
+
Event Count: {self.count}
|
|
39
|
+
|
|
40
|
+
{self.stacktrace}
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def to_prompt_result(self) -> types.GetPromptResult:
|
|
44
|
+
return types.GetPromptResult(
|
|
45
|
+
description=f"Sentry Issue: {self.title}",
|
|
46
|
+
messages=[
|
|
47
|
+
types.PromptMessage(
|
|
48
|
+
role="user", content=types.TextContent(type="text", text=self.to_text())
|
|
49
|
+
)
|
|
50
|
+
],
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def to_tool_result(self) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
|
|
54
|
+
return [types.TextContent(type="text", text=self.to_text())]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class SentryError(Exception):
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def extract_issue_id(issue_id_or_url: str) -> str:
|
|
62
|
+
"""
|
|
63
|
+
Extracts the Sentry issue ID from either a full URL or a standalone ID.
|
|
64
|
+
|
|
65
|
+
This function validates the input and returns the numeric issue ID.
|
|
66
|
+
It raises SentryError for invalid inputs, including empty strings,
|
|
67
|
+
non-Sentry URLs, malformed paths, and non-numeric IDs.
|
|
68
|
+
"""
|
|
69
|
+
if not issue_id_or_url:
|
|
70
|
+
raise SentryError("Missing issue_id_or_url argument")
|
|
71
|
+
|
|
72
|
+
if issue_id_or_url.startswith(("http://", "https://")):
|
|
73
|
+
parsed_url = urlparse(issue_id_or_url)
|
|
74
|
+
if not parsed_url.hostname or not parsed_url.hostname.endswith(".sentry.io"):
|
|
75
|
+
raise SentryError("Invalid Sentry URL. Must be a URL ending with .sentry.io")
|
|
76
|
+
|
|
77
|
+
path_parts = parsed_url.path.strip("/").split("/")
|
|
78
|
+
if len(path_parts) < 2 or path_parts[0] != "issues":
|
|
79
|
+
raise SentryError(
|
|
80
|
+
"Invalid Sentry issue URL. Path must contain '/issues/{issue_id}'"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
issue_id = path_parts[-1]
|
|
84
|
+
else:
|
|
85
|
+
issue_id = issue_id_or_url
|
|
86
|
+
|
|
87
|
+
if not issue_id.isdigit():
|
|
88
|
+
raise SentryError("Invalid Sentry issue ID. Must be a numeric value.")
|
|
89
|
+
|
|
90
|
+
return issue_id
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def create_stacktrace(latest_event: dict) -> str:
|
|
94
|
+
"""
|
|
95
|
+
Creates a formatted stacktrace string from the latest Sentry event.
|
|
96
|
+
|
|
97
|
+
This function extracts exception information and stacktrace details from the
|
|
98
|
+
provided event dictionary, formatting them into a human-readable string.
|
|
99
|
+
It handles multiple exceptions and includes file, line number, and function
|
|
100
|
+
information for each frame in the stacktrace.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
latest_event (dict): A dictionary containing the latest Sentry event data.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
str: A formatted string containing the stacktrace information,
|
|
107
|
+
or "No stacktrace found" if no relevant data is present.
|
|
108
|
+
"""
|
|
109
|
+
stacktraces = []
|
|
110
|
+
for entry in latest_event.get("entries", []):
|
|
111
|
+
if entry["type"] != "exception":
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
exception_data = entry["data"]["values"]
|
|
115
|
+
for exception in exception_data:
|
|
116
|
+
exception_type = exception.get("type", "Unknown")
|
|
117
|
+
exception_value = exception.get("value", "")
|
|
118
|
+
stacktrace = exception.get("stacktrace")
|
|
119
|
+
|
|
120
|
+
stacktrace_text = f"Exception: {exception_type}: {exception_value}\n\n"
|
|
121
|
+
if stacktrace:
|
|
122
|
+
stacktrace_text += "Stacktrace:\n"
|
|
123
|
+
for frame in stacktrace.get("frames", []):
|
|
124
|
+
filename = frame.get("filename", "Unknown")
|
|
125
|
+
lineno = frame.get("lineNo", "?")
|
|
126
|
+
function = frame.get("function", "Unknown")
|
|
127
|
+
|
|
128
|
+
stacktrace_text += f"{filename}:{lineno} in {function}\n"
|
|
129
|
+
|
|
130
|
+
if "context" in frame:
|
|
131
|
+
context = frame["context"]
|
|
132
|
+
for ctx_line in context:
|
|
133
|
+
stacktrace_text += f" {ctx_line[1]}\n"
|
|
134
|
+
|
|
135
|
+
stacktrace_text += "\n"
|
|
136
|
+
|
|
137
|
+
stacktraces.append(stacktrace_text)
|
|
138
|
+
|
|
139
|
+
return "\n".join(stacktraces) if stacktraces else "No stacktrace found"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
async def handle_sentry_issue(
|
|
143
|
+
http_client: httpx.AsyncClient, auth_token: str, issue_id_or_url: str
|
|
144
|
+
) -> SentryIssueData:
|
|
145
|
+
try:
|
|
146
|
+
issue_id = extract_issue_id(issue_id_or_url)
|
|
147
|
+
|
|
148
|
+
response = await http_client.get(
|
|
149
|
+
f"issues/{issue_id}/", headers={"Authorization": f"Bearer {auth_token}"}
|
|
150
|
+
)
|
|
151
|
+
if response.status_code == 401:
|
|
152
|
+
raise McpError(
|
|
153
|
+
"Error: Unauthorized. Please check your MCP_SENTRY_AUTH_TOKEN token."
|
|
154
|
+
)
|
|
155
|
+
response.raise_for_status()
|
|
156
|
+
issue_data = response.json()
|
|
157
|
+
|
|
158
|
+
# Get issue hashes
|
|
159
|
+
hashes_response = await http_client.get(
|
|
160
|
+
f"issues/{issue_id}/hashes/",
|
|
161
|
+
headers={"Authorization": f"Bearer {auth_token}"},
|
|
162
|
+
)
|
|
163
|
+
hashes_response.raise_for_status()
|
|
164
|
+
hashes = hashes_response.json()
|
|
165
|
+
|
|
166
|
+
if not hashes:
|
|
167
|
+
raise McpError("No Sentry events found for this issue")
|
|
168
|
+
|
|
169
|
+
latest_event = hashes[0]["latestEvent"]
|
|
170
|
+
stacktrace = create_stacktrace(latest_event)
|
|
171
|
+
|
|
172
|
+
return SentryIssueData(
|
|
173
|
+
title=issue_data["title"],
|
|
174
|
+
issue_id=issue_id,
|
|
175
|
+
status=issue_data["status"],
|
|
176
|
+
level=issue_data["level"],
|
|
177
|
+
first_seen=issue_data["firstSeen"],
|
|
178
|
+
last_seen=issue_data["lastSeen"],
|
|
179
|
+
count=issue_data["count"],
|
|
180
|
+
stacktrace=stacktrace
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
except SentryError as e:
|
|
184
|
+
raise McpError(str(e))
|
|
185
|
+
except httpx.HTTPStatusError as e:
|
|
186
|
+
raise McpError(f"Error fetching Sentry issue: {str(e)}")
|
|
187
|
+
except Exception as e:
|
|
188
|
+
raise McpError(f"An error occurred: {str(e)}")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
async def handle_list_issues(
|
|
192
|
+
http_client: httpx.AsyncClient,
|
|
193
|
+
auth_token: str,
|
|
194
|
+
project_slug: str,
|
|
195
|
+
organization_slug: str
|
|
196
|
+
) -> list[SentryIssueData]:
|
|
197
|
+
try:
|
|
198
|
+
response = await http_client.get(
|
|
199
|
+
f"projects/{organization_slug}/{project_slug}/issues/",
|
|
200
|
+
headers={"Authorization": f"Bearer {auth_token}"}
|
|
201
|
+
)
|
|
202
|
+
if response.status_code == 401:
|
|
203
|
+
raise McpError(
|
|
204
|
+
"Error: Unauthorized. Please check your MCP_SENTRY_AUTH_TOKEN token."
|
|
205
|
+
)
|
|
206
|
+
response.raise_for_status()
|
|
207
|
+
issues_data = response.json()
|
|
208
|
+
|
|
209
|
+
result = []
|
|
210
|
+
for issue in issues_data:
|
|
211
|
+
result.append(SentryIssueData(
|
|
212
|
+
title=issue["title"],
|
|
213
|
+
issue_id=issue["id"],
|
|
214
|
+
status=issue["status"],
|
|
215
|
+
level=issue["level"],
|
|
216
|
+
first_seen=issue["firstSeen"],
|
|
217
|
+
last_seen=issue["lastSeen"],
|
|
218
|
+
count=issue["count"],
|
|
219
|
+
stacktrace="Stacktrace not fetched for list view"
|
|
220
|
+
))
|
|
221
|
+
return result
|
|
222
|
+
|
|
223
|
+
except httpx.HTTPStatusError as e:
|
|
224
|
+
raise McpError(f"Error fetching Sentry issues: {str(e)}")
|
|
225
|
+
except Exception as e:
|
|
226
|
+
raise McpError(f"An error occurred: {str(e)}")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
async def serve(auth_token: str, project_slug: str, organization_slug: str) -> Server:
|
|
230
|
+
server = Server("sentry")
|
|
231
|
+
http_client = httpx.AsyncClient(base_url=SENTRY_API_BASE)
|
|
232
|
+
|
|
233
|
+
@server.list_prompts()
|
|
234
|
+
async def handle_list_prompts() -> list[types.Prompt]:
|
|
235
|
+
return [
|
|
236
|
+
types.Prompt(
|
|
237
|
+
name="sentry-issue",
|
|
238
|
+
description="Retrieve a Sentry issue by ID or URL",
|
|
239
|
+
arguments=[
|
|
240
|
+
types.PromptArgument(
|
|
241
|
+
name="issue_id_or_url",
|
|
242
|
+
description="Sentry issue ID or URL",
|
|
243
|
+
required=True,
|
|
244
|
+
)
|
|
245
|
+
],
|
|
246
|
+
),
|
|
247
|
+
types.Prompt(
|
|
248
|
+
name="sentry-issues-by-project",
|
|
249
|
+
description="Retrieve Sentry issues by project slug",
|
|
250
|
+
arguments=[
|
|
251
|
+
types.PromptArgument(
|
|
252
|
+
name="project_slug",
|
|
253
|
+
description="Sentry project slug",
|
|
254
|
+
required=True,
|
|
255
|
+
)
|
|
256
|
+
],
|
|
257
|
+
),
|
|
258
|
+
]
|
|
259
|
+
|
|
260
|
+
@server.get_prompt()
|
|
261
|
+
async def handle_get_prompt(
|
|
262
|
+
name: str, arguments: dict[str, str] | None
|
|
263
|
+
) -> types.GetPromptResult:
|
|
264
|
+
if name != "sentry-issue":
|
|
265
|
+
raise ValueError(f"Unknown prompt: {name}")
|
|
266
|
+
|
|
267
|
+
issue_id_or_url = (arguments or {}).get("issue_id_or_url", "")
|
|
268
|
+
issue_data = await handle_sentry_issue(http_client, auth_token, issue_id_or_url)
|
|
269
|
+
return issue_data.to_prompt_result()
|
|
270
|
+
|
|
271
|
+
@server.list_tools()
|
|
272
|
+
async def handle_list_tools() -> list[types.Tool]:
|
|
273
|
+
return [
|
|
274
|
+
types.Tool(
|
|
275
|
+
name="get_sentry_issue",
|
|
276
|
+
description="""Retrieve and analyze a Sentry issue by ID or URL. Use this tool when you need to:
|
|
277
|
+
- Investigate production errors and crashes
|
|
278
|
+
- Access detailed stacktraces from Sentry
|
|
279
|
+
- Analyze error patterns and frequencies
|
|
280
|
+
- Get information about when issues first/last occurred
|
|
281
|
+
- Review error counts and status""",
|
|
282
|
+
inputSchema={
|
|
283
|
+
"type": "object",
|
|
284
|
+
"properties": {
|
|
285
|
+
"issue_id_or_url": {
|
|
286
|
+
"type": "string",
|
|
287
|
+
"description": "Sentry issue ID or URL to analyze"
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
"required": ["issue_id_or_url"]
|
|
291
|
+
}
|
|
292
|
+
),
|
|
293
|
+
types.Tool(
|
|
294
|
+
name="get_list_issues",
|
|
295
|
+
description="""Retrieve and analyze Sentry issues by project slug. Use this tool when you need to:
|
|
296
|
+
- Investigate production errors and crashes
|
|
297
|
+
- Access detailed stacktraces from Sentry
|
|
298
|
+
- Analyze error patterns and frequencies
|
|
299
|
+
- Get information about when issues first/last occurred
|
|
300
|
+
- Review error counts and status""",
|
|
301
|
+
inputSchema={
|
|
302
|
+
"type": "object",
|
|
303
|
+
"properties": {
|
|
304
|
+
"project_slug": {
|
|
305
|
+
"type": "string",
|
|
306
|
+
"description": "Sentry project slug to analyze"
|
|
307
|
+
},
|
|
308
|
+
"organization_slug": {
|
|
309
|
+
"type": "string",
|
|
310
|
+
"description": "Sentry organization slug to analyze"
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
"required": []
|
|
314
|
+
}
|
|
315
|
+
)
|
|
316
|
+
]
|
|
317
|
+
|
|
318
|
+
@server.call_tool()
|
|
319
|
+
async def handle_call_tool(
|
|
320
|
+
name: str, arguments: dict | None
|
|
321
|
+
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
|
|
322
|
+
if name == "get_sentry_issue":
|
|
323
|
+
if not arguments or "issue_id_or_url" not in arguments:
|
|
324
|
+
raise ValueError("Missing issue_id_or_url argument")
|
|
325
|
+
issue_data = await handle_sentry_issue(http_client, auth_token, arguments["issue_id_or_url"])
|
|
326
|
+
return issue_data.to_tool_result()
|
|
327
|
+
|
|
328
|
+
elif name == "get_list_issues":
|
|
329
|
+
issues = await handle_list_issues(
|
|
330
|
+
http_client,
|
|
331
|
+
auth_token,
|
|
332
|
+
project_slug,
|
|
333
|
+
organization_slug
|
|
334
|
+
)
|
|
335
|
+
# 将所有issue的信息合并成一个文本返回
|
|
336
|
+
combined_text = "Sentry Issues:\n\n" + "\n---\n".join(
|
|
337
|
+
issue.to_text() for issue in issues
|
|
338
|
+
)
|
|
339
|
+
return [types.TextContent(type="text", text=combined_text)]
|
|
340
|
+
|
|
341
|
+
else:
|
|
342
|
+
raise ValueError(f"Unknown tool: {name}")
|
|
343
|
+
|
|
344
|
+
return server
|
|
345
|
+
|
|
346
|
+
@click.command()
|
|
347
|
+
@click.option(
|
|
348
|
+
"--auth-token",
|
|
349
|
+
envvar="SENTRY_TOKEN",
|
|
350
|
+
required=True,
|
|
351
|
+
help="Sentry authentication token",
|
|
352
|
+
)
|
|
353
|
+
@click.option(
|
|
354
|
+
"--project-slug",
|
|
355
|
+
envvar="SENTRY_PROJECT_SLUG",
|
|
356
|
+
required=True,
|
|
357
|
+
help="Sentry project slug",
|
|
358
|
+
)
|
|
359
|
+
@click.option(
|
|
360
|
+
"--organization-slug",
|
|
361
|
+
envvar="SENTRY_ORGANIZATION_SLUG",
|
|
362
|
+
required=True,
|
|
363
|
+
help="Sentry organization slug",
|
|
364
|
+
)
|
|
365
|
+
def main(auth_token: str, project_slug: str, organization_slug: str):
|
|
366
|
+
async def _run():
|
|
367
|
+
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
|
|
368
|
+
server = await serve(auth_token, project_slug, organization_slug)
|
|
369
|
+
await server.run(
|
|
370
|
+
read_stream,
|
|
371
|
+
write_stream,
|
|
372
|
+
InitializationOptions(
|
|
373
|
+
server_name="sentry",
|
|
374
|
+
server_version="0.4.1",
|
|
375
|
+
capabilities=server.get_capabilities(
|
|
376
|
+
notification_options=NotificationOptions(),
|
|
377
|
+
experimental_capabilities={},
|
|
378
|
+
),
|
|
379
|
+
),
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
asyncio.run(_run())
|