mirror of
https://github.com/dnlbauer/cordra-mcp.git
synced 2026-09-10 21:55:30 +00:00
feat: better error handling.
This commit is contained in:
@@ -32,6 +32,11 @@ class CordraNotFoundError(CordraClientError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CordraAuthenticationError(CordraClientError):
|
||||||
|
"""Exception raised for authentication/authorization failures."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CordraClient:
|
class CordraClient:
|
||||||
"""Client for interacting with Cordra repository using HTTP requests."""
|
"""Client for interacting with Cordra repository using HTTP requests."""
|
||||||
|
|
||||||
@@ -51,6 +56,28 @@ class CordraClient:
|
|||||||
elif config.username or config.password:
|
elif config.username or config.password:
|
||||||
logger.warning("Only username or password provided, not both. Authentication may fail.")
|
logger.warning("Only username or password provided, not both. Authentication may fail.")
|
||||||
|
|
||||||
|
def _handle_http_error(self, response: requests.Response, context: str) -> None:
|
||||||
|
"""Handle HTTP errors and raise appropriate exceptions.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
response: The HTTP response object
|
||||||
|
context: Context description for the error message
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
CordraNotFoundError: For 404 errors
|
||||||
|
CordraAuthenticationError: For 401/403 errors
|
||||||
|
CordraClientError: For other HTTP errors
|
||||||
|
"""
|
||||||
|
status_code = response.status_code
|
||||||
|
if status_code == 404:
|
||||||
|
raise CordraNotFoundError(f"{context}: Resource not found")
|
||||||
|
elif status_code in (401, 403):
|
||||||
|
raise CordraAuthenticationError(f"{context}: Authentication failed (HTTP {status_code})")
|
||||||
|
elif status_code >= 500:
|
||||||
|
raise CordraClientError(f"{context}: Server error (HTTP {status_code})")
|
||||||
|
else:
|
||||||
|
raise CordraClientError(f"{context}: HTTP error {status_code}")
|
||||||
|
|
||||||
async def get_object(self, object_id: str) -> DigitalObject:
|
async def get_object(self, object_id: str) -> DigitalObject:
|
||||||
"""Retrieve a digital object by its ID.
|
"""Retrieve a digital object by its ID.
|
||||||
|
|
||||||
@@ -61,22 +88,20 @@ class CordraClient:
|
|||||||
The full digital object
|
The full digital object
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
|
ValueError: If object_id is empty
|
||||||
CordraNotFoundError: If the object is not found
|
CordraNotFoundError: If the object is not found
|
||||||
|
CordraAuthenticationError: If authentication fails
|
||||||
CordraClientError: For other API errors
|
CordraClientError: For other API errors
|
||||||
"""
|
"""
|
||||||
try:
|
|
||||||
# Build URL: cordra_base_url/objects/prefix/postfix
|
|
||||||
url = f"{self.config.cordra_url}/objects/{object_id}"
|
url = f"{self.config.cordra_url}/objects/{object_id}"
|
||||||
|
|
||||||
# Add full=true parameter to get complete object details
|
|
||||||
params = {"full": "true"}
|
params = {"full": "true"}
|
||||||
|
|
||||||
|
try:
|
||||||
response = self.session.get(url, params=params, timeout=self.config.timeout)
|
response = self.session.get(url, params=params, timeout=self.config.timeout)
|
||||||
|
|
||||||
if response.status_code == 404:
|
if not response.ok:
|
||||||
raise CordraNotFoundError(f"Object not found: {object_id}")
|
self._handle_http_error(response, f"Failed to retrieve object {object_id}")
|
||||||
|
|
||||||
response.raise_for_status()
|
|
||||||
cordra_obj = response.json()
|
cordra_obj = response.json()
|
||||||
|
|
||||||
return DigitalObject(
|
return DigitalObject(
|
||||||
@@ -88,12 +113,8 @@ class CordraClient:
|
|||||||
payloads=cordra_obj.get('payloads'),
|
payloads=cordra_obj.get('payloads'),
|
||||||
)
|
)
|
||||||
|
|
||||||
except CordraNotFoundError:
|
|
||||||
raise
|
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
raise CordraClientError(f"Failed to retrieve object {object_id}: {e}") from e
|
raise CordraClientError(f"Failed to retrieve object {object_id}: {e}") from e
|
||||||
except Exception as e:
|
|
||||||
raise CordraClientError(f"Failed to retrieve object {object_id}: {e}") from e
|
|
||||||
|
|
||||||
async def find(self, query: str) -> list[dict[str, Any]]:
|
async def find(self, query: str) -> list[dict[str, Any]]:
|
||||||
"""Find objects using a Cordra query.
|
"""Find objects using a Cordra query.
|
||||||
@@ -105,15 +126,18 @@ class CordraClient:
|
|||||||
List of objects matching the query as dictionaries
|
List of objects matching the query as dictionaries
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
CordraClientError: If there's an API error
|
ValueError: If query is empty
|
||||||
|
CordraAuthenticationError: If authentication fails
|
||||||
|
CordraClientError: For other API errors
|
||||||
"""
|
"""
|
||||||
try:
|
|
||||||
# Use HTTP GET request to search endpoint
|
|
||||||
url = f"{self.config.cordra_url}/search"
|
url = f"{self.config.cordra_url}/search"
|
||||||
params = {"query": query}
|
params = {"query": query}
|
||||||
|
|
||||||
|
try:
|
||||||
response = self.session.get(url, params=params, timeout=self.config.timeout)
|
response = self.session.get(url, params=params, timeout=self.config.timeout)
|
||||||
response.raise_for_status()
|
|
||||||
|
if not response.ok:
|
||||||
|
self._handle_http_error(response, f"Failed to search with query '{query}'")
|
||||||
|
|
||||||
search_result = response.json()
|
search_result = response.json()
|
||||||
|
|
||||||
@@ -125,5 +149,3 @@ class CordraClient:
|
|||||||
|
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
raise CordraClientError(f"Failed to search with query '{query}': {e}") from e
|
raise CordraClientError(f"Failed to search with query '{query}': {e}") from e
|
||||||
except Exception as e:
|
|
||||||
raise CordraClientError(f"Failed to search with query '{query}': {e}") from e
|
|
||||||
@@ -5,7 +5,12 @@ import json
|
|||||||
|
|
||||||
from mcp.server.fastmcp import FastMCP
|
from mcp.server.fastmcp import FastMCP
|
||||||
|
|
||||||
from .client import CordraClient, CordraClientError, CordraNotFoundError
|
from .client import (
|
||||||
|
CordraClient,
|
||||||
|
CordraClientError,
|
||||||
|
CordraNotFoundError,
|
||||||
|
CordraAuthenticationError,
|
||||||
|
)
|
||||||
from .config import CordraConfig
|
from .config import CordraConfig
|
||||||
|
|
||||||
# Initialize the MCP server
|
# Initialize the MCP server
|
||||||
@@ -36,8 +41,12 @@ async def get_cordra_object(prefix: str, suffix: str) -> str:
|
|||||||
object_dict = digital_object.model_dump()
|
object_dict = digital_object.model_dump()
|
||||||
return json.dumps(object_dict, indent=2)
|
return json.dumps(object_dict, indent=2)
|
||||||
|
|
||||||
except CordraNotFoundError as e:
|
except ValueError as e:
|
||||||
raise RuntimeError(f"Object not found: {object_id}") from e
|
raise RuntimeError(f"Invalid parameters: {e}") from e
|
||||||
|
except CordraNotFoundError:
|
||||||
|
raise RuntimeError(f"Object not found: {object_id}")
|
||||||
|
except CordraAuthenticationError as e:
|
||||||
|
raise RuntimeError(f"Authentication failed: {e}") from e
|
||||||
except CordraClientError as e:
|
except CordraClientError as e:
|
||||||
raise RuntimeError(f"Failed to retrieve object {object_id}: {e}") from e
|
raise RuntimeError(f"Failed to retrieve object {object_id}: {e}") from e
|
||||||
|
|
||||||
@@ -68,7 +77,9 @@ async def list_cordra_schemas() -> str:
|
|||||||
}
|
}
|
||||||
return json.dumps(result, indent=2)
|
return json.dumps(result, indent=2)
|
||||||
|
|
||||||
except Exception as e:
|
except CordraAuthenticationError as e:
|
||||||
|
raise RuntimeError(f"Authentication failed: {e}") from e
|
||||||
|
except CordraClientError as e:
|
||||||
raise RuntimeError(f"Failed to list schemas: {e}") from e
|
raise RuntimeError(f"Failed to list schemas: {e}") from e
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from mcp_cordra.client import (
|
|||||||
CordraClient,
|
CordraClient,
|
||||||
CordraClientError,
|
CordraClientError,
|
||||||
CordraNotFoundError,
|
CordraNotFoundError,
|
||||||
|
CordraAuthenticationError,
|
||||||
DigitalObject,
|
DigitalObject,
|
||||||
)
|
)
|
||||||
from mcp_cordra.config import CordraConfig
|
from mcp_cordra.config import CordraConfig
|
||||||
@@ -141,11 +142,12 @@ class TestCordraClient:
|
|||||||
"""Test object not found exception."""
|
"""Test object not found exception."""
|
||||||
mock_response = mock_get.return_value
|
mock_response = mock_get.return_value
|
||||||
mock_response.status_code = 404
|
mock_response.status_code = 404
|
||||||
|
mock_response.ok = False
|
||||||
|
|
||||||
with pytest.raises(CordraNotFoundError) as exc_info:
|
with pytest.raises(CordraNotFoundError) as exc_info:
|
||||||
await client.get_object("test/nonexistent")
|
await client.get_object("test/nonexistent")
|
||||||
|
|
||||||
assert "Object not found: test/nonexistent" in str(exc_info.value)
|
assert "Resource not found" in str(exc_info.value)
|
||||||
|
|
||||||
@patch('mcp_cordra.client.requests.Session.get')
|
@patch('mcp_cordra.client.requests.Session.get')
|
||||||
async def test_get_object_general_error(self, mock_get, client):
|
async def test_get_object_general_error(self, mock_get, client):
|
||||||
|
|||||||
Reference in New Issue
Block a user