amazon-ads-mcp 0.2.7__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.
- amazon_ads_mcp/__init__.py +11 -0
- amazon_ads_mcp/auth/__init__.py +33 -0
- amazon_ads_mcp/auth/base.py +211 -0
- amazon_ads_mcp/auth/hooks.py +172 -0
- amazon_ads_mcp/auth/manager.py +791 -0
- amazon_ads_mcp/auth/oauth_state_store.py +277 -0
- amazon_ads_mcp/auth/providers/__init__.py +14 -0
- amazon_ads_mcp/auth/providers/direct.py +393 -0
- amazon_ads_mcp/auth/providers/example_auth0.py.example +216 -0
- amazon_ads_mcp/auth/providers/openbridge.py +512 -0
- amazon_ads_mcp/auth/registry.py +146 -0
- amazon_ads_mcp/auth/secure_token_store.py +297 -0
- amazon_ads_mcp/auth/token_store.py +723 -0
- amazon_ads_mcp/config/__init__.py +5 -0
- amazon_ads_mcp/config/sampling.py +111 -0
- amazon_ads_mcp/config/settings.py +366 -0
- amazon_ads_mcp/exceptions.py +314 -0
- amazon_ads_mcp/middleware/__init__.py +11 -0
- amazon_ads_mcp/middleware/authentication.py +1474 -0
- amazon_ads_mcp/middleware/caching.py +177 -0
- amazon_ads_mcp/middleware/oauth.py +175 -0
- amazon_ads_mcp/middleware/sampling.py +112 -0
- amazon_ads_mcp/models/__init__.py +320 -0
- amazon_ads_mcp/models/amc_models.py +837 -0
- amazon_ads_mcp/models/api_responses.py +847 -0
- amazon_ads_mcp/models/base_models.py +215 -0
- amazon_ads_mcp/models/builtin_responses.py +496 -0
- amazon_ads_mcp/models/dsp_models.py +556 -0
- amazon_ads_mcp/models/stores_brands.py +610 -0
- amazon_ads_mcp/server/__init__.py +6 -0
- amazon_ads_mcp/server/__main__.py +6 -0
- amazon_ads_mcp/server/builtin_prompts.py +269 -0
- amazon_ads_mcp/server/builtin_tools.py +962 -0
- amazon_ads_mcp/server/file_routes.py +547 -0
- amazon_ads_mcp/server/html_templates.py +149 -0
- amazon_ads_mcp/server/mcp_server.py +327 -0
- amazon_ads_mcp/server/openapi_utils.py +158 -0
- amazon_ads_mcp/server/sampling_handler.py +251 -0
- amazon_ads_mcp/server/server_builder.py +751 -0
- amazon_ads_mcp/server/sidecar_loader.py +178 -0
- amazon_ads_mcp/server/transform_executor.py +827 -0
- amazon_ads_mcp/tools/__init__.py +22 -0
- amazon_ads_mcp/tools/cache_management.py +105 -0
- amazon_ads_mcp/tools/download_tools.py +267 -0
- amazon_ads_mcp/tools/identity.py +236 -0
- amazon_ads_mcp/tools/oauth.py +598 -0
- amazon_ads_mcp/tools/profile.py +150 -0
- amazon_ads_mcp/tools/profile_listing.py +285 -0
- amazon_ads_mcp/tools/region.py +320 -0
- amazon_ads_mcp/tools/region_identity.py +175 -0
- amazon_ads_mcp/utils/__init__.py +6 -0
- amazon_ads_mcp/utils/async_compat.py +215 -0
- amazon_ads_mcp/utils/errors.py +452 -0
- amazon_ads_mcp/utils/export_content_type_resolver.py +249 -0
- amazon_ads_mcp/utils/export_download_handler.py +579 -0
- amazon_ads_mcp/utils/header_resolver.py +81 -0
- amazon_ads_mcp/utils/http/__init__.py +56 -0
- amazon_ads_mcp/utils/http/circuit_breaker.py +127 -0
- amazon_ads_mcp/utils/http/client_manager.py +329 -0
- amazon_ads_mcp/utils/http/request.py +207 -0
- amazon_ads_mcp/utils/http/resilience.py +512 -0
- amazon_ads_mcp/utils/http/resilient_client.py +195 -0
- amazon_ads_mcp/utils/http/retry.py +76 -0
- amazon_ads_mcp/utils/http_client.py +873 -0
- amazon_ads_mcp/utils/media/__init__.py +21 -0
- amazon_ads_mcp/utils/media/negotiator.py +243 -0
- amazon_ads_mcp/utils/media/types.py +199 -0
- amazon_ads_mcp/utils/openapi/__init__.py +16 -0
- amazon_ads_mcp/utils/openapi/json.py +55 -0
- amazon_ads_mcp/utils/openapi/loader.py +263 -0
- amazon_ads_mcp/utils/openapi/refs.py +46 -0
- amazon_ads_mcp/utils/region_config.py +200 -0
- amazon_ads_mcp/utils/response_wrapper.py +171 -0
- amazon_ads_mcp/utils/sampling_helpers.py +156 -0
- amazon_ads_mcp/utils/sampling_wrapper.py +173 -0
- amazon_ads_mcp/utils/security.py +630 -0
- amazon_ads_mcp/utils/tool_naming.py +137 -0
- amazon_ads_mcp-0.2.7.dist-info/METADATA +664 -0
- amazon_ads_mcp-0.2.7.dist-info/RECORD +82 -0
- amazon_ads_mcp-0.2.7.dist-info/WHEEL +4 -0
- amazon_ads_mcp-0.2.7.dist-info/entry_points.txt +3 -0
- amazon_ads_mcp-0.2.7.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Amazon Ads API MCP Server package.
|
|
2
|
+
|
|
3
|
+
This package provides a Model Context Protocol (MCP) server for interacting
|
|
4
|
+
with the Amazon Advertising API. It includes authentication management,
|
|
5
|
+
identity handling, and dynamic OpenAPI integration for seamless API access.
|
|
6
|
+
|
|
7
|
+
:var __version__: Current package version
|
|
8
|
+
:type __version__: str
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
__version__ = "0.2.7"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Authentication module for Amazon Ads MCP.
|
|
2
|
+
|
|
3
|
+
This module provides authentication management for the Amazon Ads MCP server,
|
|
4
|
+
including identity management, token handling, and authentication providers.
|
|
5
|
+
Uses a pluggable provider architecture supporting multiple authentication methods.
|
|
6
|
+
|
|
7
|
+
:var __all__: List of public exports from this module
|
|
8
|
+
:type __all__: List[str]
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# Import providers to trigger registration
|
|
12
|
+
from . import ( # noqa: F401 # imported for side effects (provider registration)
|
|
13
|
+
providers,
|
|
14
|
+
)
|
|
15
|
+
from .base import (
|
|
16
|
+
BaseAmazonAdsProvider,
|
|
17
|
+
BaseAuthProvider,
|
|
18
|
+
BaseIdentityProvider,
|
|
19
|
+
ProviderConfig,
|
|
20
|
+
)
|
|
21
|
+
from .manager import AuthManager, get_auth_manager
|
|
22
|
+
from .registry import ProviderRegistry, register_provider
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"AuthManager",
|
|
26
|
+
"get_auth_manager",
|
|
27
|
+
"ProviderRegistry",
|
|
28
|
+
"register_provider",
|
|
29
|
+
"BaseAuthProvider",
|
|
30
|
+
"BaseIdentityProvider",
|
|
31
|
+
"BaseAmazonAdsProvider",
|
|
32
|
+
"ProviderConfig",
|
|
33
|
+
]
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Define base authentication provider interfaces.
|
|
2
|
+
|
|
3
|
+
Provide abstract contracts for authentication providers and Amazon Ads
|
|
4
|
+
specializations, enabling pluggable auth mechanisms (e.g., direct OAuth,
|
|
5
|
+
OpenBridge) with consistent capabilities and region handling.
|
|
6
|
+
|
|
7
|
+
Examples
|
|
8
|
+
--------
|
|
9
|
+
See the BaseAmazonAdsProvider class for a complete example of how to
|
|
10
|
+
implement a custom authentication provider.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from abc import ABC, abstractmethod
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
from ..models import AuthCredentials, Identity, Token
|
|
17
|
+
from ..utils.region_config import RegionConfig
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BaseAuthProvider(ABC):
|
|
21
|
+
"""Provide the core authentication interface.
|
|
22
|
+
|
|
23
|
+
Define the minimal contract for all authentication providers regardless
|
|
24
|
+
of mechanism (OAuth2, API key, etc.).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def provider_type(self) -> str:
|
|
30
|
+
"""Return the provider type identifier.
|
|
31
|
+
|
|
32
|
+
:return: Provider type (e.g., "openbridge", "direct").
|
|
33
|
+
"""
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
async def initialize(self) -> None:
|
|
38
|
+
"""Initialize the provider.
|
|
39
|
+
|
|
40
|
+
Perform required setup (e.g., configuration validation).
|
|
41
|
+
"""
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
async def get_token(self) -> Token:
|
|
46
|
+
"""Return a valid authentication token.
|
|
47
|
+
|
|
48
|
+
Refresh the token if necessary.
|
|
49
|
+
|
|
50
|
+
:return: Valid authentication token.
|
|
51
|
+
"""
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
async def validate_token(self, token: Token) -> bool:
|
|
56
|
+
"""Return whether the token is still valid.
|
|
57
|
+
|
|
58
|
+
:param token: Token to validate.
|
|
59
|
+
:return: True if token is valid, False otherwise.
|
|
60
|
+
"""
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
@abstractmethod
|
|
64
|
+
async def close(self) -> None:
|
|
65
|
+
"""Clean up provider resources."""
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class BaseIdentityProvider(ABC):
|
|
70
|
+
"""Provide multi-identity capabilities for providers.
|
|
71
|
+
|
|
72
|
+
Implementers can list identities, resolve an identity, and obtain
|
|
73
|
+
identity-scoped credentials for downstream API calls.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
@abstractmethod
|
|
77
|
+
async def list_identities(self, **kwargs) -> List[Identity]:
|
|
78
|
+
"""List available identities.
|
|
79
|
+
|
|
80
|
+
:param kwargs: Optional provider-specific filters.
|
|
81
|
+
:return: List of identities.
|
|
82
|
+
"""
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
@abstractmethod
|
|
86
|
+
async def get_identity(self, identity_id: str) -> Optional[Identity]:
|
|
87
|
+
"""Return a specific identity by identifier.
|
|
88
|
+
|
|
89
|
+
:param identity_id: Identity identifier.
|
|
90
|
+
:return: Identity if found, otherwise None.
|
|
91
|
+
"""
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
@abstractmethod
|
|
95
|
+
async def get_identity_credentials(self, identity_id: str) -> AuthCredentials:
|
|
96
|
+
"""Return credentials for a specific identity.
|
|
97
|
+
|
|
98
|
+
:param identity_id: Identity identifier.
|
|
99
|
+
:return: Authentication credentials for the identity.
|
|
100
|
+
"""
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class BaseAmazonAdsProvider(BaseAuthProvider):
|
|
105
|
+
"""Provide Amazon Ads–specific provider functionality.
|
|
106
|
+
|
|
107
|
+
Add region handling, header generation, and endpoint resolution for
|
|
108
|
+
Amazon Advertising API integrations.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
@abstractmethod
|
|
113
|
+
def region(self) -> str:
|
|
114
|
+
"""Return the current region code ("na", "eu", or "fe")."""
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
@abstractmethod
|
|
118
|
+
async def get_headers(self) -> Dict[str, str]:
|
|
119
|
+
"""Return authentication headers for Amazon Ads API requests.
|
|
120
|
+
|
|
121
|
+
The returned mapping can include Authorization, ClientId, and
|
|
122
|
+
optional scope headers as required by the downstream API.
|
|
123
|
+
|
|
124
|
+
:return: Header mapping for requests.
|
|
125
|
+
"""
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
def get_region_endpoint(self, region: str = None) -> str:
|
|
129
|
+
"""Return the API endpoint for the given region.
|
|
130
|
+
|
|
131
|
+
:param region: Region code; defaults to the provider's region.
|
|
132
|
+
:return: API endpoint URL.
|
|
133
|
+
"""
|
|
134
|
+
region = region or self.region
|
|
135
|
+
return RegionConfig.get_api_endpoint(region)
|
|
136
|
+
|
|
137
|
+
def requires_identity_region_routing(self) -> bool:
|
|
138
|
+
"""Return whether requests must target the identity's region.
|
|
139
|
+
|
|
140
|
+
For providers that manage identities across regions, this indicates
|
|
141
|
+
whether API requests should always be routed to the region associated
|
|
142
|
+
with the active identity.
|
|
143
|
+
|
|
144
|
+
:return: True if identity-based region routing is required.
|
|
145
|
+
"""
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
def headers_are_identity_specific(self) -> bool:
|
|
149
|
+
"""Return whether authentication headers vary per identity.
|
|
150
|
+
|
|
151
|
+
For providers where different identities require different headers
|
|
152
|
+
(e.g., different client IDs or tokens), this indicates headers
|
|
153
|
+
cannot be reconstructed from a cached token alone.
|
|
154
|
+
|
|
155
|
+
:return: True if headers are identity specific.
|
|
156
|
+
"""
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
def region_controlled_by_identity(self) -> bool:
|
|
160
|
+
"""Return whether the region is determined by the active identity.
|
|
161
|
+
|
|
162
|
+
For providers that bind region to identity, region changes require
|
|
163
|
+
selecting an identity in the target region.
|
|
164
|
+
|
|
165
|
+
:return: True if region is controlled by identity.
|
|
166
|
+
"""
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
def get_oauth_endpoint(self, region: str = None) -> str:
|
|
170
|
+
"""Return the OAuth endpoint for the given region.
|
|
171
|
+
|
|
172
|
+
:param region: Region code; defaults to the provider's region.
|
|
173
|
+
:return: OAuth endpoint URL.
|
|
174
|
+
"""
|
|
175
|
+
region = region or self.region
|
|
176
|
+
return RegionConfig.get_oauth_endpoint(region)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class ProviderConfig:
|
|
180
|
+
"""Hold provider configuration values.
|
|
181
|
+
|
|
182
|
+
Store arbitrary configuration for providers with both mapping-style and
|
|
183
|
+
attribute-style access for convenience.
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
def __init__(self, **kwargs):
|
|
187
|
+
"""Initialize configuration from keyword arguments.
|
|
188
|
+
|
|
189
|
+
:param kwargs: Provider-specific configuration parameters.
|
|
190
|
+
"""
|
|
191
|
+
self._config = kwargs
|
|
192
|
+
|
|
193
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
194
|
+
"""Return configuration value by key.
|
|
195
|
+
|
|
196
|
+
:param key: Configuration key to retrieve.
|
|
197
|
+
:param default: Default value if key not present.
|
|
198
|
+
:return: The configuration value or the default.
|
|
199
|
+
"""
|
|
200
|
+
return self._config.get(key, default)
|
|
201
|
+
|
|
202
|
+
def __getattr__(self, name: str) -> Any:
|
|
203
|
+
"""Provide attribute-style access to configuration values.
|
|
204
|
+
|
|
205
|
+
:param name: Attribute name to access.
|
|
206
|
+
:return: The configuration value.
|
|
207
|
+
:raises AttributeError: If the attribute is not defined.
|
|
208
|
+
"""
|
|
209
|
+
if name in self._config:
|
|
210
|
+
return self._config[name]
|
|
211
|
+
raise AttributeError(f"Config has no attribute '{name}'")
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""FastMCP 2.11 authentication hooks for request authentication.
|
|
2
|
+
|
|
3
|
+
This module provides request hooks that integrate with FastMCP 2.11's
|
|
4
|
+
hook system to automatically add authentication headers to outgoing
|
|
5
|
+
HTTP requests, replacing the need for custom HTTP clients.
|
|
6
|
+
|
|
7
|
+
The module provides:
|
|
8
|
+
- AuthHeaderHook: Automatic authentication header injection
|
|
9
|
+
- Header pollution cleanup for MCP client compatibility
|
|
10
|
+
- Special endpoint handling (e.g., profiles endpoint)
|
|
11
|
+
- Response processing for auth-related errors
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
import httpx
|
|
18
|
+
from fastmcp import Context
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AuthHeaderHook:
|
|
24
|
+
"""Request hook that adds authentication headers to outgoing requests.
|
|
25
|
+
|
|
26
|
+
This hook integrates with FastMCP 2.11's request lifecycle to automatically
|
|
27
|
+
inject authentication headers into all outgoing HTTP requests. It replaces
|
|
28
|
+
the need for a custom AuthenticatedClient by working with FastMCP's native
|
|
29
|
+
HTTP client.
|
|
30
|
+
|
|
31
|
+
The hook provides:
|
|
32
|
+
- Automatic authentication header injection
|
|
33
|
+
- Header pollution cleanup for MCP client compatibility
|
|
34
|
+
- Special endpoint handling (e.g., profiles endpoint)
|
|
35
|
+
- Response processing for auth-related errors
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
>>> auth_hook = AuthHeaderHook(auth_manager)
|
|
39
|
+
>>> base_mcp.add_request_hook(auth_hook)
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, auth_manager):
|
|
43
|
+
"""Initialize the authentication header hook.
|
|
44
|
+
|
|
45
|
+
Sets up the hook with the authentication manager that will
|
|
46
|
+
provide the necessary headers for requests.
|
|
47
|
+
|
|
48
|
+
:param auth_manager: The authentication manager instance that provides headers
|
|
49
|
+
:type auth_manager: AuthManager
|
|
50
|
+
:return: None
|
|
51
|
+
:rtype: None
|
|
52
|
+
"""
|
|
53
|
+
self.auth_manager = auth_manager
|
|
54
|
+
|
|
55
|
+
async def before_request(
|
|
56
|
+
self, request: httpx.Request, ctx: Optional[Context] = None
|
|
57
|
+
) -> httpx.Request:
|
|
58
|
+
"""Add authentication headers before the request is sent.
|
|
59
|
+
|
|
60
|
+
This method is called by FastMCP before each HTTP request is sent.
|
|
61
|
+
It retrieves the current authentication headers from the auth manager
|
|
62
|
+
and adds them to the request, while also cleaning up any polluted
|
|
63
|
+
headers that might interfere with the Amazon Ads API.
|
|
64
|
+
|
|
65
|
+
:param request: The HTTP request to modify
|
|
66
|
+
:type request: httpx.Request
|
|
67
|
+
:param ctx: Optional FastMCP context for the current operation
|
|
68
|
+
:type ctx: Optional[Context]
|
|
69
|
+
:return: The modified request with authentication headers added
|
|
70
|
+
:rtype: httpx.Request
|
|
71
|
+
"""
|
|
72
|
+
if not self.auth_manager:
|
|
73
|
+
return request
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
# Get auth headers from manager
|
|
77
|
+
auth_headers = await self.auth_manager.get_headers()
|
|
78
|
+
logger.debug(f"Got auth headers: {list(auth_headers.keys())}")
|
|
79
|
+
except Exception as e:
|
|
80
|
+
logger.error(f"Failed to get auth headers: {e}")
|
|
81
|
+
return request
|
|
82
|
+
|
|
83
|
+
# Clean polluted MCP client headers
|
|
84
|
+
# FastMCP sometimes forwards headers from the MCP client that
|
|
85
|
+
# shouldn't go to the Amazon Ads API
|
|
86
|
+
polluted_headers = []
|
|
87
|
+
for key in list(request.headers.keys()):
|
|
88
|
+
key_lower = key.lower()
|
|
89
|
+
if any(
|
|
90
|
+
pattern in key_lower
|
|
91
|
+
for pattern in [
|
|
92
|
+
"authorization",
|
|
93
|
+
"clientid",
|
|
94
|
+
"client-id",
|
|
95
|
+
"client_id",
|
|
96
|
+
"amazon-ads",
|
|
97
|
+
"amazon-advertising",
|
|
98
|
+
"scope",
|
|
99
|
+
]
|
|
100
|
+
):
|
|
101
|
+
polluted_headers.append(key)
|
|
102
|
+
del request.headers[key]
|
|
103
|
+
|
|
104
|
+
if polluted_headers:
|
|
105
|
+
logger.debug(
|
|
106
|
+
f"Removed {len(polluted_headers)} polluted headers: {polluted_headers}"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Check for special endpoint handling
|
|
110
|
+
url = str(request.url)
|
|
111
|
+
|
|
112
|
+
# Handle profiles endpoint special case
|
|
113
|
+
# The /v2/profiles endpoint when listing (no profileId in URL)
|
|
114
|
+
# doesn't accept the Scope header
|
|
115
|
+
if "/v2/profiles" in url and "profileId" not in url:
|
|
116
|
+
auth_headers = auth_headers.copy()
|
|
117
|
+
auth_headers.pop("Amazon-Advertising-API-Scope", None)
|
|
118
|
+
logger.debug("Removed Scope header for profiles listing endpoint")
|
|
119
|
+
|
|
120
|
+
# Add auth headers to request
|
|
121
|
+
request.headers.update(auth_headers)
|
|
122
|
+
|
|
123
|
+
# Log final headers at DEBUG level only for production safety
|
|
124
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
125
|
+
logger.debug("Final headers being sent to Amazon:")
|
|
126
|
+
for key, value in request.headers.items():
|
|
127
|
+
if "authorization" in key.lower():
|
|
128
|
+
logger.debug(f" {key}: {value[:50]}...")
|
|
129
|
+
else:
|
|
130
|
+
logger.debug(f" {key}: {value}")
|
|
131
|
+
|
|
132
|
+
return request
|
|
133
|
+
|
|
134
|
+
async def after_response(
|
|
135
|
+
self, response: httpx.Response, ctx: Optional[Context] = None
|
|
136
|
+
) -> httpx.Response:
|
|
137
|
+
"""Process response after it's received.
|
|
138
|
+
|
|
139
|
+
This method can be used to handle auth-related response processing,
|
|
140
|
+
such as detecting expired tokens or rate limits.
|
|
141
|
+
|
|
142
|
+
:param response: The HTTP response received
|
|
143
|
+
:type response: httpx.Response
|
|
144
|
+
:param ctx: Optional FastMCP context for the current operation
|
|
145
|
+
:type ctx: Optional[Context]
|
|
146
|
+
:return: The response, potentially modified
|
|
147
|
+
:rtype: httpx.Response
|
|
148
|
+
"""
|
|
149
|
+
# Log auth-related errors with more detail
|
|
150
|
+
if response.status_code == 401:
|
|
151
|
+
error_detail = ""
|
|
152
|
+
try:
|
|
153
|
+
error_body = response.json()
|
|
154
|
+
error_detail = f" - Error: {error_body}"
|
|
155
|
+
except Exception:
|
|
156
|
+
error_detail = f" - Response: {response.text[:200]}"
|
|
157
|
+
|
|
158
|
+
logger.error(f"Received 401 Unauthorized - token may be expired or invalid{error_detail}")
|
|
159
|
+
logger.error(f"Request URL: {response.request.url}")
|
|
160
|
+
logger.error(f"Request had headers: {list(response.request.headers.keys())}")
|
|
161
|
+
|
|
162
|
+
# Check for specific auth headers
|
|
163
|
+
auth_header = response.request.headers.get("authorization", "")
|
|
164
|
+
if not auth_header:
|
|
165
|
+
logger.error("CRITICAL: No Authorization header in request!")
|
|
166
|
+
elif not auth_header.startswith("Bearer "):
|
|
167
|
+
logger.error(f"CRITICAL: Authorization header missing 'Bearer ' prefix: {auth_header[:20]}...")
|
|
168
|
+
|
|
169
|
+
elif response.status_code == 403:
|
|
170
|
+
logger.warning("Received 403 Forbidden - check permissions")
|
|
171
|
+
|
|
172
|
+
return response
|