Skip to content

Commit 3e4b10d

Browse files
committed
fix: raise RateLimitError on 429 so FastMCP sets isError in MCP response
Add RateLimitError exception class for 429 responses from the duplication API. This propagates through FastMCP as isError: true, enabling the Next.js proxy to detect and retry rate-limited requests.
1 parent 4020739 commit 3e4b10d

File tree

2 files changed

+31
-22
lines changed

2 files changed

+31
-22
lines changed

meta_ads_mcp/core/duplication.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
logger = logging.getLogger(__name__)
1414

1515

16+
class RateLimitError(Exception):
17+
"""Raised on 429 so FastMCP sets isError: true in the MCP response."""
18+
pass
19+
20+
1621
# Only register the duplication functions if the environment variable is set
1722
ENABLE_DUPLICATION = bool(os.environ.get("META_ADS_ENABLE_DUPLICATION", ""))
1823

@@ -368,14 +373,16 @@ async def _forward_duplication_request(resource_type: str, resource_id: str, acc
368373
"suggestion": f"Verify the {resource_type} ID and your Facebook account permissions"
369374
}, indent=2)
370375
elif response.status_code == 429:
371-
return json.dumps({
372-
"error": "rate_limit_exceeded",
376+
# Raise so FastMCP sets isError: true in MCP response,
377+
# enabling the Next.js proxy to detect and retry.
378+
raise RateLimitError(json.dumps({
379+
"error": "rate_limit_exceeded",
373380
"message": "Meta API rate limit exceeded",
374381
"details": {
375382
"suggestion": "Please wait before retrying",
376383
"retry_after": response.headers.get("Retry-After", "60")
377384
}
378-
}, indent=2)
385+
}, indent=2))
379386
elif response.status_code == 502:
380387
try:
381388
error_data = response.json()
@@ -443,6 +450,9 @@ async def _forward_duplication_request(resource_type: str, resource_id: str, acc
443450
}
444451
}, indent=2)
445452

453+
except RateLimitError:
454+
raise # Let FastMCP handle this to set isError: true
455+
446456
except Exception as e:
447457
return json.dumps({
448458
"error": "unexpected_error",

tests/test_duplication_regression.py

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -307,27 +307,26 @@ async def test_http_status_code_handling(self, enable_feature):
307307
mock_response.text = f"Error {status_code}"
308308

309309
mock_client.return_value.__aenter__.return_value.post.return_value = mock_response
310-
311-
result = await duplication._forward_duplication_request(
312-
"campaign", "123", "token", {}
313-
)
314-
result_json = json.loads(result)
315-
316-
if response_type == "error":
317-
if status_code == 401:
318-
assert result_json["error"] == expected_error_type
319-
elif status_code == 403:
320-
assert result_json["error"] == expected_error_type
321-
elif status_code == 400:
322-
assert result_json["error"] == expected_error_type
323-
elif status_code == 404:
324-
assert result_json["error"] == expected_error_type
325-
elif status_code == 502:
310+
311+
if status_code == 429:
312+
# 429 raises RateLimitError so FastMCP sets isError: true
313+
from meta_ads_mcp.core.duplication import RateLimitError
314+
with pytest.raises(RateLimitError) as exc_info:
315+
await duplication._forward_duplication_request(
316+
"campaign", "123", "token", {}
317+
)
318+
exc_json = json.loads(str(exc_info.value))
319+
assert exc_json["error"] == expected_error_type
320+
else:
321+
result = await duplication._forward_duplication_request(
322+
"campaign", "123", "token", {}
323+
)
324+
result_json = json.loads(result)
325+
326+
if response_type == "error":
326327
assert result_json["error"] == expected_error_type
327328
else:
328-
assert result_json["error"] == expected_error_type
329-
else:
330-
assert "success" in result_json or "id" in result_json
329+
assert "success" in result_json or "id" in result_json
331330

332331
@pytest.mark.asyncio
333332
async def test_network_error_handling(self, enable_feature):

0 commit comments

Comments
 (0)