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,225 @@
1
+ """Utility functions for Meta Ads API."""
2
+
3
+ import os
4
+ import pathlib
5
+ import platform
6
+ import sys
7
+ from typing import Any, Dict, List, Optional
8
+
9
+ import httpx
10
+ from loguru import logger
11
+ from PIL import Image as PILImage # noqa: F401 (re-exported for callers)
12
+
13
+ # ---------------------------------------------------------------------------
14
+ # Logging configuration (loguru)
15
+ # ---------------------------------------------------------------------------
16
+ # Remove the default stderr sink so we can control format and level precisely.
17
+ logger.remove()
18
+
19
+ _log_level = os.environ.get("LOG_LEVEL", "INFO").upper()
20
+
21
+ # Console sink: WARNING+ only -- keeps stdio MCP transport clean.
22
+ logger.add(sys.stderr, level="WARNING", format="{time} | {level} | {message}")
23
+
24
+
25
+ def _log_file_path() -> pathlib.Path:
26
+ """Return a platform-appropriate log file path, creating the directory."""
27
+ if platform.system() == "Windows":
28
+ base = pathlib.Path(os.environ.get("APPDATA", str(pathlib.Path.home())))
29
+ elif platform.system() == "Darwin":
30
+ base = pathlib.Path.home() / "Library" / "Application Support"
31
+ else:
32
+ base = pathlib.Path.home() / ".config"
33
+ log_dir = base / "meta-ads-mcp"
34
+ log_dir.mkdir(parents=True, exist_ok=True)
35
+ return log_dir / "meta_ads_debug.log"
36
+
37
+
38
+ # File sink: full detail for troubleshooting.
39
+ logger.add(
40
+ _log_file_path(),
41
+ level=_log_level,
42
+ rotation="10 MB",
43
+ retention="7 days",
44
+ format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name}:{line} | {message}",
45
+ )
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # Convenience env-var constants (kept for backwards-compat with callers)
49
+ # ---------------------------------------------------------------------------
50
+ META_APP_ID: str = os.environ.get("META_APP_ID", "")
51
+ META_APP_SECRET: str = os.environ.get("META_APP_SECRET", "")
52
+
53
+ logger.info("meta-ads-mcp logging initialised (level={})", _log_level)
54
+ logger.info("Platform: {} {}", platform.system(), platform.release())
55
+ logger.info(
56
+ "Auth mode: {}",
57
+ "pipeboard" if os.environ.get("PIPEBOARD_API_TOKEN") else "direct-meta",
58
+ )
59
+
60
+ # ---------------------------------------------------------------------------
61
+ # Global image store (used by resources.py)
62
+ # ---------------------------------------------------------------------------
63
+ ad_creative_images: Dict[str, Any] = {}
64
+
65
+
66
+ # ---------------------------------------------------------------------------
67
+ # Creative image URL extraction
68
+ # ---------------------------------------------------------------------------
69
+
70
+
71
+ def extract_creative_image_urls(creative: Dict[str, Any]) -> List[str]:
72
+ """Extract image URLs from a creative object, ordered by quality.
73
+
74
+ Args:
75
+ creative: Meta Ads creative object.
76
+
77
+ Returns:
78
+ Deduplicated list of image URLs, highest quality first.
79
+ """
80
+ image_urls: List[str] = []
81
+
82
+ # 1. image_urls_for_viewing (highest quality)
83
+ image_urls.extend(creative.get("image_urls_for_viewing") or [])
84
+
85
+ # 2. Direct image_url field
86
+ if creative.get("image_url"):
87
+ image_urls.append(creative["image_url"])
88
+
89
+ # 3. object_story_spec
90
+ story_spec = creative.get("object_story_spec", {})
91
+ link_data = story_spec.get("link_data", {})
92
+ for field in ("picture", "image_url"):
93
+ if link_data.get(field):
94
+ image_urls.append(link_data[field])
95
+ video_data = story_spec.get("video_data", {})
96
+ if video_data.get("image_url"):
97
+ image_urls.append(video_data["image_url"])
98
+
99
+ # 4. asset_feed_spec images
100
+ for img in (creative.get("asset_feed_spec") or {}).get("images", []):
101
+ if img.get("url"):
102
+ image_urls.append(img["url"])
103
+
104
+ # 5. thumbnail_url (lowest priority)
105
+ if creative.get("thumbnail_url"):
106
+ image_urls.append(creative["thumbnail_url"])
107
+
108
+ # Deduplicate, preserving order
109
+ seen: set = set()
110
+ unique: List[str] = []
111
+ for url in image_urls:
112
+ if url not in seen:
113
+ seen.add(url)
114
+ unique.append(url)
115
+ return unique
116
+
117
+
118
+ # ---------------------------------------------------------------------------
119
+ # Image download helpers
120
+ # ---------------------------------------------------------------------------
121
+
122
+
123
+ async def download_image(url: str) -> Optional[bytes]:
124
+ """Download an image from a URL.
125
+
126
+ Args:
127
+ url: Image URL.
128
+
129
+ Returns:
130
+ Raw image bytes, or None on failure.
131
+ """
132
+ headers = {"User-Agent": "curl/8.4.0", "Accept": "*/*"}
133
+ try:
134
+ async with httpx.AsyncClient(follow_redirects=True, timeout=30.0) as client:
135
+ response = await client.get(url, headers=headers)
136
+ if response.status_code == 200:
137
+ logger.debug("Downloaded {} bytes from {}", len(response.content), url)
138
+ return response.content
139
+ logger.warning("Image download failed: HTTP {} for {}", response.status_code, url)
140
+ except httpx.HTTPStatusError as exc:
141
+ logger.warning("HTTP error downloading {}: {}", url, exc)
142
+ except httpx.RequestError as exc:
143
+ logger.warning("Request error downloading {}: {}", url, exc)
144
+ except Exception:
145
+ logger.exception("Unexpected error downloading {}", url)
146
+ return None
147
+
148
+
149
+ async def try_multiple_download_methods(url: str) -> Optional[bytes]:
150
+ """Try multiple strategies to download a Meta CDN image.
151
+
152
+ Args:
153
+ url: Image URL.
154
+
155
+ Returns:
156
+ Raw image bytes, or None if all methods fail.
157
+ """
158
+ # Method 1: direct curl-style request
159
+ data = await download_image(url)
160
+ if data:
161
+ return data
162
+
163
+ logger.debug("Direct download failed, trying browser UA for {}", url)
164
+
165
+ # Method 2: browser User-Agent
166
+ try:
167
+ headers = {
168
+ "User-Agent": (
169
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
170
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
171
+ "Chrome/91.0.4472.124 Safari/537.36"
172
+ ),
173
+ "Accept": "image/webp,image/apng,image/*,*/*;q=0.8",
174
+ }
175
+ async with httpx.AsyncClient(follow_redirects=True, timeout=30.0) as client:
176
+ response = await client.get(url, headers=headers)
177
+ response.raise_for_status()
178
+ logger.debug("Browser-UA download succeeded: {} bytes", len(response.content))
179
+ return response.content
180
+ except Exception as exc:
181
+ logger.debug("Browser-UA download failed for {}: {}", url, exc)
182
+
183
+ # Method 3: seed cookies via a Facebook visit first
184
+ try:
185
+ async with httpx.AsyncClient(follow_redirects=True, timeout=30.0) as client:
186
+ await client.get("https://www.facebook.com/", timeout=30.0)
187
+ response = await client.get(url, timeout=30.0)
188
+ response.raise_for_status()
189
+ logger.debug("Facebook-session download succeeded: {} bytes", len(response.content))
190
+ return response.content
191
+ except Exception as exc:
192
+ logger.debug("Facebook-session download failed for {}: {}", url, exc)
193
+
194
+ return None
195
+
196
+
197
+ # ---------------------------------------------------------------------------
198
+ # Resource creation helper
199
+ # ---------------------------------------------------------------------------
200
+
201
+
202
+ def create_resource_from_image(
203
+ image_bytes: bytes, resource_id: str, name: str
204
+ ) -> Dict[str, Any]:
205
+ """Store image bytes and return a resource descriptor.
206
+
207
+ Args:
208
+ image_bytes: Raw image data.
209
+ resource_id: Unique identifier for the resource.
210
+ name: Human-readable name.
211
+
212
+ Returns:
213
+ Resource descriptor dict with uri and size.
214
+ """
215
+ ad_creative_images[resource_id] = {
216
+ "data": image_bytes,
217
+ "mime_type": "image/jpeg",
218
+ "name": name,
219
+ }
220
+ return {
221
+ "resource_id": resource_id,
222
+ "resource_uri": f"meta-ads://images/{resource_id}",
223
+ "name": name,
224
+ "size": len(image_bytes),
225
+ }
@@ -0,0 +1,33 @@
1
+ """Configuration management for meta-ads-mcp."""
2
+
3
+ from pydantic import Field
4
+ from pydantic_settings import BaseSettings
5
+
6
+
7
+ class Settings(BaseSettings):
8
+ """Application settings loaded from environment variables."""
9
+
10
+ log_level: str = Field(default="INFO", description="Logging level")
11
+
12
+ # Meta direct-auth (optional -- Pipeboard auth takes precedence)
13
+ meta_app_id: str = Field(default="", description="Meta App ID for direct OAuth")
14
+ meta_app_secret: str = Field(default="", description="Meta App Secret for appsecret_proof signing")
15
+
16
+ # Pipeboard cloud auth (recommended)
17
+ pipeboard_api_token: str = Field(default="", description="Pipeboard API token for cloud-hosted auth")
18
+
19
+ # HTTP transport
20
+ server_host: str = Field(default="localhost", description="Host for streamable-http transport")
21
+ server_port: int = Field(default=8080, description="Port for streamable-http transport")
22
+
23
+
24
+ def load_settings(**kwargs: object) -> Settings:
25
+ """Load application settings from environment variables.
26
+
27
+ Args:
28
+ **kwargs: Override any setting value.
29
+
30
+ Returns:
31
+ Populated Settings instance.
32
+ """
33
+ return Settings(**kwargs)
@@ -0,0 +1,187 @@
1
+ Metadata-Version: 2.3
2
+ Name: meta-ads-mcp-python
3
+ Version: 1.0.79
4
+ Summary: MCP server for Meta Ads API -- manage campaigns, ad sets, ads, and Business Manager accounts
5
+ Keywords: meta,facebook,ads,api,mcp,claude
6
+ Author: Yves Junqueira
7
+ Author-email: Yves Junqueira <yves.junqueira@gmail.com>
8
+ License: BUSL-1.1
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: Other/Proprietary License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Requires-Dist: fastmcp>=2.0.0
15
+ Requires-Dist: loguru>=0.7.3
16
+ Requires-Dist: pydantic>=2.11.7
17
+ Requires-Dist: pydantic-settings>=2.10.1
18
+ Requires-Dist: httpx>=0.26.0
19
+ Requires-Dist: python-dotenv>=1.1.0
20
+ Requires-Dist: requests>=2.32.3
21
+ Requires-Dist: pillow>=10.0.0
22
+ Requires-Dist: python-dateutil>=2.8.2
23
+ Requires-Python: >=3.12
24
+ Project-URL: Homepage, https://github.com/locomotive-agency/meta-ads-mcp-python
25
+ Project-URL: Bug Tracker, https://github.com/locomotive-agency/meta-ads-mcp-python/issues
26
+ Description-Content-Type: text/markdown
27
+
28
+ # Meta Ads MCP
29
+
30
+ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for analysing Meta Ads campaigns through an AI interface. Retrieve performance data, visualize ad creatives, and surface insights for Facebook, Instagram, and other Meta platforms.
31
+
32
+ > **Read-only:** All tools are strictly read-only. No tool creates, modifies, or deletes any data.
33
+
34
+ > **Independent project:** This is an open-source project using Meta's public APIs. Not affiliated with or endorsed by Meta. For an officially approved Meta app, see [Pipeboard](https://pipeboard.co).
35
+
36
+ ---
37
+
38
+ ## Quick Start (Remote MCP)
39
+
40
+ The fastest way to get started is the [Pipeboard remote MCP](https://pipeboard.co) — no local setup required.
41
+
42
+ Remote MCP URL: `https://mcp.pipeboard.co/meta-ads-mcp`
43
+
44
+ With token auth: `https://mcp.pipeboard.co/meta-ads-mcp?token=YOUR_PIPEBOARD_TOKEN`
45
+
46
+ Get your token at [pipeboard.co/api-tokens](https://pipeboard.co/api-tokens).
47
+
48
+ ### Claude (Pro/Max)
49
+
50
+ 1. Go to [claude.ai/settings/integrations](https://claude.ai/settings/integrations)
51
+ 2. Click **Add Integration**, set the URL to `https://mcp.pipeboard.co/meta-ads-mcp`
52
+ 3. Click **Connect** and follow the prompts.
53
+
54
+ ### Cursor
55
+
56
+ Add to `~/.cursor/mcp.json`:
57
+
58
+ ```json
59
+ {
60
+ "mcpServers": {
61
+ "meta-ads-remote": {
62
+ "url": "https://mcp.pipeboard.co/meta-ads-mcp?token=YOUR_PIPEBOARD_TOKEN"
63
+ }
64
+ }
65
+ }
66
+ ```
67
+
68
+ ### Other MCP Clients
69
+
70
+ Use the remote URL above. Full setup guides: [pipeboard.co](https://pipeboard.co)
71
+
72
+ ---
73
+
74
+ ## Local Installation
75
+
76
+ > For advanced users. The remote MCP is recommended for most cases.
77
+
78
+ **Requirements:** Python 3.12+, a Meta Developer App (App ID + Secret), and [uv](https://github.com/astral-sh/uv).
79
+
80
+ ```bash
81
+ pip install meta-ads-mcp
82
+ ```
83
+
84
+ ### Authentication
85
+
86
+ Copy `.env.example` to `.env` and set one of:
87
+
88
+ ```
89
+ PIPEBOARD_API_TOKEN=your_token # Recommended — no Meta app required
90
+ META_APP_ID=your_app_id # Direct Meta app credentials
91
+ META_APP_SECRET=your_app_secret
92
+ ```
93
+
94
+ ### Run
95
+
96
+ ```bash
97
+ meta-ads-mcp # stdio (default)
98
+ python -m meta_ads_mcp --transport streamable-http --host localhost --port 8080 # HTTP
99
+ ```
100
+
101
+ ### Claude Desktop config
102
+
103
+ ```json
104
+ {
105
+ "mcpServers": {
106
+ "meta-ads": {
107
+ "command": "meta-ads-mcp"
108
+ }
109
+ }
110
+ }
111
+ ```
112
+
113
+ ---
114
+
115
+ ## Configuration
116
+
117
+ | Variable | Description |
118
+ |---|---|
119
+ | `PIPEBOARD_API_TOKEN` | Pipeboard API token (recommended) |
120
+ | `META_APP_ID` | Meta Developer App ID (direct auth) |
121
+ | `META_APP_SECRET` | Meta Developer App Secret (direct auth) |
122
+ | `LOG_LEVEL` | `DEBUG`, `INFO`, `WARNING`, `ERROR` (default: `INFO`) |
123
+ | `SERVER_HOST` | HTTP transport host (default: `localhost`) |
124
+ | `SERVER_PORT` | HTTP transport port (default: `8080`) |
125
+
126
+ ---
127
+
128
+ ## Available Tools
129
+
130
+ | Tool | Description |
131
+ |---|---|
132
+ | `mcp_meta_ads_get_ad_accounts` | List accessible ad accounts |
133
+ | `mcp_meta_ads_get_account_info` | Get details for a specific ad account |
134
+ | `mcp_meta_ads_get_account_pages` | Get pages associated with an ad account |
135
+ | `mcp_meta_ads_get_campaigns` | List campaigns with optional status filter |
136
+ | `mcp_meta_ads_get_campaign_details` | Get details for a specific campaign |
137
+ | `mcp_meta_ads_get_adsets` | List ad sets, optionally filtered by campaign |
138
+ | `mcp_meta_ads_get_adset_details` | Get details for a specific ad set |
139
+ | `mcp_meta_ads_get_ads` | List ads, optionally filtered by campaign or ad set |
140
+ | `mcp_meta_ads_get_ad_details` | Get details for a specific ad |
141
+ | `mcp_meta_ads_get_ad_creatives` | Get creative details (text, images, URLs) |
142
+ | `mcp_meta_ads_get_ad_image` | Download and visualize an ad image |
143
+ | `mcp_meta_ads_get_insights` | Get performance metrics with attribution windows and breakdowns |
144
+ | `mcp_meta_ads_get_login_link` | Get a login link for Meta authentication |
145
+ | `mcp_meta_ads_search_interests` | Search interest targeting options by keyword |
146
+ | `mcp_meta_ads_get_interest_suggestions` | Get interest suggestions based on existing interests |
147
+ | `mcp_meta_ads_validate_interests` | Validate interest names or IDs |
148
+ | `mcp_meta_ads_search_behaviors` | List available behavior targeting options |
149
+ | `mcp_meta_ads_search_demographics` | List demographic targeting options |
150
+ | `mcp_meta_ads_search_geo_locations` | Search geographic targeting locations |
151
+ | `mcp_meta_ads_search` | Generic search across accounts, campaigns, ads, and pages |
152
+ | `mcp_meta_ads_get_businesses` | List all Business Manager accounts |
153
+ | `mcp_meta_ads_get_business_ad_accounts` | List ad accounts under a Business Manager |
154
+ | `mcp_meta_ads_find_client_account` | Search for a client account across Business Managers |
155
+ | `mcp_meta_ads_get_business_details` | Get details for a specific Business Manager |
156
+
157
+ ---
158
+
159
+ ## Troubleshooting
160
+
161
+ - **Auth errors:** Check your token/credentials in `.env`. Use `mcp_meta_ads_get_login_link` to re-authenticate.
162
+ - **No accounts returned:** Ensure the user has access to at least one ad account in Meta Business Manager.
163
+ - **Tool not found:** Confirm the server is running and your client config is correct.
164
+
165
+ ---
166
+
167
+ ## Privacy and Security
168
+
169
+ - Remote MCP: auth is handled in the cloud, no tokens stored locally.
170
+ - Local install: tokens are cached on your machine. Never commit your `.env` file.
171
+ - The server never exposes your access token to the LLM.
172
+
173
+ ---
174
+
175
+ ## Licensing
176
+
177
+ Licensed under the [Business Source License 1.1](LICENSE). Free for individual and business use. Converts to Apache 2.0 on January 1, 2029. You may not offer this as a competing hosted service.
178
+
179
+ For commercial licensing: [info@pipeboard.co](mailto:info@pipeboard.co)
180
+
181
+ ---
182
+
183
+ ## Support
184
+
185
+ - [Discord](https://discord.gg/YzMwQ8zrjr)
186
+ - [GitHub Issues](https://github.com/pipeboard-co/meta-ads-mcp/issues)
187
+ - [Email](mailto:info@pipeboard.co)
@@ -0,0 +1,29 @@
1
+ meta_ads_mcp/__init__.py,sha256=-thYt7S219dpbxUk4NFJOxAjP3M2TgDv0ZTt_ygh48U,1759
2
+ meta_ads_mcp/__main__.py,sha256=XaQt3iXftG_7f0Zu7Wop9SeFgrD2WBn0EQOaPMc27d8,207
3
+ meta_ads_mcp/core/__init__.py,sha256=QBtXQtHlni_kg0DzsJk2w9yjMRQ481Wm_DEmtTMtxec,1982
4
+ meta_ads_mcp/core/accounts.py,sha256=Qfcp2p0EUqqUqFPzRnQMPKkblQL97Ka66vN5vEeXceo,5815
5
+ meta_ads_mcp/core/ads.py,sha256=SlqOaUDEAEKyV-h-Whze7v6HRZm4uRcL-3j6_EIyTCk,124995
6
+ meta_ads_mcp/core/ads_library.py,sha256=smGz9FhM6RIUjlQT4Jv1BaZmXahGdK21eRCB7QMhK-4,3228
7
+ meta_ads_mcp/core/adsets.py,sha256=og2WVmPyXO7c9PtrX8g21PfpcksmwAVZcOGzlqD-FZ0,37429
8
+ meta_ads_mcp/core/api.py,sha256=60DLCo3YsuqlbpI76q6qdW-jiVrFn3DsWdUPbeI9iMg,21645
9
+ meta_ads_mcp/core/auth.py,sha256=saZZyYE0nLgoUFVsIfTcAWq4XZN5orI0DCI5xmxGtbQ,23649
10
+ meta_ads_mcp/core/authentication.py,sha256=dQ-vFrTuP-uzkfOuAJf4d4RketOUnCZeF891LkHQcDg,10472
11
+ meta_ads_mcp/core/budget_schedules.py,sha256=ljTloal8ohFJTOSqaZhbRnvA6SRW0z5RvvFTkfVtxTk,2892
12
+ meta_ads_mcp/core/callback_server.py,sha256=fpDl9JEMpvbfmHBPj2R5T55JjeGEL9bbAjeWhOUAQcg,9354
13
+ meta_ads_mcp/core/campaigns.py,sha256=JAAOPbnxMWhwtNsOhqX9QmXWt8pSDX0MqJ1Ih9SeNTM,17162
14
+ meta_ads_mcp/core/duplication.py,sha256=fLZsJrSlvnLjdMLCww_nA1CiaYivbGLPxVcJbepzlzQ,22974
15
+ meta_ads_mcp/core/http_auth_integration.py,sha256=lGpKhfzJcyWugBcYEvypY-qnlt-3UDBLqh7xAUH0DGw,12473
16
+ meta_ads_mcp/core/insights.py,sha256=T0Qwsfd9QUiJOf8gAvhtp_zs5OCLmOhSbBpIHqyKQPo,9114
17
+ meta_ads_mcp/core/mcc.py,sha256=XozUBRGzh6V4YLEJjNQcam71GzgSR8L-WKAU9a6q3s4,8010
18
+ meta_ads_mcp/core/openai_deep_research.py,sha256=EdYSZAHK0llS6YthXSXWrU3cJn5F_pVeimIWD9M_LiY,19069
19
+ meta_ads_mcp/core/pipeboard_auth.py,sha256=06PHiSg1rUBRY4FJ-eo-EFi9cVe32hwhjDkxXHykPKY,24524
20
+ meta_ads_mcp/core/reports.py,sha256=4_6MgwkdXiGedh3cGVgFaCsiHrcaO9hiAl3-gAgCkgw,5813
21
+ meta_ads_mcp/core/resources.py,sha256=-zIIfZulpo76vcKv6jhAlQq91cR2SZ3cjYZt3ek3x0w,1236
22
+ meta_ads_mcp/core/server.py,sha256=jSUkOzwxIL7v8eIvuN9weBoq8R7s5GRSrec-AGFoFDY,17892
23
+ meta_ads_mcp/core/targeting.py,sha256=S5m3zVs4rnuwC4eIt0hLsTf7_mATUnsXYNTCXJkAXz0,25274
24
+ meta_ads_mcp/core/utils.py,sha256=dQJURUXyB_ojWk9NlftjanR48-cElA3iOT35XMTF01c,7832
25
+ meta_ads_mcp/settings.py,sha256=F-uPfXrnDFyW7cJ5NmWhg1PYmexyVHCCrwU2LXUDlo4,1171
26
+ meta_ads_mcp_python-1.0.79.dist-info/WHEEL,sha256=WvwXFgRajeoYkfRVmDhkP4Qlqo31Mk687zIO2QQoFmw,80
27
+ meta_ads_mcp_python-1.0.79.dist-info/entry_points.txt,sha256=1Jl6H25XiykQqFHYOYJQ4LJrrMd4o_Fn0ombZaHHP_g,64
28
+ meta_ads_mcp_python-1.0.79.dist-info/METADATA,sha256=H13RNdBQWRxonE5wnXMSjMCXmvh6O0hgNng1leZKxuE,6775
29
+ meta_ads_mcp_python-1.0.79.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.7
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ meta-ads-mcp = meta_ads_mcp.core.server:main
3
+