Skip to content

Commit e39acbc

Browse files
committed
Nicer error handling
1 parent 59075e2 commit e39acbc

File tree

6 files changed

+399
-257
lines changed

6 files changed

+399
-257
lines changed

src/mcpadapt/auth/__init__.py

Lines changed: 114 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,128 @@
1-
"""Authentication module for MCPAdapt."""
1+
"""Authentication module for MCPAdapt.
2+
3+
This module provides OAuth, API Key, and Bearer token authentication support
4+
for MCP servers.
5+
6+
Example usage with OAuth:
7+
8+
```python
9+
from mcp.client.auth import OAuthClientProvider
10+
from mcp.shared.auth import OAuthClientMetadata
11+
from pydantic import HttpUrl
12+
13+
from mcpadapt.auth import (
14+
InMemoryTokenStorage,
15+
LocalBrowserOAuthHandler
16+
)
17+
from mcpadapt.core import MCPAdapt
18+
from mcpadapt.smolagents_adapter import SmolAgentsAdapter
19+
20+
# Create OAuth provider directly
21+
client_metadata = OAuthClientMetadata(
22+
client_name="My App",
23+
redirect_uris=[HttpUrl("http://localhost:3030/callback")],
24+
grant_types=["authorization_code", "refresh_token"],
25+
response_types=["code"],
26+
token_endpoint_auth_method="client_secret_post",
27+
)
28+
29+
oauth_handler = LocalBrowserOAuthHandler(callback_port=3030)
30+
token_storage = InMemoryTokenStorage()
31+
32+
oauth_provider = OAuthClientProvider(
33+
server_url="https://example.com",
34+
client_metadata=client_metadata,
35+
storage=token_storage,
36+
redirect_handler=oauth_handler.handle_redirect,
37+
callback_handler=oauth_handler.handle_callback,
38+
)
39+
40+
# Use with MCPAdapt
41+
with MCPAdapt(
42+
serverparams={"url": "https://example.com/mcp", "transport": "streamable-http"},
43+
adapter=SmolAgentsAdapter(),
44+
auth_provider=oauth_provider,
45+
) as tools:
46+
print(f"Connected with {len(tools)} tools")
47+
```
48+
49+
Example usage with API Key:
50+
51+
```python
52+
from mcpadapt.auth import ApiKeyAuthProvider
53+
from mcpadapt.core import MCPAdapt
54+
from mcpadapt.smolagents_adapter import SmolAgentsAdapter
55+
56+
# Create API Key provider
57+
api_key_provider = ApiKeyAuthProvider(
58+
header_name="X-API-Key",
59+
header_value="your-api-key-here"
60+
)
61+
62+
with MCPAdapt(
63+
serverparams={"url": "https://example.com/mcp", "transport": "streamable-http"},
64+
adapter=SmolAgentsAdapter(),
65+
auth_provider=api_key_provider,
66+
) as tools:
67+
print(f"Connected with {len(tools)} tools")
68+
```
69+
70+
For custom implementations, extend BaseOAuthHandler:
71+
72+
```python
73+
from mcpadapt.auth import BaseOAuthHandler
74+
75+
class CustomOAuthHandler(BaseOAuthHandler):
76+
async def handle_redirect(self, authorization_url: str) -> None:
77+
# Custom redirect logic (e.g., print URL for headless environments)
78+
print(f"Please open: {authorization_url}")
79+
80+
async def handle_callback(self) -> tuple[str, str | None]:
81+
# Custom callback logic (e.g., manual code input)
82+
auth_code = input("Enter authorization code: ")
83+
return auth_code, None
84+
```
85+
"""
286

