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 +1 -1
- meta_ads_mcp/core/api.py +10 -2
- meta_ads_mcp/core/authentication.py +1 -1
- meta_ads_mcp/core/campaigns.py +5 -1
- meta_ads_mcp/core/http_auth_integration.py +248 -0
- meta_ads_mcp/core/server.py +260 -8
- meta_ads_mcp/core/utils.py +6 -2
- {meta_ads_mcp-0.3.6.dist-info → meta_ads_mcp-0.3.8.dist-info}/METADATA +94 -191
- {meta_ads_mcp-0.3.6.dist-info → meta_ads_mcp-0.3.8.dist-info}/RECORD +12 -12
- meta_ads_mcp/api.py +0 -2087
- {meta_ads_mcp-0.3.6.dist-info → meta_ads_mcp-0.3.8.dist-info}/WHEEL +0 -0
- {meta_ads_mcp-0.3.6.dist-info → meta_ads_mcp-0.3.8.dist-info}/entry_points.txt +0 -0
- {meta_ads_mcp-0.3.6.dist-info → meta_ads_mcp-0.3.8.dist-info}/licenses/LICENSE +0 -0
meta_ads_mcp/__init__.py
CHANGED
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
|
-
"
|
|
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
|
|
meta_ads_mcp/core/campaigns.py
CHANGED
|
@@ -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.")
|
meta_ads_mcp/core/server.py
CHANGED
|
@@ -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.
|
|
38
|
-
logger.
|
|
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(
|
|
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.
|
|
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.
|
|
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
|
-
#
|
|
128
|
-
|
|
129
|
-
|
|
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')
|
meta_ads_mcp/core/utils.py
CHANGED
|
@@ -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.
|
|
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.
|
|
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():
|