meta-ads-mcp-python 1.0.79__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.
@@ -0,0 +1,46 @@
1
+ """Resource handling for Meta Ads API."""
2
+
3
+ from typing import Dict, Any
4
+ import base64
5
+ from .utils import ad_creative_images
6
+
7
+
8
+ async def list_resources() -> Dict[str, Any]:
9
+ """
10
+ List all available resources (like ad creative images)
11
+
12
+ Returns:
13
+ Dictionary with resources list
14
+ """
15
+ resources = []
16
+
17
+ # Add all ad creative images as resources
18
+ for resource_id, image_info in ad_creative_images.items():
19
+ resources.append({
20
+ "uri": f"meta-ads://images/{resource_id}",
21
+ "mimeType": image_info["mime_type"],
22
+ "name": image_info["name"]
23
+ })
24
+
25
+ return {"resources": resources}
26
+
27
+
28
+ async def get_resource(resource_id: str) -> Dict[str, Any]:
29
+ """
30
+ Get a specific resource by URI
31
+
32
+ Args:
33
+ resource_id: Unique identifier for the resource
34
+
35
+ Returns:
36
+ Dictionary with resource data
37
+ """
38
+ if resource_id in ad_creative_images:
39
+ image_info = ad_creative_images[resource_id]
40
+ return {
41
+ "data": base64.b64encode(image_info["data"]).decode("utf-8"),
42
+ "mimeType": image_info["mime_type"]
43
+ }
44
+
45
+ # Resource not found
46
+ return {"error": f"Resource not found: {resource_id}"}
@@ -0,0 +1,391 @@
1
+ """MCP server configuration for Meta Ads API."""
2
+
3
+ from fastmcp import FastMCP
4
+ import argparse
5
+ import os
6
+ import sys
7
+ import webbrowser
8
+ import json
9
+ from typing import Dict, Any, Optional
10
+ import time
11
+
12
+ from .auth import login as login_auth
13
+ from .pipeboard_auth import pipeboard_auth_manager
14
+ from .resources import list_resources, get_resource
15
+ from .utils import logger
16
+ from ..settings import load_settings
17
+
18
+ settings = load_settings()
19
+
20
+ # Initialize FastMCP server
21
+ mcp_server = FastMCP("meta-ads")
22
+
23
+ # Register resource URIs
24
+ mcp_server.resource(uri="meta-ads://resources")(list_resources)
25
+ mcp_server.resource(uri="meta-ads://images/{resource_id}")(get_resource)
26
+
27
+
28
+ class StreamableHTTPHandler:
29
+ """Handles stateless Streamable HTTP requests for Meta Ads MCP"""
30
+
31
+ def __init__(self):
32
+ """Initialize handler with no session storage - all auth per request"""
33
+ logger.debug("StreamableHTTPHandler initialized for stateless operation")
34
+
35
+ def handle_request(self, request_headers: Dict[str, str], request_body: Dict[str, Any]) -> Dict[str, Any]:
36
+ """Handle individual request with authentication
37
+
38
+ Args:
39
+ request_headers: HTTP request headers
40
+ request_body: JSON-RPC request body
41
+
42
+ Returns:
43
+ JSON response with auth status and any tool results
44
+ """
45
+ try:
46
+ # Extract authentication configuration from headers
47
+ auth_config = self.get_auth_config_from_headers(request_headers)
48
+ logger.debug(f"Auth method detected: {auth_config['auth_method']}")
49
+
50
+ # Handle based on auth method
51
+ if auth_config['auth_method'] == 'bearer':
52
+ return self.handle_bearer_request(auth_config, request_body)
53
+ elif auth_config['auth_method'] == 'custom_meta_app':
54
+ return self.handle_custom_app_request(auth_config, request_body)
55
+ else:
56
+ return self.handle_unauthenticated_request(request_body)
57
+
58
+ except Exception as e:
59
+ logger.error(f"Error handling request: {e}")
60
+ return {
61
+ 'jsonrpc': '2.0',
62
+ 'error': {
63
+ 'code': -32603,
64
+ 'message': 'Internal error',
65
+ 'data': str(e)
66
+ },
67
+ 'id': request_body.get('id')
68
+ }
69
+
70
+ def get_auth_config_from_headers(self, request_headers: Dict[str, str]) -> Dict[str, Any]:
71
+ """Extract authentication configuration from HTTP headers
72
+
73
+ Args:
74
+ request_headers: HTTP request headers
75
+
76
+ Returns:
77
+ Dictionary with auth method and relevant credentials
78
+ """
79
+ # Security validation - only allow safe headers
80
+ ALLOWED_VIA_HEADERS = {
81
+ 'pipeboard_api_token': True, # Primary method - simple and secure
82
+ 'meta_app_id': True, # Fallback only - triggers OAuth complexity
83
+ 'meta_app_secret': False, # Server environment only
84
+ 'meta_access_token': False, # Use proper auth flows instead
85
+ }
86
+
87
+ # PRIMARY: Check for Bearer token in Authorization header (handles 90%+ of cases)
88
+ auth_header = request_headers.get('Authorization') or request_headers.get('authorization')
89
+ if auth_header and auth_header.lower().startswith('bearer '):
90
+ token = auth_header[7:].strip()
91
+ logger.info("Bearer authentication detected (primary path)")
92
+ return {
93
+ 'auth_method': 'bearer',
94
+ 'bearer_token': token,
95
+ 'requires_oauth': False # Simple token-based auth
96
+ }
97
+
98
+ # FALLBACK: Custom Meta app (minority of users)
99
+ meta_app_id = request_headers.get('X-META-APP-ID') or request_headers.get('x-meta-app-id')
100
+ if meta_app_id:
101
+ logger.debug("Custom Meta app authentication detected (fallback path)")
102
+ return {
103
+ 'auth_method': 'custom_meta_app',
104
+ 'meta_app_id': meta_app_id,
105
+ 'requires_oauth': True # Complex OAuth flow required
106
+ }
107
+
108
+ # No authentication provided
109
+ logger.warning("No authentication method detected in headers")
110
+ return {
111
+ 'auth_method': 'none',
112
+ 'requires_oauth': False
113
+ }
114
+
115
+ def handle_bearer_request(self, auth_config: Dict[str, Any], request_body: Dict[str, Any]) -> Dict[str, Any]:
116
+ """Handle request with Bearer token (primary path)
117
+
118
+ Args:
119
+ auth_config: Authentication configuration from headers
120
+ request_body: JSON-RPC request body
121
+
122
+ Returns:
123
+ JSON response ready for tool execution
124
+ """
125
+ logger.debug("Processing Bearer authenticated request")
126
+ token = auth_config['bearer_token']
127
+
128
+ # Token is ready to use immediately for API calls
129
+ # TODO: In next phases, this will execute the actual tool call
130
+ return {
131
+ 'jsonrpc': '2.0',
132
+ 'result': {
133
+ 'status': 'ready',
134
+ 'auth_method': 'bearer',
135
+ 'message': 'Authentication successful with Bearer token',
136
+ 'token_source': 'bearer_header'
137
+ },
138
+ 'id': request_body.get('id')
139
+ }
140
+
141
+ def handle_custom_app_request(self, auth_config: Dict[str, Any], request_body: Dict[str, Any]) -> Dict[str, Any]:
142
+ """Handle request with custom Meta app (fallback path)
143
+
144
+ Args:
145
+ auth_config: Authentication configuration from headers
146
+ request_body: JSON-RPC request body
147
+
148
+ Returns:
149
+ JSON response indicating OAuth flow is required
150
+ """
151
+ logger.debug("Processing custom Meta app request (OAuth required)")
152
+
153
+ # This may require OAuth flow initiation
154
+ # Each request is independent - no session state
155
+ return {
156
+ 'jsonrpc': '2.0',
157
+ 'result': {
158
+ 'status': 'oauth_required',
159
+ 'auth_method': 'custom_meta_app',
160
+ 'meta_app_id': auth_config['meta_app_id'],
161
+ 'message': 'OAuth flow required for custom Meta app authentication',
162
+ 'next_steps': 'Use get_login_link tool to initiate OAuth flow'
163
+ },
164
+ 'id': request_body.get('id')
165
+ }
166
+
167
+ def handle_unauthenticated_request(self, request_body: Dict[str, Any]) -> Dict[str, Any]:
168
+ """Handle request with no authentication
169
+
170
+ Args:
171
+ request_body: JSON-RPC request body
172
+
173
+ Returns:
174
+ JSON error response requesting authentication
175
+ """
176
+ logger.warning("Unauthenticated request received")
177
+
178
+ return {
179
+ 'jsonrpc': '2.0',
180
+ 'error': {
181
+ 'code': -32600,
182
+ 'message': 'Authentication required',
183
+ 'data': {
184
+ 'supported_methods': [
185
+ 'Authorization: Bearer <token> (recommended)',
186
+ 'X-META-APP-ID: Custom Meta app OAuth (advanced users)'
187
+ ],
188
+ 'documentation': 'https://github.com/pipeboard-co/meta-ads-mcp'
189
+ }
190
+ },
191
+ 'id': request_body.get('id')
192
+ }
193
+
194
+
195
+ def login_cli():
196
+ """
197
+ Command-line function to authenticate with Meta
198
+ """
199
+ logger.info("Starting Meta Ads CLI authentication flow")
200
+ print("Starting Meta Ads CLI authentication flow...")
201
+
202
+ # Call the common login function
203
+ login_auth()
204
+
205
+
206
+ def main():
207
+ """Main entry point for the package"""
208
+ # Log startup information
209
+ logger.info("Meta Ads MCP server starting")
210
+ logger.debug(f"Python version: {sys.version}")
211
+ logger.debug(f"Args: {sys.argv}")
212
+
213
+ # Initialize argument parser
214
+ parser = argparse.ArgumentParser(
215
+ description="Meta Ads MCP Server - Model Context Protocol server for Meta Ads API",
216
+ epilog="For more information, see https://github.com/pipeboard-co/meta-ads-mcp"
217
+ )
218
+ parser.add_argument("--login", action="store_true", help="Authenticate with Meta and store the token")
219
+ parser.add_argument("--app-id", type=str, help="Meta App ID (Client ID) for authentication")
220
+ parser.add_argument("--version", action="store_true", help="Show the version of the package")
221
+
222
+ # Transport configuration arguments
223
+ parser.add_argument("--transport", type=str, choices=["stdio", "streamable-http"],
224
+ default="stdio",
225
+ help="Transport method: 'stdio' for MCP clients (default), 'streamable-http' for HTTP API access")
226
+ parser.add_argument("--port", type=int, default=settings.server_port,
227
+ help="Port for Streamable HTTP transport (only used with --transport streamable-http)")
228
+ parser.add_argument("--host", type=str, default=settings.server_host,
229
+ help="Host for Streamable HTTP transport (only used with --transport streamable-http)")
230
+ parser.add_argument("--sse-response", action="store_true",
231
+ help="Use SSE response format instead of JSON (default: JSON, only used with --transport streamable-http)")
232
+
233
+ args = parser.parse_args()
234
+ logger.debug(f"Parsed args: login={args.login}, app_id={args.app_id}, version={args.version}")
235
+ logger.debug(f"Transport args: transport={args.transport}, port={args.port}, host={args.host}, sse_response={args.sse_response}")
236
+
237
+ # Validate CLI argument combinations
238
+ if args.transport == "stdio" and (args.port != 8080 or args.host != "localhost" or args.sse_response):
239
+ logger.warning("HTTP transport arguments (--port, --host, --sse-response) are ignored when using stdio transport")
240
+ print("Warning: HTTP transport arguments are ignored when using stdio transport")
241
+
242
+ # Update app ID if provided as environment variable or command line arg
243
+ from .auth import auth_manager, meta_config
244
+
245
+ # Check environment variable first (early init)
246
+ env_app_id = os.environ.get("META_APP_ID")
247
+ if env_app_id:
248
+ logger.debug(f"Found META_APP_ID in environment: {env_app_id}")
249
+ else:
250
+ logger.warning("META_APP_ID not found in environment variables")
251
+
252
+ # Command line takes precedence
253
+ if args.app_id:
254
+ logger.info(f"Setting app_id from command line: {args.app_id}")
255
+ auth_manager.app_id = args.app_id
256
+ meta_config.set_app_id(args.app_id)
257
+ elif env_app_id:
258
+ logger.info(f"Setting app_id from environment: {env_app_id}")
259
+ auth_manager.app_id = env_app_id
260
+ meta_config.set_app_id(env_app_id)
261
+
262
+ # Log the final app ID that will be used
263
+ logger.info(f"Final app_id from meta_config: {meta_config.get_app_id()}")
264
+ logger.info(f"Final app_id from auth_manager: {auth_manager.app_id}")
265
+ logger.info(f"ENV META_APP_ID: {os.environ.get('META_APP_ID')}")
266
+
267
+ # Show version if requested
268
+ if args.version:
269
+ from meta_ads_mcp import __version__
270
+ logger.info(f"Displaying version: {__version__}")
271
+ print(f"Meta Ads MCP v{__version__}")
272
+ return 0
273
+
274
+ # Handle login command
275
+ if args.login:
276
+ login_cli()
277
+ return 0
278
+
279
+ # Check for Pipeboard authentication and token
280
+ pipeboard_api_token = os.environ.get("PIPEBOARD_API_TOKEN")
281
+ if pipeboard_api_token:
282
+ logger.info("Using Pipeboard authentication")
283
+ print(" Pipeboard authentication enabled")
284
+ print(f" API token: {pipeboard_api_token[:8]}...{pipeboard_api_token[-4:]}")
285
+ # Check for existing token
286
+ token = pipeboard_auth_manager.get_access_token()
287
+ if not token:
288
+ logger.info("No valid Pipeboard token found. Initiating browser-based authentication flow.")
289
+ print("No valid Meta token found. Opening browser for authentication...")
290
+ try:
291
+ # Initialize the auth flow and get the login URL
292
+ auth_data = pipeboard_auth_manager.initiate_auth_flow()
293
+ login_url = auth_data.get('loginUrl')
294
+ if login_url:
295
+ logger.info(f"Opening browser with login URL: {login_url}")
296
+ webbrowser.open(login_url)
297
+ print("Please authorize the application in your browser.")
298
+ print("After authorization, the token will be automatically retrieved.")
299
+ print("Waiting for authentication to complete...")
300
+
301
+ # Poll for token completion
302
+ max_attempts = 30 # Try for 30 * 2 = 60 seconds
303
+ for attempt in range(max_attempts):
304
+ print(f"Waiting for authentication... ({attempt+1}/{max_attempts})")
305
+ # Try to get the token again
306
+ token = pipeboard_auth_manager.get_access_token(force_refresh=True)
307
+ if token:
308
+ print("Authentication successful!")
309
+ break
310
+ time.sleep(2) # Wait 2 seconds between attempts
311
+
312
+ if not token:
313
+ print("Authentication timed out. Starting server anyway.")
314
+ print("You may need to restart the server after completing authentication.")
315
+ else:
316
+ logger.error("No login URL received from Pipeboard API")
317
+ print("Error: Could not get authentication URL. Check your API token.")
318
+ except Exception as e:
319
+ logger.error(f"Error initiating browser-based authentication: {e}")
320
+ print(f"Error: Could not start authentication: {e}")
321
+ else:
322
+ print(f" Valid Pipeboard access token found")
323
+ print(f" Token preview: {token[:10]}...{token[-5:]}")
324
+
325
+ # Transport-specific server initialization and startup
326
+ if args.transport == "streamable-http":
327
+ logger.info(f"Starting MCP server with Streamable HTTP transport on {args.host}:{args.port}")
328
+ logger.info("Mode: Stateless (no session persistence)")
329
+ logger.info(f"Response format: {'SSE' if args.sse_response else 'JSON'}")
330
+ logger.info("Primary auth method: Bearer Token (recommended)")
331
+ logger.info("Fallback auth method: Custom Meta App OAuth (complex setup)")
332
+
333
+ print(f"Starting Meta Ads MCP server with Streamable HTTP transport")
334
+ print(f"Server will listen on {args.host}:{args.port}")
335
+ print(f"Response format: {'SSE' if args.sse_response else 'JSON'}")
336
+ print("Primary authentication: Bearer Token (via Authorization: Bearer <token> header)")
337
+ print("Fallback authentication: Custom Meta App OAuth (via X-META-APP-ID header)")
338
+
339
+ # Configure the existing server with streamable HTTP settings
340
+ mcp_server.settings.host = args.host
341
+ mcp_server.settings.port = args.port
342
+ mcp_server.settings.stateless_http = True
343
+ mcp_server.settings.json_response = not args.sse_response
344
+
345
+ # Import all tool modules to ensure they are registered
346
+ logger.info("Ensuring all tools are registered for HTTP transport")
347
+ from . import accounts, campaigns, adsets, ads, insights, authentication
348
+ from . import ads_library, budget_schedules, reports, openai_deep_research
349
+ from . import mcc
350
+
351
+ # NEW: Setup HTTP authentication middleware
352
+ logger.info("Setting up HTTP authentication middleware")
353
+ try:
354
+ from .http_auth_integration import setup_fastmcp_http_auth
355
+
356
+ # Setup the FastMCP HTTP auth integration
357
+ setup_fastmcp_http_auth(mcp_server)
358
+ logger.info("FastMCP HTTP authentication integration setup successful")
359
+ print(" FastMCP HTTP authentication integration enabled")
360
+ print(" - Bearer tokens via Authorization: Bearer <token> header")
361
+ print(" - Direct Meta tokens via X-META-ACCESS-TOKEN header")
362
+
363
+ except Exception as e:
364
+ logger.error(f"Failed to setup FastMCP HTTP authentication integration: {e}")
365
+ print(f" FastMCP HTTP authentication integration setup failed: {e}")
366
+ print(" Server will still start but may not support header-based auth")
367
+
368
+ # Log final server configuration
369
+ logger.info(f"FastMCP server configured with:")
370
+ logger.info(f" - Host: {mcp_server.settings.host}")
371
+ logger.info(f" - Port: {mcp_server.settings.port}")
372
+ logger.info(f" - Stateless HTTP: {mcp_server.settings.stateless_http}")
373
+ logger.info(f" - JSON Response: {mcp_server.settings.json_response}")
374
+ logger.info(f" - Streamable HTTP Path: {mcp_server.settings.streamable_http_path}")
375
+
376
+ # Start the FastMCP server with Streamable HTTP transport
377
+ try:
378
+ logger.info("Starting FastMCP server with Streamable HTTP transport")
379
+ print(f" Server configured successfully")
380
+ print(f" URL: http://{args.host}:{args.port}{mcp_server.settings.streamable_http_path}/")
381
+ print(f" Mode: {'Stateless' if mcp_server.settings.stateless_http else 'Stateful'}")
382
+ print(f" Format: {'JSON' if mcp_server.settings.json_response else 'SSE'}")
383
+ mcp_server.run(transport="streamable-http")
384
+ except Exception as e:
385
+ logger.error(f"Error starting Streamable HTTP server: {e}")
386
+ print(f"Error: Failed to start Streamable HTTP server: {e}")
387
+ return 1
388
+ else:
389
+ # Default stdio transport
390
+ logger.info("Starting MCP server with stdio transport")
391
+ mcp_server.run(transport='stdio')