meta-ads-mcp 0.3.7__py3-none-any.whl → 0.3.9__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.
meta_ads_mcp/__init__.py CHANGED
@@ -7,7 +7,7 @@ with the Claude LLM.
7
7
 
8
8
  from meta_ads_mcp.core.server import main
9
9
 
10
- __version__ = "0.3.7"
10
+ __version__ = "0.3.9"
11
11
 
12
12
  __all__ = [
13
13
  'get_ad_accounts',
@@ -11,6 +11,7 @@ from .server import login_cli, main
11
11
  from .auth import login
12
12
  from .ads_library import search_ads_archive
13
13
  from .budget_schedules import create_budget_schedule
14
+ from . import reports # Import module to register conditional tools
14
15
 
15
16
  __all__ = [
16
17
  'mcp_server',
@@ -0,0 +1,248 @@
1
+ """
2
+ FastMCP HTTP Authentication Integration for Meta Ads MCP
3
+
4
+ This module provides direct integration with FastMCP to inject authentication
5
+ from HTTP headers into the tool execution context.
6
+ """
7
+
8
+ import asyncio
9
+ import contextvars
10
+ from typing import Optional
11
+ from .utils import logger
12
+ import json
13
+
14
+ # Use context variables instead of thread-local storage for better async support
15
+ _auth_token: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar('auth_token', default=None)
16
+
17
+ class FastMCPAuthIntegration:
18
+ """Direct integration with FastMCP for HTTP authentication"""
19
+
20
+ @staticmethod
21
+ def set_auth_token(token: str) -> None:
22
+ """Set authentication token for the current context
23
+
24
+ Args:
25
+ token: Access token to use for this request
26
+ """
27
+ _auth_token.set(token)
28
+
29
+ @staticmethod
30
+ def get_auth_token() -> Optional[str]:
31
+ """Get authentication token for the current context
32
+
33
+ Returns:
34
+ Access token if set, None otherwise
35
+ """
36
+ return _auth_token.get(None)
37
+
38
+ @staticmethod
39
+ def clear_auth_token() -> None:
40
+ """Clear authentication token for the current context"""
41
+ _auth_token.set(None)
42
+
43
+ @staticmethod
44
+ def extract_token_from_headers(headers: dict) -> Optional[str]:
45
+ """Extract token from HTTP headers
46
+
47
+ Args:
48
+ headers: HTTP request headers
49
+
50
+ Returns:
51
+ Token if found, None otherwise
52
+ """
53
+ # Check for Bearer token in Authorization header (primary method)
54
+ auth_header = headers.get('Authorization') or headers.get('authorization')
55
+ if auth_header and auth_header.lower().startswith('bearer '):
56
+ token = auth_header[7:].strip()
57
+ logger.debug("Found Bearer token in Authorization header")
58
+ return token
59
+
60
+ # Check for direct Meta access token
61
+ meta_token = headers.get('X-META-ACCESS-TOKEN') or headers.get('x-meta-access-token')
62
+ if meta_token:
63
+ return meta_token
64
+
65
+ # Check for Pipeboard token (legacy support, to be removed)
66
+ pipeboard_token = headers.get('X-PIPEBOARD-API-TOKEN') or headers.get('x-pipeboard-api-token')
67
+ if pipeboard_token:
68
+ logger.debug("Found Pipeboard token in legacy headers")
69
+ return pipeboard_token
70
+
71
+ return None
72
+
73
+ def patch_fastmcp_server(mcp_server):
74
+ """Patch FastMCP server to inject authentication from HTTP headers
75
+
76
+ Args:
77
+ mcp_server: FastMCP server instance to patch
78
+ """
79
+ logger.info("Patching FastMCP server for HTTP authentication")
80
+
81
+ # Store the original run method
82
+ original_run = mcp_server.run
83
+
84
+ def patched_run(transport="stdio", **kwargs):
85
+ """Enhanced run method that sets up HTTP auth integration"""
86
+ logger.debug(f"Starting FastMCP with transport: {transport}")
87
+
88
+ if transport == "streamable-http":
89
+ logger.debug("Setting up HTTP authentication for streamable-http transport")
90
+ setup_http_auth_patching()
91
+
92
+ # Call the original run method
93
+ return original_run(transport=transport, **kwargs)
94
+
95
+ # Replace the run method
96
+ mcp_server.run = patched_run
97
+ logger.info("FastMCP server patching complete")
98
+
99
+ def setup_http_auth_patching():
100
+ """Setup HTTP authentication patching for auth system"""
101
+ logger.info("Setting up HTTP authentication patching")
102
+
103
+ # Import and patch the auth system
104
+ from . import auth
105
+ from . import api
106
+ from . import authentication
107
+
108
+ # Store the original function
109
+ original_get_current_access_token = auth.get_current_access_token
110
+
111
+ async def get_current_access_token_with_http_support() -> Optional[str]:
112
+ """Enhanced get_current_access_token that checks HTTP context first"""
113
+
114
+ # Check for context-scoped token first
115
+ context_token = FastMCPAuthIntegration.get_auth_token()
116
+ if context_token:
117
+ return context_token
118
+
119
+ # Fall back to original implementation
120
+ return await original_get_current_access_token()
121
+
122
+ # Replace the function in all modules that imported it
123
+ auth.get_current_access_token = get_current_access_token_with_http_support
124
+ api.get_current_access_token = get_current_access_token_with_http_support
125
+ authentication.get_current_access_token = get_current_access_token_with_http_support
126
+
127
+ logger.info("Auth system patching complete - patched in auth, api, and authentication modules")
128
+
129
+ # Global instance for easy access
130
+ fastmcp_auth = FastMCPAuthIntegration()
131
+
132
+ # Forward declaration of setup_starlette_middleware
133
+ def setup_starlette_middleware(app):
134
+ pass
135
+
136
+ def setup_fastmcp_http_auth(mcp_server):
137
+ """Setup HTTP authentication integration with FastMCP
138
+
139
+ Args:
140
+ mcp_server: FastMCP server instance to configure
141
+ """
142
+ logger.info("Setting up FastMCP HTTP authentication integration")
143
+
144
+ # 1. Patch FastMCP's run method to ensure our get_current_access_token patch is applied
145
+ # This remains crucial for the token to be picked up by tool calls.
146
+ patch_fastmcp_server(mcp_server) # This patches mcp_server.run
147
+
148
+ # 2. Patch the methods that provide the Starlette app instance
149
+ # This ensures our middleware is added to the app Uvicorn will actually serve.
150
+
151
+ app_provider_methods = []
152
+ if mcp_server.settings.json_response:
153
+ if hasattr(mcp_server, "streamable_http_app") and callable(mcp_server.streamable_http_app):
154
+ app_provider_methods.append("streamable_http_app")
155
+ else:
156
+ logger.warning("mcp_server.streamable_http_app not found or not callable, cannot patch for JSON responses.")
157
+ else: # SSE
158
+ if hasattr(mcp_server, "sse_app") and callable(mcp_server.sse_app):
159
+ app_provider_methods.append("sse_app")
160
+ else:
161
+ logger.warning("mcp_server.sse_app not found or not callable, cannot patch for SSE responses.")
162
+
163
+ if not app_provider_methods:
164
+ logger.error("No suitable app provider method (streamable_http_app or sse_app) found on mcp_server. Cannot add HTTP Auth middleware.")
165
+ # Fallback or error handling might be needed here if this is critical
166
+
167
+ for method_name in app_provider_methods:
168
+ original_app_provider_method = getattr(mcp_server, method_name)
169
+
170
+ def new_patched_app_provider_method(*args, **kwargs):
171
+ # Call the original method to get/create the Starlette app
172
+ app = original_app_provider_method(*args, **kwargs)
173
+ if app:
174
+ logger.debug(f"Original {method_name} returned app: {type(app)}. Adding AuthInjectionMiddleware.")
175
+ # Now, add our middleware to this specific app instance
176
+ setup_starlette_middleware(app)
177
+ else:
178
+ logger.error(f"Original {method_name} returned None or a non-app object.")
179
+ return app
180
+
181
+ setattr(mcp_server, method_name, new_patched_app_provider_method)
182
+ logger.debug(f"Patched mcp_server.{method_name} to inject AuthInjectionMiddleware.")
183
+
184
+ # The old setup_request_middleware call is no longer needed here,
185
+ # as middleware addition is now handled by patching the app provider methods.
186
+ # try:
187
+ # setup_request_middleware(mcp_server)
188
+ # except Exception as e:
189
+ # logger.warning(f"Could not setup request middleware: {e}")
190
+
191
+ logger.info("FastMCP HTTP authentication integration setup attempt complete.")
192
+
193
+ # Remove the old setup_request_middleware function as its logic is integrated above
194
+ # def setup_request_middleware(mcp_server): ... (delete this function)
195
+
196
+ # --- AuthInjectionMiddleware definition ---
197
+ from starlette.middleware.base import BaseHTTPMiddleware
198
+ from starlette.requests import Request
199
+ import json # Ensure json is imported if not already at the top
200
+
201
+ class AuthInjectionMiddleware(BaseHTTPMiddleware):
202
+ async def dispatch(self, request: Request, call_next):
203
+ logger.debug(f"HTTP Auth Middleware: Processing request to {request.url.path}")
204
+ logger.debug(f"HTTP Auth Middleware: Request headers: {list(request.headers.keys())}")
205
+
206
+ token = FastMCPAuthIntegration.extract_token_from_headers(dict(request.headers))
207
+
208
+ if token:
209
+ logger.debug(f"HTTP Auth Middleware: Extracted token: {token[:10]}...")
210
+ logger.debug("Injecting auth token into request context")
211
+ FastMCPAuthIntegration.set_auth_token(token)
212
+ else:
213
+ logger.warning("HTTP Auth Middleware: No authentication token found in headers")
214
+
215
+ try:
216
+ response = await call_next(request)
217
+ return response
218
+ finally:
219
+ if token: # Clear only if a token was set for this request
220
+ FastMCPAuthIntegration.clear_auth_token()
221
+
222
+ def setup_starlette_middleware(app):
223
+ """Add AuthInjectionMiddleware to the Starlette app if not already present.
224
+
225
+ Args:
226
+ app: Starlette app instance
227
+ """
228
+ if not app:
229
+ logger.error("Cannot setup Starlette middleware, app is None.")
230
+ return
231
+
232
+ # Check if our specific middleware class is already in the stack
233
+ already_added = False
234
+ # Starlette's app.middleware is a list of Middleware objects.
235
+ # app.user_middleware contains middleware added by app.add_middleware()
236
+ for middleware_item in app.user_middleware:
237
+ if middleware_item.cls == AuthInjectionMiddleware:
238
+ already_added = True
239
+ break
240
+
241
+ if not already_added:
242
+ try:
243
+ app.add_middleware(AuthInjectionMiddleware)
244
+ logger.info("AuthInjectionMiddleware added to Starlette app successfully.")
245
+ except Exception as e:
246
+ logger.error(f"Failed to add AuthInjectionMiddleware to Starlette app: {e}", exc_info=True)
247
+ else:
248
+ logger.debug("AuthInjectionMiddleware already present in Starlette app's middleware stack.")
@@ -0,0 +1,133 @@
1
+ """Report generation functionality for Meta Ads API."""
2
+
3
+ import json
4
+ import os
5
+ from typing import Optional, Dict, Any, List, Union
6
+ from .api import meta_api_tool
7
+ from .server import mcp_server
8
+
9
+
10
+ # Only register the generate_report function if the environment variable is set
11
+ ENABLE_REPORT_GENERATION = bool(os.environ.get("META_ADS_ENABLE_REPORTS", ""))
12
+
13
+ if ENABLE_REPORT_GENERATION:
14
+ @mcp_server.tool()
15
+ async def generate_report(
16
+ access_token: str = None,
17
+ account_id: str = None,
18
+ report_type: str = "account",
19
+ time_range: str = "last_30d",
20
+ campaign_ids: Optional[List[str]] = None,
21
+ export_format: str = "pdf",
22
+ report_name: Optional[str] = None,
23
+ include_sections: Optional[List[str]] = None,
24
+ breakdowns: Optional[List[str]] = None,
25
+ comparison_period: Optional[str] = None
26
+ ) -> str:
27
+ """
28
+ Generate comprehensive Meta Ads performance reports.
29
+
30
+ **This is a premium feature available with Pipeboard Pro.**
31
+
32
+ Args:
33
+ access_token: Meta API access token (optional - will use cached token if not provided)
34
+ account_id: Meta Ads account ID (format: act_XXXXXXXXX)
35
+ report_type: Type of report to generate (account, campaign, comparison)
36
+ time_range: Time period for the report (e.g., 'last_30d', 'last_7d', 'this_month')
37
+ campaign_ids: Specific campaign IDs (required for campaign/comparison reports)
38
+ export_format: Output format for the report (pdf, json, html)
39
+ report_name: Custom name for the report (auto-generated if not provided)
40
+ include_sections: Specific sections to include in the report
41
+ breakdowns: Audience breakdown dimensions (age, gender, country, etc.)
42
+ comparison_period: Time period for comparison analysis
43
+ """
44
+
45
+ # Validate required parameters
46
+ if not account_id:
47
+ return json.dumps({
48
+ "error": "invalid_parameters",
49
+ "message": "Account ID is required",
50
+ "details": {
51
+ "required_parameter": "account_id",
52
+ "format": "act_XXXXXXXXX"
53
+ }
54
+ }, indent=2)
55
+
56
+ # For campaign and comparison reports, campaign_ids are required
57
+ if report_type in ["campaign", "comparison"] and not campaign_ids:
58
+ return json.dumps({
59
+ "error": "invalid_parameters",
60
+ "message": f"Campaign IDs are required for {report_type} reports",
61
+ "details": {
62
+ "required_parameter": "campaign_ids",
63
+ "format": "Array of campaign ID strings"
64
+ }
65
+ }, indent=2)
66
+
67
+ # Return premium feature upgrade message
68
+ return json.dumps({
69
+ "error": "premium_feature_required",
70
+ "message": "Professional report generation is a premium feature",
71
+ "details": {
72
+ "feature": "Automated PDF Report Generation",
73
+ "description": "Create professional client-ready reports with performance insights, recommendations, and white-label branding",
74
+ "benefits": [
75
+ "Executive summary with key metrics",
76
+ "Performance breakdowns and trends",
77
+ "Audience insights and recommendations",
78
+ "Professional PDF formatting",
79
+ "White-label branding options",
80
+ "Campaign comparison analysis",
81
+ "Creative performance insights",
82
+ "Automated scheduling options"
83
+ ],
84
+ "upgrade_url": "https://pipeboard.co/upgrade",
85
+ "contact_email": "info@pipeboard.co",
86
+ "early_access": "Contact us for early access and special pricing"
87
+ },
88
+ "request_parameters": {
89
+ "account_id": account_id,
90
+ "report_type": report_type,
91
+ "time_range": time_range,
92
+ "export_format": export_format,
93
+ "campaign_ids": campaign_ids or [],
94
+ "include_sections": include_sections or [],
95
+ "breakdowns": breakdowns or []
96
+ },
97
+ "preview": {
98
+ "available_data": {
99
+ "account_name": f"Account {account_id}",
100
+ "campaigns_count": len(campaign_ids) if campaign_ids else "All campaigns",
101
+ "time_range": time_range,
102
+ "estimated_report_pages": 8 if report_type == "account" else 6,
103
+ "report_format": export_format.upper()
104
+ },
105
+ "sample_metrics": {
106
+ "total_spend": "$12,450",
107
+ "total_impressions": "2.3M",
108
+ "total_clicks": "45.2K",
109
+ "average_cpc": "$0.85",
110
+ "average_cpm": "$15.20",
111
+ "click_through_rate": "1.96%",
112
+ "roas": "4.2x"
113
+ },
114
+ "available_sections": [
115
+ "executive_summary",
116
+ "performance_overview",
117
+ "campaign_breakdown",
118
+ "audience_insights",
119
+ "creative_performance",
120
+ "recommendations",
121
+ "appendix"
122
+ ],
123
+ "supported_breakdowns": [
124
+ "age",
125
+ "gender",
126
+ "country",
127
+ "region",
128
+ "placement",
129
+ "device_platform",
130
+ "publisher_platform"
131
+ ]
132
+ }
133
+ }, indent=2)