Skip to content

Commit 76a288c

Browse files
committed
commit updates for fastmcp 2.14.1
1 parent 1347a68 commit 76a288c

File tree

14 files changed

+593
-75
lines changed

14 files changed

+593
-75
lines changed

AGENTS.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,46 @@ export MCP_MASK_ERROR_DETAILS=true
232232
export MCP_ON_DUPLICATE_TOOLS=warn
233233
```
234234

235+
### Security-Related Environment Variables
236+
237+
```bash
238+
# Token Persistence (disabled by default for security)
239+
# When false (default): Tokens stored in memory only, lost on restart
240+
# When true: Refresh tokens cached to disk/volume, persist across restarts
241+
export AMAZON_ADS_TOKEN_PERSIST="false" # Set to "true" to enable persistence
242+
243+
# Token Encryption (required when AMAZON_ADS_TOKEN_PERSIST=true)
244+
# If not set, a random key is auto-generated and stored alongside tokens
245+
# For production: Generate with `python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"`
246+
export AMAZON_ADS_ENCRYPTION_KEY="your-44-char-base64-key"
247+
248+
# Plaintext Storage (INSECURE - testing only!)
249+
# Only enable if cryptography library is unavailable AND you accept the risk
250+
export AMAZON_ADS_ALLOW_PLAINTEXT_PERSIST="false" # Never set to "true" in production
251+
```
252+
253+
**Security Implications of Token Persistence:**
254+
255+
When `AMAZON_ADS_TOKEN_PERSIST=true`:
256+
- ✅ Convenience: Refresh tokens survive container restarts (no re-authentication)
257+
- ⚠️ Risk: Tokens are encrypted but stored with their decryption key on same volume
258+
- ⚠️ Risk: Anyone with filesystem/volume access can potentially extract tokens
259+
- 🔒 Mitigation: Set `AMAZON_ADS_ENCRYPTION_KEY` from external secrets manager
260+
- 🔒 Best Practice: Use in-memory storage (default) unless persistence is required
261+
262+
**Recommended Configurations:**
263+
264+
```bash
265+
# Development (local laptop): In-memory is usually sufficient
266+
AMAZON_ADS_TOKEN_PERSIST=false
267+
268+
# Shared/Production: Either in-memory OR persistence with external key
269+
AMAZON_ADS_TOKEN_PERSIST=false # Preferred if feasible
270+
# OR
271+
AMAZON_ADS_TOKEN_PERSIST=true
272+
AMAZON_ADS_ENCRYPTION_KEY=${VAULT_SECRET} # From secrets manager
273+
```
274+
235275
### Code Standards
236276

237277
- Python ≥ 3.10 with full type annotations

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,7 @@ In this example, we show how to use the bearer token using the Openbridge API ke
408408
"http://${HOSTNAME}:${PORT}/mcp/",
409409
"--allow-http",
410410
"--header",
411-
"Authorization:Bearer ${OPENBRIDGE_REFRESH_TOKEN}",
411+
"Authorization:Bearer ${OPENBRIDGE_API_KEY}",
412412
"--header",
413413
"Accept:application/json,text/event-stream",
414414
"--debug"
@@ -423,7 +423,7 @@ In this example, we show how to use the bearer token using the Openbridge API ke
423423
"MCP_SERVER_REQUEST_TIMEOUT": "60000",
424424
"MCP_TOOL_TIMEOUT": "120000",
425425
"MCP_REQUEST_WARNING_THRESHOLD": "10000",
426-
"OPENBRIDGE_REFRESH_TOKEN": "your_openbridge_token_here"
426+
"OPENBRIDGE_API_KEY": "your_openbridge_token_here"
427427
}
428428
}
429429
}
@@ -468,7 +468,7 @@ The config would look something like this:
468468
}
469469
```
470470

471-
Here is another example, which can be used if you are using OAuth since the `OPENBRIDGE_REFRESH_TOKEN` is not needed:
471+
Here is another example, which can be used if you are using OAuth since the `OPENBRIDGE_API_KEY` is not needed:
472472