3-
from .handlers import default_callback_handler, default_redirect_handler
487
from .oauth import InMemoryTokenStorage
88+
from .handlers import (
89+
BaseOAuthHandler,
90+
LocalBrowserOAuthHandler,
91+
LocalCallbackServer,
92+
)
593
from .providers import (
694
ApiKeyAuthProvider,
795
BearerAuthProvider,
8-
create_auth_provider,
996
get_auth_headers,
1097
)
11-
from .models import (
12-
ApiKeyConfig,
13-
AuthConfig,
14-
AuthConfigBase,
15-
BearerAuthConfig,
16-
CallbackHandler,
17-
OAuthConfig,
18-
RedirectHandler,
98+
from .exceptions import (
99+
OAuthError,
100+
OAuthTimeoutError,
101+
OAuthCancellationError,
102+
OAuthNetworkError,
103+
OAuthConfigurationError,
104+
OAuthServerError,
105+
OAuthCallbackError,
19106
)
20107

21108
__all__ = [
22-
# Types
23-
"AuthConfig",
24-
"AuthConfigBase",
25-
"OAuthConfig",
26-
"ApiKeyConfig",
27-
"BearerAuthConfig",
28-
"CallbackHandler",
29-
"RedirectHandler",
30-
# OAuth utilities
31-
"InMemoryTokenStorage",
32-
# Handlers
33-
"default_callback_handler",
34-
"default_redirect_handler",
35-
# Providers
109+
# Handler classes
110+
"BaseOAuthHandler",
111+
"LocalBrowserOAuthHandler",
112+
"LocalCallbackServer",
113+
# Provider classes
36114
"ApiKeyAuthProvider",
37115
"BearerAuthProvider",
38-
"create_auth_provider",
116+
# Default implementations
117+
"InMemoryTokenStorage",
118+
# Provider functions
39119
"get_auth_headers",
120+
# Exception classes
121+
"OAuthError",
122+
"OAuthTimeoutError",
123+
"OAuthCancellationError",
124+
"OAuthNetworkError",
125+
"OAuthConfigurationError",
126+
"OAuthServerError",
127+
"OAuthCallbackError",
40128
]

src/mcpadapt/auth/exceptions.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""Custom exceptions for OAuth authentication errors."""
2+
3+
4+
class OAuthError(Exception):
5+
"""Base class for all OAuth authentication errors."""
6+
7+
def __init__(self, message: str, error_code: str | None = None, context: dict | None = None):
8+
"""Initialize OAuth error.
9+
10+
Args:
11+
message: Human-readable error message
12+
error_code: Machine-readable error code (optional)
13+
context: Additional context about the error (optional)
14+
"""
15+
super().__init__(message)
16+
self.error_code = error_code
17+
self.context = context or {}
18+
19+
20+
class OAuthTimeoutError(OAuthError):
21+
"""Raised when OAuth callback doesn't arrive within the specified timeout."""
22+
23+
def __init__(self, timeout_seconds: int, context: dict | None = None):
24+
message = (
25+
f"OAuth authentication timed out after {timeout_seconds} seconds. "
26+
f"The user may have closed the browser window or the OAuth server may be unreachable. "
27+
f"Try refreshing the browser or check your network connection."
28+
)
29+
super().__init__(message, "oauth_timeout", context)
30+
self.timeout_seconds = timeout_seconds
31+
32+
33+
class OAuthCancellationError(OAuthError):
34+
"""Raised when the user cancels or denies the OAuth authorization."""
35+
36+
def __init__(self, error_details: str | None = None, context: dict | None = None):
37+
base_message = "OAuth authorization was cancelled or denied by the user."
38+
if error_details:
39+
message = f"{base_message} Details: {error_details}"
40+
else:
41+
message = base_message
42+
super().__init__(message, "oauth_cancelled", context)
43+
self.error_details = error_details
44+
45+
46+
class OAuthNetworkError(OAuthError):
47+
"""Raised when network-related issues prevent OAuth completion."""
48+
49+
def __init__(self, original_error: Exception, context: dict | None = None):
50+
message = (
51+
f"OAuth authentication failed due to network error: {str(original_error)}. "
52+
f"Check your internet connection and try again."
53+
)
54+
super().__init__(message, "oauth_network_error", context)
55+
self.original_error = original_error
56+
57+
58+
class OAuthConfigurationError(OAuthError):
59+
"""Raised when OAuth configuration is invalid or incomplete."""
60+
61+
def __init__(self, config_issue: str, context: dict | None = None):
62+
message = f"OAuth configuration error: {config_issue}"
63+
super().__init__(message, "oauth_config_error", context)
64+
self.config_issue = config_issue
65+
66+
67+
class OAuthServerError(OAuthError):
68+
"""Raised when the OAuth server returns an error response."""
69+
70+
def __init__(self, server_error: str, error_description: str | None = None, context: dict | None = None):
71+
message = f"OAuth server error: {server_error}"
72+
if error_description:
73+
message += f" - {error_description}"
74+
super().__init__(message, "oauth_server_error", context)
75+
self.server_error = server_error
76+
self.error_description = error_description
77+
78+
79+
class OAuthCallbackError(OAuthError):
80+
"""Raised when there's an issue with the OAuth callback handling."""
81+
82+
def __init__(self, callback_issue: str, context: dict | None = None):
83+
message = f"OAuth callback error: {callback_issue}"
84+
super().__init__(message, "oauth_callback_error", context)
85+
self.callback_issue = callback_issue

0 commit comments

Comments
 (0)