|
| 1 | +from mcp.server.fastmcp import FastMCP |
| 2 | +import os |
| 3 | +import sys |
| 4 | +import requests |
| 5 | +import json |
| 6 | +from datetime import datetime |
| 7 | +import argparse |
| 8 | + |
| 9 | +# Initialize FastMCP server |
| 10 | +mcp = FastMCP("sentry_issue_manager") |
| 11 | + |
| 12 | +# Parse command-line arguments |
| 13 | +parser = argparse.ArgumentParser(description="Sentry Issue Manager MCP Server") |
| 14 | +parser.add_argument('--sentry_auth_token', required=True, help="Sentry authentication token") |
| 15 | +parser.add_argument('--sentry_organization_slug', required=True, help="Sentry organization slug") |
| 16 | +parser.add_argument('--sentry_organization_id', required=True, help="Sentry organization ID") |
| 17 | +args = parser.parse_args() |
| 18 | + |
| 19 | +# Use the parsed arguments |
| 20 | +AUTH_TOKEN = args.sentry_auth_token |
| 21 | +ORGANIZATION_SLUG = args.sentry_organization_slug |
| 22 | +ORGANIZATION_ID = args.sentry_organization_id |
| 23 | + |
| 24 | +# check that the parser arguments are set |
| 25 | +if not AUTH_TOKEN or not ORGANIZATION_SLUG or not ORGANIZATION_ID: |
| 26 | + print("Error: Sentry authentication token, organization slug, and organization ID must be provided.") |
| 27 | + # write to ./logs.txt |
| 28 | + with open("logs.txt", "a") as log_file: |
| 29 | + log_file.write("Error: Sentry authentication token, organization slug, and organization ID must be provided.\n") |
| 30 | + # exit the program |
| 31 | + sys.exit(1) |
| 32 | + |
| 33 | +@mcp.tool() |
| 34 | +async def sentry_list_projects(cursor: str = None) -> str: |
| 35 | + """List all projects available to the authenticated session. |
| 36 | + |
| 37 | + Args: |
| 38 | + cursor: Optional. A pointer to the last object fetched and its sort order; |
| 39 | + used to retrieve the next or previous results. |
| 40 | + |
| 41 | + Returns: |
| 42 | + str: Formatted list of available Sentry projects |
| 43 | + """ |
| 44 | + url = "https://sentry.io/api/0/projects/" |
| 45 | + headers = { |
| 46 | + "Authorization": f"Bearer {AUTH_TOKEN}" |
| 47 | + } |
| 48 | + |
| 49 | + params = {} |
| 50 | + if cursor: |
| 51 | + params["cursor"] = cursor |
| 52 | + |
| 53 | + try: |
| 54 | + response = requests.get(url, headers=headers, params=params) |
| 55 | + response.raise_for_status() # Raise an exception for bad status codes |
| 56 | + |
| 57 | + try: |
| 58 | + projects = response.json() |
| 59 | + except json.JSONDecodeError: |
| 60 | + return "Error: Unable to parse JSON response" |
| 61 | + |
| 62 | + if not projects: |
| 63 | + return "No projects found." |
| 64 | + |
| 65 | + result = "Available Projects:\n" |
| 66 | + for project in projects: |
| 67 | + org = project.get('organization', {}) |
| 68 | + org_slug = org.get('slug', 'Unknown') |
| 69 | + org_name = org.get('name', 'Unknown') |
| 70 | + project_name = project.get('name', 'Unnamed') |
| 71 | + project_slug = project.get('slug', 'unknown') |
| 72 | + project_id = project.get('id', 'unknown') |
| 73 | + platform = project.get('platform', 'Unknown') |
| 74 | + |
| 75 | + result += f"Organization: {org_name} ({org_slug}) | " |
| 76 | + result += f"Project: {project_name} (ID: {project_id}, slug: {project_slug}) | " |
| 77 | + result += f"Platform: {platform}\n" |
| 78 | + |
| 79 | + # Add pagination information if available |
| 80 | + links = response.headers.get('Link') |
| 81 | + if links and 'cursor=' in links: |
| 82 | + result += "\n(Use the cursor parameter for pagination to see more results)" |
| 83 | + |
| 84 | + return result |
| 85 | + except requests.RequestException as e: |
| 86 | + return f"Error retrieving projects: {str(e)}" |
| 87 | + except Exception as e: |
| 88 | + return f"Exception occurred: {str(e)}" |
| 89 | + |
| 90 | + |
| 91 | +@mcp.tool() |
| 92 | +async def sentry_list_project_issues(organization_slug: str, project_slug: str, limit: int = 10) -> str: |
| 93 | + """List recent issues from a specific Sentry project. |
| 94 | + Args: |
| 95 | + organization_slug: Slug of the Sentry organization |
| 96 | + project_slug: Slug of the Sentry project |
| 97 | + limit: Maximum number of issues to return (default: 10) |
| 98 | + Returns: |
| 99 | + str: List of issues with their details for the specified project |
| 100 | + """ |
| 101 | + url = f"https://sentry.io/api/0/projects/{organization_slug}/{project_slug}/issues/" |
| 102 | + headers = { |
| 103 | + "Authorization": f"Bearer {AUTH_TOKEN}" |
| 104 | + } |
| 105 | + params = {"limit": limit} |
| 106 | + |
| 107 | + try: |
| 108 | + response = requests.get(url, headers=headers, params=params) |
| 109 | + response.raise_for_status() # Raises an HTTPError for bad responses |
| 110 | + |
| 111 | + issues = response.json() |
| 112 | + if not issues: |
| 113 | + return f"No issues found for project {project_slug}." |
| 114 | + |
| 115 | + result = f"Recent Issues for {project_slug}:\n\n" |
| 116 | + for issue in issues: |
| 117 | + result += f"ID: {issue['id']}\n" |
| 118 | + result += f"Title: {issue['title'][:100]}...\n" # Truncate long titles |
| 119 | + result += f"Type: {issue['type']}\n" |
| 120 | + result += f"Level: {issue['level']}\n" |
| 121 | + result += f"Status: {issue['status']}\n" |
| 122 | + result += f"First Seen: {datetime.fromisoformat(issue['firstSeen'][:-1]).strftime('%Y-%m-%d %H:%M:%S')}\n" |
| 123 | + result += f"Last Seen: {datetime.fromisoformat(issue['lastSeen'][:-1]).strftime('%Y-%m-%d %H:%M:%S')}\n" |
| 124 | + result += f"Count: {issue['count']}\n" |
| 125 | + result += f"User Count: {issue['userCount']}\n" |
| 126 | + result += f"Culprit: {issue['culprit']}\n" |
| 127 | + result += f"Priority: {issue['priority']}\n" |
| 128 | + result += f"Permalink: {issue['permalink']}\n" |
| 129 | + result += "\n" |
| 130 | + |
| 131 | + return result |
| 132 | + |
| 133 | + except requests.RequestException as e: |
| 134 | + return f"Error retrieving project issues: {str(e)}" |
| 135 | + except Exception as e: |
| 136 | + return f"Exception occurred: {str(e)}" |
| 137 | + |
| 138 | +@mcp.tool() |
| 139 | +async def sentry_get_issue_details(issue_id: str) -> str: |
| 140 | + """Get detailed information about a specific issue. |
| 141 | + Args: |
| 142 | + issue_id: ID of the Sentry issue to retrieve |
| 143 | + Returns: |
| 144 | + str: Issue details in formatted text |
| 145 | + """ |
| 146 | + url = f"https://sentry.io/api/0/issues/{issue_id}/" |
| 147 | + headers = { |
| 148 | + "Authorization": f"Bearer {AUTH_TOKEN}" |
| 149 | + } |
| 150 | + |
| 151 | + try: |
| 152 | + response = requests.get(url, headers=headers) |
| 153 | + if response.status_code == 200: |
| 154 | + issue_data = response.json() |
| 155 | + return (f"Issue Details:\n" |
| 156 | + f"Title: {issue_data['title']}\n" |
| 157 | + f"Status: {issue_data['status']}\n" |
| 158 | + f"Level: {issue_data['level']}\n" |
| 159 | + f"First Seen: {issue_data['firstSeen']}\n" |
| 160 | + f"Last Seen: {issue_data['lastSeen']}\n" |
| 161 | + f"Count: {issue_data['count']}") |
| 162 | + else: |
| 163 | + return f"Error retrieving issue details: {response.status_code}\n{response.text}" |
| 164 | + except Exception as e: |
| 165 | + return f"Exception occurred: {str(e)}" |
| 166 | + |
| 167 | +@mcp.tool() |
| 168 | +async def sentry_list_all_org_issues(limit: int = 10) -> str: |
| 169 | + """List recent issues from your Sentry organization. |
| 170 | + Args: |
| 171 | + limit: Maximum number of issues to return (default: 10) |
| 172 | + Returns: |
| 173 | + str: List of issues with their IDs and titles |
| 174 | + """ |
| 175 | + url = f"https://sentry.io/api/0/organizations/{ORGANIZATION_ID}/issues/" |
| 176 | + headers = { |
| 177 | + "Authorization": f"Bearer {AUTH_TOKEN}" |
| 178 | + } |
| 179 | + params = {"limit": limit} |
| 180 | + |
| 181 | + try: |
| 182 | + response = requests.get(url, headers=headers, params=params) |
| 183 | + if response.status_code == 200: |
| 184 | + issues = response.json() |
| 185 | + if not issues: |
| 186 | + return "No issues found." |
| 187 | + |
| 188 | + result = "Recent Issues:\n" |
| 189 | + for issue in issues: |
| 190 | + result += f"ID: {issue['id']} - {issue['title']}\n" |
| 191 | + return result |
| 192 | + else: |
| 193 | + return f"Error retrieving issues: {response.status_code}\n{response.text}" |
| 194 | + except Exception as e: |
| 195 | + return f"Exception occurred: {str(e)}" |
| 196 | + |
| 197 | +if __name__ == "__main__": |
| 198 | + try: |
| 199 | + mcp.run(transport='stdio') |
| 200 | + except Exception as e: |
| 201 | + print(f"Error starting server: {e}") |
| 202 | + sys.exit(1) |
0 commit comments