Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
error handling + pr cleanup
  • Loading branch information
saramaebee committed Jun 2, 2025
commit ec6ea526c6202adeadf2506fd3d6e901c0960cc6
1 change: 1 addition & 0 deletions CLAUDE.md
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why named it claude.md ?

Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
This project uses `uv` for package management
- Reference https://gofastmcp.com/llms.txt
- Reference https://developer.devrev.ai/llms.txt
37 changes: 37 additions & 0 deletions src/devrev_mcp/endpoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""
DevRev API Endpoints Constants

This module defines all DevRev API endpoint strings used throughout the application.
Centralizing these constants prevents typos and makes API changes easier to manage.
"""


class DevRevEndpoints:
"""DevRev API endpoint constants for consistent usage across the application."""

# Works (Tickets, Issues, etc.)
WORKS_GET = "works.get"
WORKS_CREATE = "works.create"
WORKS_UPDATE = "works.update"

# Timeline Entries
TIMELINE_ENTRIES_LIST = "timeline-entries.list"
TIMELINE_ENTRIES_GET = "timeline-entries.get"

# Artifacts
ARTIFACTS_GET = "artifacts.get"
ARTIFACTS_LOCATE = "artifacts.locate"

# Search
SEARCH_HYBRID = "search.hybrid"


# Convenience exports for simpler imports
WORKS_GET = DevRevEndpoints.WORKS_GET
WORKS_CREATE = DevRevEndpoints.WORKS_CREATE
WORKS_UPDATE = DevRevEndpoints.WORKS_UPDATE
TIMELINE_ENTRIES_LIST = DevRevEndpoints.TIMELINE_ENTRIES_LIST
TIMELINE_ENTRIES_GET = DevRevEndpoints.TIMELINE_ENTRIES_GET
ARTIFACTS_GET = DevRevEndpoints.ARTIFACTS_GET
ARTIFACTS_LOCATE = DevRevEndpoints.ARTIFACTS_LOCATE
SEARCH_HYBRID = DevRevEndpoints.SEARCH_HYBRID
224 changes: 224 additions & 0 deletions src/devrev_mcp/error_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
"""
DevRev MCP Error Handler

Provides standardized error handling for resources and tools.
"""

import json
from typing import Dict, Optional
from functools import wraps
from fastmcp import Context


class DevRevMCPError(Exception):
"""Base exception for DevRev MCP errors."""
def __init__(self, message: str, error_code: str = "UNKNOWN", details: Optional[Dict] = None):
self.message = message
self.error_code = error_code
self.details = details or {}
super().__init__(message)


class ResourceNotFoundError(DevRevMCPError):
"""Raised when a requested resource is not found."""
def __init__(self, resource_type: str, resource_id: str, details: Optional[Dict] = None):
message = f"{resource_type} {resource_id} not found"
super().__init__(message, "RESOURCE_NOT_FOUND", details)
self.resource_type = resource_type
self.resource_id = resource_id


class APIError(DevRevMCPError):
"""Raised when DevRev API returns an error."""
def __init__(self, endpoint: str, status_code: int, response_text: str):
message = f"DevRev API error on {endpoint}: HTTP {status_code}"
details = {"status_code": status_code, "response": response_text}
super().__init__(message, "API_ERROR", details)
self.endpoint = endpoint
self.status_code = status_code


def create_error_response(
error: Exception,
resource_type: str = "resource",
resource_id: str = "",
additional_data: Optional[Dict] = None
) -> str:
"""
Create a standardized JSON error response.

Args:
error: The exception that occurred
resource_type: Type of resource (ticket, artifact, etc.)
resource_id: ID of the resource that failed
additional_data: Additional data to include in error response

Returns:
JSON string containing error information
"""
error_data = {
"error": True,
"error_type": type(error).__name__,
"message": str(error),
"resource_type": resource_type,
"resource_id": resource_id,
"timestamp": None # Could add timestamp if needed
}

# Add specific error details for known error types
if isinstance(error, DevRevMCPError):
error_data["error_code"] = error.error_code
error_data["details"] = error.details

if isinstance(error, APIError):
error_data["api_endpoint"] = error.endpoint
error_data["http_status"] = error.status_code

# Include any additional data
if additional_data:
error_data.update(additional_data)

return json.dumps(error_data, indent=2)


def resource_error_handler(resource_type: str):
"""
Decorator for resource handlers that provides standardized error handling.

Args:
resource_type: The type of resource (e.g., "ticket", "artifact")

Returns:
Decorated function with error handling
"""
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
# Extract resource_id from function arguments
resource_id = args[0] if args else "unknown"
ctx = None

# Find Context in arguments
for arg in args:
if isinstance(arg, Context):
ctx = arg
break

try:
return await func(*args, **kwargs)

except DevRevMCPError as e:
if ctx:
await ctx.error(f"{resource_type} error: {e.message}")
return create_error_response(e, resource_type, resource_id)

except Exception as e:
if ctx:
await ctx.error(f"Unexpected error in {resource_type} {resource_id}: {str(e)}")

# Convert to standardized error
mcp_error = DevRevMCPError(
f"Unexpected error: {str(e)}",
"INTERNAL_ERROR"
)
return create_error_response(mcp_error, resource_type, resource_id)

return wrapper
return decorator


def tool_error_handler(tool_name: str):
"""
Decorator for tool handlers that provides standardized error handling.

Args:
tool_name: The name of the tool

Returns:
Decorated function with error handling
"""
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
ctx = None

# Find Context in arguments or kwargs
for arg in args:
if isinstance(arg, Context):
ctx = arg
break

if not ctx and 'ctx' in kwargs:
ctx = kwargs['ctx']

try:
return await func(*args, **kwargs)

except DevRevMCPError as e:
if ctx:
await ctx.error(f"{tool_name} error: {e.message}")
raise # Re-raise for tools since they can handle exceptions

except Exception as e:
if ctx:
await ctx.error(f"Unexpected error in {tool_name}: {str(e)}")

# Convert to standardized error and re-raise
raise DevRevMCPError(
f"Tool {tool_name} failed: {str(e)}",
"TOOL_ERROR"
) from e

return wrapper
return decorator


def handle_api_response(response, endpoint: str, expected_status: int = 200):
"""
Handle DevRev API response and raise appropriate errors.

Args:
response: The requests Response object
endpoint: API endpoint that was called
expected_status: Expected HTTP status code (default 200)

Raises:
APIError: If the response status is not as expected
"""
if response.status_code != expected_status:
raise APIError(endpoint, response.status_code, response.text)

return response


# Utility function to check and validate resource IDs
def validate_resource_id(resource_id: str, resource_type: str) -> str:
"""
Validate and normalize resource IDs.

Args:
resource_id: The resource ID to validate
resource_type: Type of resource for error messages

Returns:
Normalized resource ID

Raises:
ResourceNotFoundError: If resource ID is invalid
"""
if not resource_id or not isinstance(resource_id, str):
raise ResourceNotFoundError(
resource_type,
str(resource_id),
{"reason": "Invalid or empty resource ID"}
)

resource_id = resource_id.strip()
if not resource_id:
raise ResourceNotFoundError(
resource_type,
resource_id,
{"reason": "Empty resource ID after normalization"}
)

return resource_id
Loading