meta-ads-mcp 0.3.6__py3-none-any.whl → 0.3.8__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.6"
10
+ __version__ = "0.3.8"
11
11
 
12
12
  __all__ = [
13
13
  'get_ad_accounts',
meta_ads_mcp/core/api.py CHANGED
@@ -206,6 +206,9 @@ def meta_api_tool(func):
206
206
  if (auth_manager.app_id == "YOUR_META_APP_ID" or not auth_manager.app_id) and not auth_manager.use_pipeboard:
207
207
  logger.error("TOKEN VALIDATION FAILED: No valid app_id configured")
208
208
  logger.error("Please set META_APP_ID environment variable or configure in your code")
209
+ elif auth_manager.use_pipeboard:
210
+ logger.error("TOKEN VALIDATION FAILED: Pipeboard authentication enabled but no valid token available")
211
+ logger.error("Complete authentication via Pipeboard service or check PIPEBOARD_API_TOKEN")
209
212
  else:
210
213
  logger.error("Check logs above for detailed token validation failures")
211
214
  except Exception as e:
@@ -221,16 +224,21 @@ def meta_api_tool(func):
221
224
  # Add more specific troubleshooting information
222
225
  auth_url = auth_manager.get_auth_url()
223
226
  app_id = auth_manager.app_id
227
+ using_pipeboard = auth_manager.use_pipeboard
224
228
 
225
229
  logger.error("TOKEN VALIDATION SUMMARY:")
226
230
  logger.error(f"- Current app_id: '{app_id}'")
227
231
  logger.error(f"- Environment META_APP_ID: '{os.environ.get('META_APP_ID', 'Not set')}'")
228
232
  logger.error(f"- Pipeboard API token configured: {'Yes' if os.environ.get('PIPEBOARD_API_TOKEN') else 'No'}")
233
+ logger.error(f"- Using Pipeboard authentication: {'Yes' if using_pipeboard else 'No'}")
229
234
 
230
- # Check for common configuration issues
231
- if app_id == "YOUR_META_APP_ID" or not app_id:
235
+ # Check for common configuration issues - but only if not using Pipeboard
236
+ if not using_pipeboard and (app_id == "YOUR_META_APP_ID" or not app_id):
232
237
  logger.error("ISSUE DETECTED: No valid Meta App ID configured")
233
238
  logger.error("ACTION REQUIRED: Set META_APP_ID environment variable with a valid App ID")
239
+ elif using_pipeboard:
240
+ logger.error("ISSUE DETECTED: Pipeboard authentication configured but no valid token available")
241
+ logger.error("ACTION REQUIRED: Complete authentication via Pipeboard service")
234
242
 
235
243
  return json.dumps({
236
244
  "error": {
@@ -116,7 +116,7 @@ async def get_login_link(access_token: str = None) -> str:
116
116
  "authentication_method": "meta_oauth",
117
117
  "token_exchange_message": f"Your authentication token will be valid for approximately {token_duration}." +
118
118
  (" Long-lived token exchange is enabled." if token_exchange_supported else
119
- " To enable long-lived tokens (60 days), set the META_APP_SECRET environment variable."),
119
+ " For direct Meta authentication, long-lived tokens require META_APP_SECRET. Consider using Pipeboard authentication instead (60-day tokens by default)."),
120
120
  "note": "After authenticating, the token will be automatically saved."
121
121
  }
122
122
 
@@ -9,7 +9,7 @@ from .server import mcp_server
9
9
 
10
10
  @mcp_server.tool()
11
11
  @meta_api_tool
12
- async def get_campaigns(access_token: str = None, account_id: str = None, limit: int = 10, status_filter: str = "") -> str:
12
+ async def get_campaigns(access_token: str = None, account_id: str = None, limit: int = 10, status_filter: str = "", after: str = "") -> str:
13
13
  """
14
14
  Get campaigns for a Meta Ads account with optional filtering.
15
15
 
@@ -26,6 +26,7 @@ async def get_campaigns(access_token: str = None, account_id: str = None, limit:
26
26
  status_filter: Filter by effective status (e.g., 'ACTIVE', 'PAUSED', 'ARCHIVED').
27
27
  Maps to the 'effective_status' API parameter, which expects an array
28
28
  (this function handles the required JSON formatting). Leave empty for all statuses.
29
+ after: Pagination cursor to get the next set of results
29
30
  """
30
31
  # If no account ID is specified, try to get the first one for the user
31
32
  if not account_id:
@@ -47,6 +48,9 @@ async def get_campaigns(access_token: str = None, account_id: str = None, limit:
47
48
  # API expects an array, encode it as a JSON string
48
49
  params["effective_status"] = json.dumps([status_filter])
49
50
 
51
+ if after:
52
+ params["after"] = after
53
+
50
54
  data = await make_api_request(endpoint, access_token, params)
51
55
 
52
56
  return json.dumps(data, indent=2)
@@ -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.")
@@ -5,6 +5,8 @@ import argparse
5
5
  import os
6
6
  import sys
7
7
  import webbrowser
8
+ import json
9
+ from typing import Dict, Any, Optional
8
10
  from .auth import login as login_auth
9
11
  from .resources import list_resources, get_resource
10
12
  from .utils import logger
@@ -19,6 +21,173 @@ mcp_server.resource(uri="meta-ads://resources")(list_resources)
19
21
  mcp_server.resource(uri="meta-ads://images/{resource_id}")(get_resource)
20
22
 
21
23
 
24
+ class StreamableHTTPHandler:
25
+ """Handles stateless Streamable HTTP requests for Meta Ads MCP"""
26
+
27
+ def __init__(self):
28
+ """Initialize handler with no session storage - all auth per request"""
29
+ logger.debug("StreamableHTTPHandler initialized for stateless operation")
30
+
31
+ def handle_request(self, request_headers: Dict[str, str], request_body: Dict[str, Any]) -> Dict[str, Any]:
32
+ """Handle individual request with authentication
33
+
34
+ Args:
35
+ request_headers: HTTP request headers
36
+ request_body: JSON-RPC request body
37
+
38
+ Returns:
39
+ JSON response with auth status and any tool results
40
+ """
41
+ try:
42
+ # Extract authentication configuration from headers
43
+ auth_config = self.get_auth_config_from_headers(request_headers)
44
+ logger.debug(f"Auth method detected: {auth_config['auth_method']}")
45
+
46
+ # Handle based on auth method
47
+ if auth_config['auth_method'] == 'bearer':
48
+ return self.handle_bearer_request(auth_config, request_body)
49
+ elif auth_config['auth_method'] == 'custom_meta_app':
50
+ return self.handle_custom_app_request(auth_config, request_body)
51
+ else:
52
+ return self.handle_unauthenticated_request(request_body)
53
+
54
+ except Exception as e:
55
+ logger.error(f"Error handling request: {e}")
56
+ return {
57
+ 'jsonrpc': '2.0',
58
+ 'error': {
59
+ 'code': -32603,
60
+ 'message': 'Internal error',
61
+ 'data': str(e)
62
+ },
63
+ 'id': request_body.get('id')
64
+ }
65
+
66
+ def get_auth_config_from_headers(self, request_headers: Dict[str, str]) -> Dict[str, Any]:
67
+ """Extract authentication configuration from HTTP headers
68
+
69
+ Args:
70
+ request_headers: HTTP request headers
71
+
72
+ Returns:
73
+ Dictionary with auth method and relevant credentials
74
+ """
75
+ # Security validation - only allow safe headers
76
+ ALLOWED_VIA_HEADERS = {
77
+ 'pipeboard_api_token': True, # ✅ Primary method - simple and secure
78
+ 'meta_app_id': True, # ✅ Fallback only - triggers OAuth complexity
79
+ 'meta_app_secret': False, # ❌ Server environment only
80
+ 'meta_access_token': False, # ❌ Use proper auth flows instead
81
+ }
82
+
83
+ # PRIMARY: Check for Bearer token in Authorization header (handles 90%+ of cases)
84
+ auth_header = request_headers.get('Authorization') or request_headers.get('authorization')
85
+ if auth_header and auth_header.lower().startswith('bearer '):
86
+ token = auth_header[7:].strip()
87
+ logger.info("Bearer authentication detected (primary path)")
88
+ return {
89
+ 'auth_method': 'bearer',
90
+ 'bearer_token': token,
91
+ 'requires_oauth': False # Simple token-based auth
92
+ }
93
+
94
+ # FALLBACK: Custom Meta app (minority of users)
95
+ meta_app_id = request_headers.get('X-META-APP-ID') or request_headers.get('x-meta-app-id')
96
+ if meta_app_id:
97
+ logger.debug("Custom Meta app authentication detected (fallback path)")
98
+ return {
99
+ 'auth_method': 'custom_meta_app',
100
+ 'meta_app_id': meta_app_id,
101
+ 'requires_oauth': True # Complex OAuth flow required
102
+ }
103
+
104
+ # No authentication provided
105
+ logger.warning("No authentication method detected in headers")
106
+ return {
107
+ 'auth_method': 'none',
108
+ 'requires_oauth': False
109
+ }
110
+
111
+ def handle_bearer_request(self, auth_config: Dict[str, Any], request_body: Dict[str, Any]) -> Dict[str, Any]:
112
+ """Handle request with Bearer token (primary path)
113
+
114
+ Args:
115
+ auth_config: Authentication configuration from headers
116
+ request_body: JSON-RPC request body
117
+
118
+ Returns:
119
+ JSON response ready for tool execution
120
+ """
121
+ logger.debug("Processing Bearer authenticated request")
122
+ token = auth_config['bearer_token']
123
+
124
+ # Token is ready to use immediately for API calls
125
+ # TODO: In next phases, this will execute the actual tool call
126
+ return {
127
+ 'jsonrpc': '2.0',
128
+ 'result': {
129
+ 'status': 'ready',
130
+ 'auth_method': 'bearer',
131
+ 'message': 'Authentication successful with Bearer token',
132
+ 'token_source': 'bearer_header'
133
+ },
134
+ 'id': request_body.get('id')
135
+ }
136
+
137
+ def handle_custom_app_request(self, auth_config: Dict[str, Any], request_body: Dict[str, Any]) -> Dict[str, Any]:
138
+ """Handle request with custom Meta app (fallback path)
139
+
140
+ Args:
141
+ auth_config: Authentication configuration from headers
142
+ request_body: JSON-RPC request body
143
+
144
+ Returns:
145
+ JSON response indicating OAuth flow is required
146
+ """
147
+ logger.debug("Processing custom Meta app request (OAuth required)")
148
+
149
+ # This may require OAuth flow initiation
150
+ # Each request is independent - no session state
151
+ return {
152
+ 'jsonrpc': '2.0',
153
+ 'result': {
154
+ 'status': 'oauth_required',
155
+ 'auth_method': 'custom_meta_app',
156
+ 'meta_app_id': auth_config['meta_app_id'],
157
+ 'message': 'OAuth flow required for custom Meta app authentication',
158
+ 'next_steps': 'Use get_login_link tool to initiate OAuth flow'
159
+ },
160
+ 'id': request_body.get('id')
161
+ }
162
+
163
+ def handle_unauthenticated_request(self, request_body: Dict[str, Any]) -> Dict[str, Any]:
164
+ """Handle request with no authentication
165
+
166
+ Args:
167
+ request_body: JSON-RPC request body
168
+
169
+ Returns:
170
+ JSON error response requesting authentication
171
+ """
172
+ logger.warning("Unauthenticated request received")
173
+
174
+ return {
175
+ 'jsonrpc': '2.0',
176
+ 'error': {
177
+ 'code': -32600,
178
+ 'message': 'Authentication required',
179
+ 'data': {
180
+ 'supported_methods': [
181
+ 'Authorization: Bearer <token> (recommended)',
182
+ 'X-META-APP-ID: Custom Meta app OAuth (advanced users)'
183
+ ],
184
+ 'documentation': 'https://github.com/pipeboard-co/meta-ads-mcp'
185
+ }
186
+ },
187
+ 'id': request_body.get('id')
188
+ }
189
+
190
+
22
191
  def login_cli():
23
192
  """
24
193
  Command-line function to authenticate with Meta
@@ -34,17 +203,37 @@ def main():
34
203
  """Main entry point for the package"""
35
204
  # Log startup information
36
205
  logger.info("Meta Ads MCP server starting")
37
- logger.info(f"Python version: {sys.version}")
38
- logger.info(f"Args: {sys.argv}")
206
+ logger.debug(f"Python version: {sys.version}")
207
+ logger.debug(f"Args: {sys.argv}")
39
208
 
40
209
  # Initialize argument parser
41
- parser = argparse.ArgumentParser(description="Meta Ads MCP Server")
210
+ parser = argparse.ArgumentParser(
211
+ description="Meta Ads MCP Server - Model Context Protocol server for Meta Ads API",
212
+ epilog="For more information, see https://github.com/pipeboard-co/meta-ads-mcp"
213
+ )
42
214
  parser.add_argument("--login", action="store_true", help="Authenticate with Meta and store the token")
43
215
  parser.add_argument("--app-id", type=str, help="Meta App ID (Client ID) for authentication")
44
216
  parser.add_argument("--version", action="store_true", help="Show the version of the package")
45
217
 
218
+ # Transport configuration arguments
219
+ parser.add_argument("--transport", type=str, choices=["stdio", "streamable-http"],
220
+ default="stdio",
221
+ help="Transport method: 'stdio' for MCP clients (default), 'streamable-http' for HTTP API access")
222
+ parser.add_argument("--port", type=int, default=8080,
223
+ help="Port for Streamable HTTP transport (default: 8080, only used with --transport streamable-http)")
224
+ parser.add_argument("--host", type=str, default="localhost",
225
+ help="Host for Streamable HTTP transport (default: localhost, only used with --transport streamable-http)")
226
+ parser.add_argument("--sse-response", action="store_true",
227
+ help="Use SSE response format instead of JSON (default: JSON, only used with --transport streamable-http)")
228
+
46
229
  args = parser.parse_args()
47
- logger.info(f"Parsed args: login={args.login}, app_id={args.app_id}, version={args.version}")
230
+ logger.debug(f"Parsed args: login={args.login}, app_id={args.app_id}, version={args.version}")
231
+ logger.debug(f"Transport args: transport={args.transport}, port={args.port}, host={args.host}, sse_response={args.sse_response}")
232
+
233
+ # Validate CLI argument combinations
234
+ if args.transport == "stdio" and (args.port != 8080 or args.host != "localhost" or args.sse_response):
235
+ logger.warning("HTTP transport arguments (--port, --host, --sse-response) are ignored when using stdio transport")
236
+ print("Warning: HTTP transport arguments are ignored when using stdio transport")
48
237
 
49
238
  # Update app ID if provided as environment variable or command line arg
50
239
  from .auth import auth_manager, meta_config
@@ -52,7 +241,7 @@ def main():
52
241
  # Check environment variable first (early init)
53
242
  env_app_id = os.environ.get("META_APP_ID")
54
243
  if env_app_id:
55
- logger.info(f"Found META_APP_ID in environment: {env_app_id}")
244
+ logger.debug(f"Found META_APP_ID in environment: {env_app_id}")
56
245
  else:
57
246
  logger.warning("META_APP_ID not found in environment variables")
58
247
 
@@ -124,6 +313,69 @@ def main():
124
313
  logger.error(f"Error initiating browser-based authentication: {e}")
125
314
  print(f"Error: Could not start authentication: {e}")
126
315
 
127
- # Initialize and run the server
128
- logger.info("Starting MCP server with stdio transport")
129
- mcp_server.run(transport='stdio')
316
+ # Transport-specific server initialization and startup
317
+ if args.transport == "streamable-http":
318
+ logger.info(f"Starting MCP server with Streamable HTTP transport on {args.host}:{args.port}")
319
+ logger.info("Mode: Stateless (no session persistence)")
320
+ logger.info(f"Response format: {'SSE' if args.sse_response else 'JSON'}")
321
+ logger.info("Primary auth method: Bearer Token (recommended)")
322
+ logger.info("Fallback auth method: Custom Meta App OAuth (complex setup)")
323
+
324
+ print(f"Starting Meta Ads MCP server with Streamable HTTP transport")
325
+ print(f"Server will listen on {args.host}:{args.port}")
326
+ print(f"Response format: {'SSE' if args.sse_response else 'JSON'}")
327
+ print("Primary authentication: Bearer Token (via Authorization: Bearer <token> header)")
328
+ print("Fallback authentication: Custom Meta App OAuth (via X-META-APP-ID header)")
329
+
330
+ # Configure the existing server with streamable HTTP settings
331
+ mcp_server.settings.host = args.host
332
+ mcp_server.settings.port = args.port
333
+ mcp_server.settings.stateless_http = True
334
+ mcp_server.settings.json_response = not args.sse_response
335
+
336
+ # Import all tool modules to ensure they are registered
337
+ logger.info("Ensuring all tools are registered for HTTP transport")
338
+ from . import accounts, campaigns, adsets, ads, insights, authentication
339
+ from . import ads_library, budget_schedules
340
+
341
+ # ✅ NEW: Setup HTTP authentication middleware
342
+ logger.info("Setting up HTTP authentication middleware")
343
+ try:
344
+ from .http_auth_integration import setup_fastmcp_http_auth
345
+
346
+ # Setup the FastMCP HTTP auth integration
347
+ setup_fastmcp_http_auth(mcp_server)
348
+ logger.info("FastMCP HTTP authentication integration setup successful")
349
+ print("✅ FastMCP HTTP authentication integration enabled")
350
+ print(" - Bearer tokens via Authorization: Bearer <token> header")
351
+ print(" - Direct Meta tokens via X-META-ACCESS-TOKEN header")
352
+
353
+ except Exception as e:
354
+ logger.error(f"Failed to setup FastMCP HTTP authentication integration: {e}")
355
+ print(f"⚠️ FastMCP HTTP authentication integration setup failed: {e}")
356
+ print(" Server will still start but may not support header-based auth")
357
+
358
+ # Log final server configuration
359
+ logger.info(f"FastMCP server configured with:")
360
+ logger.info(f" - Host: {mcp_server.settings.host}")
361
+ logger.info(f" - Port: {mcp_server.settings.port}")
362
+ logger.info(f" - Stateless HTTP: {mcp_server.settings.stateless_http}")
363
+ logger.info(f" - JSON Response: {mcp_server.settings.json_response}")
364
+ logger.info(f" - Streamable HTTP Path: {mcp_server.settings.streamable_http_path}")
365
+
366
+ # Start the FastMCP server with Streamable HTTP transport
367
+ try:
368
+ logger.info("Starting FastMCP server with Streamable HTTP transport")
369
+ print(f"✅ Server configured successfully")
370
+ print(f" URL: http://{args.host}:{args.port}{mcp_server.settings.streamable_http_path}/")
371
+ print(f" Mode: {'Stateless' if mcp_server.settings.stateless_http else 'Stateful'}")
372
+ print(f" Format: {'JSON' if mcp_server.settings.json_response else 'SSE'}")
373
+ mcp_server.run(transport="streamable-http")
374
+ except Exception as e:
375
+ logger.error(f"Error starting Streamable HTTP server: {e}")
376
+ print(f"Error: Failed to start Streamable HTTP server: {e}")
377
+ return 1
378
+ else:
379
+ # Default stdio transport
380
+ logger.info("Starting MCP server with stdio transport")
381
+ mcp_server.run(transport='stdio')
@@ -24,9 +24,13 @@ using_pipeboard = bool(os.environ.get("PIPEBOARD_API_TOKEN", ""))
24
24
  # Print warning if Meta app credentials are not configured and not using Pipeboard
25
25
  if not using_pipeboard:
26
26
  if not META_APP_ID:
27
- print("WARNING: META_APP_ID environment variable is not set. Authentication will not work properly.")
27
+ print("WARNING: META_APP_ID environment variable is not set.")
28
+ print("RECOMMENDED: Use Pipeboard authentication by setting PIPEBOARD_API_TOKEN instead.")
29
+ print("ALTERNATIVE: For direct Meta authentication, set META_APP_ID to your Meta App ID.")
28
30
  if not META_APP_SECRET:
29
- print("WARNING: META_APP_SECRET environment variable is not set. Long-lived token exchange will not work.")
31
+ print("WARNING: META_APP_SECRET environment variable is not set.")
32
+ print("NOTE: This is only needed for direct Meta authentication. Pipeboard authentication doesn't require this.")
33
+ print("RECOMMENDED: Use Pipeboard authentication by setting PIPEBOARD_API_TOKEN instead.")
30
34
 
31
35
  # Configure logging to file
32
36
  def setup_logging():