473473
```json
474474
{

docker-compose.yaml

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,20 @@ services:
1313
- HOST=0.0.0.0
1414
- PORT=9080
1515
- LOG_LEVEL=INFO
16-
- AMAZON_AD_PACKAGES=profiles,accounts-ads-accounts
17-
- AMAZON_ADS_TOKEN_PERSIST=true
16+
# Note: This is overridden by .env if AMAZON_AD_API_PACKAGES is set there
17+
- AMAZON_AD_API_PACKAGES=profiles,accounts-ads-accounts,exports-snapshots
18+
# Token persistence (disabled by default for security)
19+
# Set to 'true' to cache refresh tokens across restarts (see docs for security implications)
20+
- AMAZON_ADS_TOKEN_PERSIST=false
1821
- AMAZON_ADS_CACHE_DIR=/app/.cache/amazon-ads-mcp
19-
# Authentication (configure as needed for your environment)
20-
# - AUTH_METHOD=openbridge
22+
# Authentication method - REQUIRED for header-based auth!
23+
# Set to 'openbridge' when passing credentials via Authorization header from client
24+
- AMAZON_ADS_AUTH_METHOD=openbridge
25+
# REQUIRED: Enable refresh token middleware to process Authorization headers
26+
- REFRESH_TOKEN_ENABLED=true
27+
- AUTH_ENABLED=true
28+
# Disable JWT validation (OpenBridge handles authentication)
29+
- JWT_VALIDATION_ENABLED=false
2130

2231
ports:
2332
- "9080:9080"

pyproject.toml

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ version = "0.1.15"
44
description = "Amazon Ads API MCP Server - Implementation for Amazon Advertising API"
55
readme = "README.md"
66
requires-python = ">=3.10,<4.0"
7-
dependencies = [ "fastmcp>=2.11.3", "python-dotenv>=1.0.0", "pydantic>=2.5.0", "pydantic-settings>=2.0.0", "httpx>=0.27.0", "pyjwt>=2.8.0", "cryptography>=41.0.0", "openai>=1.0.0", "PyYAML>=6.0.0",]
7+
dependencies = [ "fastmcp>=2.14.1", "python-dotenv>=1.2.1", "pydantic>=2.12.5", "pydantic-settings>=2.12.0", "httpx>=0.28.1", "pyjwt>=2.10.1", "cryptography>=41.0.0", "openai>=1.109.1", "PyYAML>=6.0.2",]
88
[[project.authors]]
99
name = "Amazon Ads API MCP SDK"
1010

@@ -13,7 +13,7 @@ requires = [ "poetry-core",]
1313
build-backend = "poetry.core.masonry.api"
1414

1515
[dependency-groups]
16-
dev = [ "black>=25.1.0", "isort>=6.0.1", "mypy>=1.18.1", "pytest>=8.4.2", "pytest-asyncio>=1.2.0", "ruff>=0.13.0",]
16+
dev = [ "black>=25.12.0", "isort>=7.0.0", "mypy>=1.19.1", "pytest>=9.0.2", "pytest-asyncio>=1.3.0", "ruff>=0.14.10",]
1717

1818
[project.license]
1919
text = "MIT"
@@ -78,15 +78,15 @@ asyncio_mode = "auto"
7878

7979
[tool.poetry.dependencies]
8080
python = ">=3.10,<4.0"
81-
fastmcp = "^2.11.3"
82-
python-dotenv = "^1.0.0"
83-
pydantic = "^2.5.0"
84-
pydantic-settings = "^2.0.0"
85-
httpx = ">=0.27.0"
86-
pyjwt = "^2.8.0"
81+
fastmcp = "^2.14.1"
82+
python-dotenv = "^1.2.1"
83+
pydantic = "^2.12.5"
84+
pydantic-settings = "^2.12.0"
85+
httpx = ">=0.28.1"
86+
pyjwt = "^2.10.1"
8787
cryptography = "^41.0.0"
88-
openai = "^1.0.0"
89-
PyYAML = "^6.0.0"
88+
openai = "^1.109.1"
89+
PyYAML = "^6.0.2"
9090

9191
[tool.poetry.scripts]
9292
amazon-ads-mcp = "amazon_ads_mcp.server.mcp_server:main"
@@ -103,11 +103,11 @@ omit = [ "*/tests/*", "*/debug/*",]
103103
exclude_lines = [ "pragma: no cover", "def __repr__", "if self.debug:", "if settings.DEBUG", "raise AssertionError", "raise NotImplementedError", "if 0:", "if __name__ == .__main__.:", "class .*\\bProtocol\\):", "@(abc\\.)?abstractmethod",]
104104

105105
[tool.poetry.group.dev.dependencies]
106-
pytest = "^8.4.2"
107-
pytest-asyncio = "^1.2.0"
108-
black = "^25.1.0"
109-
isort = "^6.0.1"
110-
ruff = "^0.13.0"
111-
mypy = "^1.18.1"
106+
pytest = "^9.0.2"
107+
pytest-asyncio = "^1.3.0"
108+
black = "^25.12.0"
109+
isort = "^7.0.0"
110+
ruff = "^0.14.10"
111+
mypy = "^1.19.1"
112112
types-pyjwt = "^1.7.1"
113113
types-python-dotenv = "^1.0.0"

requirements.txt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
# Core dependencies
2-
fastmcp>=2.12.2
2+
fastmcp>=2.14.1
33
httpx[http2]>=0.28.1
4-
pydantic>=2.11.9
5-
pydantic-settings>=2.10.1
6-
python-dotenv>=1.1.1
4+
pydantic>=2.12.5
5+
pydantic-settings>=2.12.0
6+
python-dotenv>=1.2.1
77
PyJWT>=2.10.1
88
PyYAML>=6.0.2
99
jsonschema>=4.25.1
10-
openai>=1.108.0
10+
openai>=1.109.1
1111

1212
# Development dependencies (optional)
1313
# Install with: pip install -r requirements-dev.txt

src/amazon_ads_mcp/auth/hooks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ async def after_response(
152152
try:
153153
error_body = response.json()
154154
error_detail = f" - Error: {error_body}"
155-
except:
155+
except Exception:
156156
error_detail = f" - Response: {response.text[:200]}"
157157

158158
logger.error(f"Received 401 Unauthorized - token may be expired or invalid{error_detail}")

src/amazon_ads_mcp/auth/manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ def _determine_auth_method(self) -> str:
192192
raise ValueError(
193193
"No authentication method configured. Please set one of:\n"
194194
"- For direct auth: AD_API_CLIENT_ID, AD_API_CLIENT_SECRET, AD_API_REFRESH_TOKEN\n"
195-
"- For OpenBridge: OPENBRIDGE_REFRESH_TOKEN\n"
195+
"- For OpenBridge: OPENBRIDGE_REFRESH_TOKEN (or OPENBRIDGE_API_KEY)\n"
196196
"- Or explicitly set AUTH_METHOD environment variable"
197197
)
198198

src/amazon_ads_mcp/auth/providers/openbridge.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ async def get_token(self) -> Token:
157157

158158
if not self.refresh_token:
159159
raise ValueError(
160-
"No refresh token available. Provide it via config or Authorization header."
160+
"No OpenBridge token available. Set OPENBRIDGE_REFRESH_TOKEN (or OPENBRIDGE_API_KEY), or pass it via Authorization header."
161161
)
162162

163163
return await self._refresh_jwt_token()

src/amazon_ads_mcp/auth/token_store.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,16 @@ def __init__(
372372
# Load existing tokens on startup
373373
self._load_from_disk()
374374

375+
# Warn about security implications of token persistence
376+
logger.info(
377+
f"Token persistence ENABLED. Refresh tokens will be stored at {self._storage_path}\n"
378+
f"Security considerations:\n"
379+
f" - Tokens are encrypted at rest, but the encryption key is stored alongside\n"
380+
f" - Anyone with access to the volume/filesystem can potentially decrypt tokens\n"
381+
f" - For production use, set AMAZON_ADS_ENCRYPTION_KEY externally\n"
382+
f" - Consider in-memory-only storage (AMAZON_ADS_TOKEN_PERSIST=false) if possible"
383+
)
384+
375385
async def set(self, key: TokenKey, entry: TokenEntry) -> None:
376386
"""Store token, persisting refresh tokens to disk."""
377387
await super().set(key, entry)

src/amazon_ads_mcp/config/settings.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from typing import Literal, Optional
99

10-
from pydantic import Field, field_validator
10+
from pydantic import AliasChoices, Field, field_validator
1111
from pydantic_settings import BaseSettings, SettingsConfigDict
1212

1313
from ..utils.region_config import RegionConfig
@@ -29,7 +29,7 @@ class Settings(BaseSettings):
2929
:type amazon_ads_client_secret: Optional[str]
3030
:param amazon_ads_refresh_token: Amazon Ads API Refresh Token for direct auth
3131
:type amazon_ads_refresh_token: Optional[str]
32-
:param openbridge_refresh_token: OpenBridge refresh token (key:secret format)
32+
:param openbridge_refresh_token: OpenBridge API key (aka refresh token)
3333
:type openbridge_refresh_token: Optional[str]
3434
:param openbridge_remote_identity_id: OpenBridge remote identity ID
3535
:type openbridge_remote_identity_id: Optional[str]
@@ -100,7 +100,9 @@ class Settings(BaseSettings):
100100

101101
# Openbridge Configuration
102102
openbridge_refresh_token: Optional[str] = Field(
103-
None, description="Openbridge Refresh Token (format: key:secret)"
103+
None,
104+
validation_alias=AliasChoices("OPENBRIDGE_REFRESH_TOKEN", "OPENBRIDGE_API_KEY"),
105+
description="OpenBridge API key (aka refresh token)",
104106
)
105107
openbridge_remote_identity_id: Optional[str] = Field(
106108
None, description="Openbridge Remote Identity ID for Amazon Ads"

0 commit comments

Comments
 (0)