meta-ads-mcp 0.2.8__py3-none-any.whl → 0.2.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
meta_ads_mcp/__init__.py CHANGED
@@ -7,7 +7,7 @@ with the Claude LLM.
7
7
 
8
8
  from meta_ads_mcp.core.server import main
9
9
 
10
- __version__ = "0.2.8"
10
+ __version__ = "0.2.9"
11
11
 
12
12
  __all__ = [
13
13
  'get_ad_accounts',
meta_ads_mcp/core/ads.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """Ad and Creative-related functionality for Meta Ads API."""
2
2
 
3
3
  import json
4
- from typing import Optional, Dict, Any
4
+ from typing import Optional, Dict, Any, List
5
5
  import io
6
6
  from PIL import Image as PILImage
7
7
  from mcp.server.fastmcp import Image
@@ -36,18 +36,27 @@ async def get_ads(access_token: str = None, account_id: str = None, limit: int =
36
36
  else:
37
37
  return json.dumps({"error": "No account ID specified and no accounts found for user"}, indent=2)
38
38
 
39
- endpoint = f"{account_id}/ads"
40
- params = {
41
- "fields": "id,name,adset_id,campaign_id,status,creative,created_time,updated_time,bid_amount,conversion_domain,tracking_specs",
42
- "limit": limit
43
- }
44
-
39
+ # Use campaign-specific endpoint if campaign_id is provided
45
40
  if campaign_id:
46
- params["campaign_id"] = campaign_id
47
-
48
- if adset_id:
49
- params["adset_id"] = adset_id
50
-
41
+ endpoint = f"{campaign_id}/ads"
42
+ params = {
43
+ "fields": "id,name,adset_id,campaign_id,status,creative,created_time,updated_time,bid_amount,conversion_domain,tracking_specs",
44
+ "limit": limit
45
+ }
46
+ # Adset ID can still be used to filter within the campaign
47
+ if adset_id:
48
+ params["adset_id"] = adset_id
49
+ else:
50
+ # Default to account-level endpoint if no campaign_id
51
+ endpoint = f"{account_id}/ads"
52
+ params = {
53
+ "fields": "id,name,adset_id,campaign_id,status,creative,created_time,updated_time,bid_amount,conversion_domain,tracking_specs",
54
+ "limit": limit
55
+ }
56
+ # Adset ID can filter at the account level if no campaign specified
57
+ if adset_id:
58
+ params["adset_id"] = adset_id
59
+
51
60
  data = await make_api_request(endpoint, access_token, params)
52
61
 
53
62
  return json.dumps(data, indent=2)
@@ -78,156 +87,98 @@ async def get_ad_details(access_token: str = None, ad_id: str = None) -> str:
78
87
 
79
88
  @mcp_server.tool()
80
89
  @meta_api_tool
81
- async def get_ad_creatives(access_token: str = None, ad_id: str = None) -> str:
90
+ async def create_ad(
91
+ account_id: str = None,
92
+ name: str = None,
93
+ adset_id: str = None,
94
+ creative_id: str = None,
95
+ status: str = "PAUSED",
96
+ bid_amount = None,
97
+ tracking_specs: Optional[List[Dict[str, Any]]] = None,
98
+ access_token: str = None
99
+ ) -> str:
82
100
  """
83
- Get creative details for a specific ad. Best if combined with get_ad_image to get the full image.
101
+ Create a new ad with an existing creative.
84
102
 
85
103
  Args:
104
+ account_id: Meta Ads account ID (format: act_XXXXXXXXX)
105
+ name: Ad name
106
+ adset_id: Ad set ID where this ad will be placed
107
+ creative_id: ID of an existing creative to use
108
+ status: Initial ad status (default: PAUSED)
109
+ bid_amount: Optional bid amount in account currency (in cents)
110
+ tracking_specs: Optional tracking specifications (e.g., for pixel events).
111
+ Example: [{"action.type":"offsite_conversion","fb_pixel":["YOUR_PIXEL_ID"]}]
86
112
  access_token: Meta API access token (optional - will use cached token if not provided)
87
- ad_id: Meta Ads ad ID
88
113
  """
89
- if not ad_id:
90
- return json.dumps({"error": "No ad ID provided"}, indent=2)
91
-
92
- # First, get the creative ID from the ad
93
- endpoint = f"{ad_id}"
94
- params = {
95
- "fields": "creative"
96
- }
114
+ # Check required parameters
115
+ if not account_id:
116
+ return json.dumps({"error": "No account ID provided"}, indent=2)
97
117
 
98
- ad_data = await make_api_request(endpoint, access_token, params)
118
+ if not name:
119
+ return json.dumps({"error": "No ad name provided"}, indent=2)
99
120
 
100
- if "error" in ad_data:
101
- return json.dumps(ad_data, indent=2)
121
+ if not adset_id:
122
+ return json.dumps({"error": "No ad set ID provided"}, indent=2)
102
123
 
103
- if "creative" not in ad_data:
104
- return json.dumps({"error": "No creative found for this ad"}, indent=2)
105
-
106
- creative_id = ad_data.get("creative", {}).get("id")
107
124
  if not creative_id:
108
- return json.dumps({"error": "Creative ID not found", "ad_data": ad_data}, indent=2)
125
+ return json.dumps({"error": "No creative ID provided"}, indent=2)
109
126
 
110
- # Now get the creative details with essential fields
111
- creative_endpoint = f"{creative_id}"
112
- creative_params = {
113
- "fields": "id,name,title,body,image_url,object_story_spec,url_tags,link_url,thumbnail_url,image_hash,asset_feed_spec,object_type"
114
- }
127
+ endpoint = f"{account_id}/ads"
115
128
 
116
- creative_data = await make_api_request(creative_endpoint, access_token, creative_params)
129
+ params = {
130
+ "name": name,
131
+ "adset_id": adset_id,
132
+ "creative": {"creative_id": creative_id},
133
+ "status": status
134
+ }
117
135
 
118
- # Try to get full-size images in different ways:
136
+ # Add bid amount if provided
137
+ if bid_amount is not None:
138
+ params["bid_amount"] = str(bid_amount)
139
+
140
+ # Add tracking specs if provided
141
+ if tracking_specs is not None:
142
+ params["tracking_specs"] = json.dumps(tracking_specs) # Needs to be JSON encoded string
119
143
 
120
- # 1. First approach: Get ad images directly using the adimages endpoint
121
- if "image_hash" in creative_data:
122
- image_hash = creative_data.get("image_hash")
123
- image_endpoint = f"act_{ad_data.get('account_id', '')}/adimages"
124
- image_params = {
125
- "hashes": [image_hash]
126
- }
127
- image_data = await make_api_request(image_endpoint, access_token, image_params)
128
- if "data" in image_data and len(image_data["data"]) > 0:
129
- creative_data["full_image_url"] = image_data["data"][0].get("url")
144
+ try:
145
+ data = await make_api_request(endpoint, access_token, params, method="POST")
146
+ return json.dumps(data, indent=2)
147
+ except Exception as e:
148
+ error_msg = str(e)
149
+ return json.dumps({
150
+ "error": "Failed to create ad",
151
+ "details": error_msg,
152
+ "params_sent": params
153
+ }, indent=2)
154
+
155
+
156
+ @mcp_server.tool()
157
+ @meta_api_tool
158
+ async def get_ad_creatives(access_token: str = None, ad_id: str = None) -> str:
159
+ """
160
+ Get creative details for a specific ad. Best if combined with get_ad_image to get the full image.
130
161
 
131
- # 2. For creatives with object_story_spec
132
- if "object_story_spec" in creative_data:
133
- spec = creative_data.get("object_story_spec", {})
134
-
135
- # For link ads
136
- if "link_data" in spec:
137
- link_data = spec.get("link_data", {})
138
- # If there's an explicit image_url, use it
139
- if "image_url" in link_data:
140
- creative_data["full_image_url"] = link_data.get("image_url")
141
- # If there's an image_hash, try to get the full image
142
- elif "image_hash" in link_data:
143
- image_hash = link_data.get("image_hash")
144
- account_id = ad_data.get('account_id', '')
145
- if not account_id:
146
- # Try to get account ID from ad ID
147
- ad_details_endpoint = f"{ad_id}"
148
- ad_details_params = {
149
- "fields": "account_id"
150
- }
151
- ad_details = await make_api_request(ad_details_endpoint, access_token, ad_details_params)
152
- account_id = ad_details.get('account_id', '')
153
-
154
- if account_id:
155
- image_endpoint = f"act_{account_id}/adimages"
156
- image_params = {
157
- "hashes": [image_hash]
158
- }
159
- image_data = await make_api_request(image_endpoint, access_token, image_params)
160
- if "data" in image_data and len(image_data["data"]) > 0:
161
- creative_data["full_image_url"] = image_data["data"][0].get("url")
162
+ Args:
163
+ access_token: Meta API access token (optional - will use cached token if not provided)
164
+ ad_id: Meta Ads ad ID
165
+ """
166
+ if not ad_id:
167
+ return json.dumps({"error": "No ad ID provided"}, indent=2)
162
168
 
163
- # For photo ads
164
- if "photo_data" in spec:
165
- photo_data = spec.get("photo_data", {})
166
- if "image_hash" in photo_data:
167
- image_hash = photo_data.get("image_hash")
168
- account_id = ad_data.get('account_id', '')
169
- if not account_id:
170
- # Try to get account ID from ad ID
171
- ad_details_endpoint = f"{ad_id}"
172
- ad_details_params = {
173
- "fields": "account_id"
174
- }
175
- ad_details = await make_api_request(ad_details_endpoint, access_token, ad_details_params)
176
- account_id = ad_details.get('account_id', '')
177
-
178
- if account_id:
179
- image_endpoint = f"act_{account_id}/adimages"
180
- image_params = {
181
- "hashes": [image_hash]
182
- }
183
- image_data = await make_api_request(image_endpoint, access_token, image_params)
184
- if "data" in image_data and len(image_data["data"]) > 0:
185
- creative_data["full_image_url"] = image_data["data"][0].get("url")
186
-
187
- # 3. If there's an asset_feed_spec, try to get images from there
188
- if "asset_feed_spec" in creative_data and "images" in creative_data["asset_feed_spec"]:
189
- images = creative_data["asset_feed_spec"]["images"]
190
- if images and len(images) > 0 and "hash" in images[0]:
191
- image_hash = images[0]["hash"]
192
- account_id = ad_data.get('account_id', '')
193
- if not account_id:
194
- # Try to get account ID
195
- ad_details_endpoint = f"{ad_id}"
196
- ad_details_params = {
197
- "fields": "account_id"
198
- }
199
- ad_details = await make_api_request(ad_details_endpoint, access_token, ad_details_params)
200
- account_id = ad_details.get('account_id', '')
201
-
202
- if account_id:
203
- image_endpoint = f"act_{account_id}/adimages"
204
- image_params = {
205
- "hashes": [image_hash]
206
- }
207
- image_data = await make_api_request(image_endpoint, access_token, image_params)
208
- if "data" in image_data and len(image_data["data"]) > 0:
209
- creative_data["full_image_url"] = image_data["data"][0].get("url")
210
-
211
- # If we have a thumbnail_url but no full_image_url, let's attempt to convert the thumbnail URL to full size
212
- if "thumbnail_url" in creative_data and "full_image_url" not in creative_data:
213
- thumbnail_url = creative_data["thumbnail_url"]
214
- # Try to convert the URL to get higher resolution by removing size parameters
215
- if "p64x64" in thumbnail_url:
216
- full_url = thumbnail_url.replace("p64x64", "p1080x1080")
217
- creative_data["full_image_url"] = full_url
218
- elif "dst-emg0" in thumbnail_url:
219
- # Remove the dst-emg0 parameter that seems to reduce size
220
- full_url = thumbnail_url.replace("dst-emg0_", "")
221
- creative_data["full_image_url"] = full_url
222
-
223
- # Fallback to using thumbnail or image_url if we still don't have a full image
224
- if "full_image_url" not in creative_data:
225
- if "thumbnail_url" in creative_data:
226
- creative_data["full_image_url"] = creative_data["thumbnail_url"]
227
- elif "image_url" in creative_data:
228
- creative_data["full_image_url"] = creative_data["image_url"]
229
-
230
- return json.dumps(creative_data, indent=2)
169
+ endpoint = f"{ad_id}/adcreatives"
170
+ params = {
171
+ "fields": "id,name,status,thumbnail_url,image_url,image_hash,object_story_spec" # Added image_hash
172
+ }
173
+
174
+ data = await make_api_request(endpoint, access_token, params)
175
+
176
+ # Add image URLs for direct viewing if available
177
+ if 'data' in data:
178
+ for creative in data['data']:
179
+ creative['image_urls_for_viewing'] = ad_creative_images(creative)
180
+
181
+ return json.dumps(data, indent=2)
231
182
 
232
183
 
233
184
  @mcp_server.tool()
@@ -368,7 +319,13 @@ async def get_ad_image(access_token: str = None, ad_id: str = None) -> Image:
368
319
 
369
320
  @mcp_server.tool()
370
321
  @meta_api_tool
371
- async def update_ad(ad_id: str, status: str = None, bid_amount: int = None, access_token: str = None) -> str:
322
+ async def update_ad(
323
+ ad_id: str,
324
+ status: str = None,
325
+ bid_amount: int = None,
326
+ tracking_specs = None,
327
+ access_token: str = None
328
+ ) -> str:
372
329
  """
373
330
  Update an ad with new settings.
374
331
 
@@ -376,51 +333,25 @@ async def update_ad(ad_id: str, status: str = None, bid_amount: int = None, acce
376
333
  ad_id: Meta Ads ad ID
377
334
  status: Update ad status (ACTIVE, PAUSED, etc.)
378
335
  bid_amount: Bid amount in account currency (in cents for USD)
336
+ tracking_specs: Optional tracking specifications (e.g., for pixel events).
379
337
  access_token: Meta API access token (optional - will use cached token if not provided)
380
338
  """
381
339
  if not ad_id:
382
- return json.dumps({"error": "No ad ID provided"}, indent=2)
383
-
384
- changes = {}
385
-
386
- if status is not None:
387
- changes['status'] = status
388
-
340
+ return json.dumps({"error": "Ad ID is required"}, indent=2)
341
+
342
+ params = {}
343
+ if status:
344
+ params["status"] = status
389
345
  if bid_amount is not None:
390
- changes['bid_amount'] = bid_amount
391
-
392
- if not changes:
393
- return json.dumps({"error": "No update parameters provided"}, indent=2)
394
-
395
- # Get current ad details for comparison
396
- current_details_json = await get_ad_details(ad_id=ad_id, access_token=access_token)
397
- current_details = json.loads(current_details_json)
398
-
399
- # Import the callback server components
400
- from .callback_server import start_callback_server, update_confirmation
401
- import urllib.parse
402
-
403
- # Start the callback server if not already running
404
- port = start_callback_server()
405
-
406
- # Generate confirmation URL with properly encoded parameters
407
- changes_json = json.dumps(changes)
408
- encoded_changes = urllib.parse.quote(changes_json)
409
- confirmation_url = f"http://localhost:{port}/confirm-update?ad_id={ad_id}&token={access_token}&changes={encoded_changes}"
410
-
411
- # Reset the update confirmation
412
- update_confirmation.clear()
413
- update_confirmation.update({"approved": False})
414
-
415
- # Return the confirmation link
416
- response = {
417
- "message": "Please confirm the ad update",
418
- "confirmation_url": confirmation_url,
419
- "markdown_link": f"[Click here to confirm ad update]({confirmation_url})",
420
- "current_details": current_details,
421
- "proposed_changes": changes,
422
- "instructions_for_llm": "You must present this link as clickable Markdown to the user using the markdown_link format provided.",
423
- "note": "After authenticating, the token will be automatically saved and your ad will be updated. Refresh the browser page if it doesn't load immediately."
424
- }
425
-
426
- return json.dumps(response, indent=2)
346
+ # Ensure bid_amount is sent as a string if it's not null
347
+ params["bid_amount"] = str(bid_amount)
348
+ if tracking_specs is not None: # Add tracking_specs to params if provided
349
+ params["tracking_specs"] = json.dumps(tracking_specs) # Needs to be JSON encoded string
350
+
351
+ if not params:
352
+ return json.dumps({"error": "No update parameters provided (status, bid_amount, or tracking_specs)"}, indent=2)
353
+
354
+ endpoint = f"{ad_id}"
355
+ data = await make_api_request(endpoint, access_token, params, method='POST')
356
+
357
+ return json.dumps(data, indent=2)
@@ -32,15 +32,23 @@ async def get_adsets(access_token: str = None, account_id: str = None, limit: in
32
32
  else:
33
33
  return json.dumps({"error": "No account ID specified and no accounts found for user"}, indent=2)
34
34
 
35
- endpoint = f"{account_id}/adsets"
36
- params = {
37
- "fields": "id,name,campaign_id,status,daily_budget,lifetime_budget,targeting,bid_amount,bid_strategy,optimization_goal,billing_event,start_time,end_time,created_time,updated_time,frequency_control_specs{event,interval_days,max_frequency}",
38
- "limit": limit
39
- }
40
-
35
+ # Change endpoint based on whether campaign_id is provided
41
36
  if campaign_id:
42
- params["campaign_id"] = campaign_id
43
-
37
+ endpoint = f"{campaign_id}/adsets"
38
+ params = {
39
+ "fields": "id,name,campaign_id,status,daily_budget,lifetime_budget,targeting,bid_amount,bid_strategy,optimization_goal,billing_event,start_time,end_time,created_time,updated_time,frequency_control_specs{event,interval_days,max_frequency}",
40
+ "limit": limit
41
+ }
42
+ else:
43
+ # Use account endpoint if no campaign_id is given
44
+ endpoint = f"{account_id}/adsets"
45
+ params = {
46
+ "fields": "id,name,campaign_id,status,daily_budget,lifetime_budget,targeting,bid_amount,bid_strategy,optimization_goal,billing_event,start_time,end_time,created_time,updated_time,frequency_control_specs{event,interval_days,max_frequency}",
47
+ "limit": limit
48
+ }
49
+ # Note: Removed the attempt to add campaign_id to params for the account endpoint case,
50
+ # as it was ineffective and the logic now uses the correct endpoint for campaign filtering.
51
+
44
52
  data = await make_api_request(endpoint, access_token, params)
45
53
 
46
54
  return json.dumps(data, indent=2)
@@ -66,15 +74,128 @@ async def get_adset_details(access_token: str = None, adset_id: str = None) -> s
66
74
  return json.dumps({"error": "No ad set ID provided"}, indent=2)
67
75
 
68
76
  endpoint = f"{adset_id}"
77
+ # Explicitly prioritize frequency_control_specs in the fields request
69
78
  params = {
70
- "fields": "id,name,campaign_id,status,daily_budget,lifetime_budget,targeting,bid_amount,bid_strategy,optimization_goal,billing_event,start_time,end_time,created_time,updated_time,attribution_spec,destination_type,promoted_object,pacing_type,budget_remaining,frequency_control_specs{event,interval_days,max_frequency}"
79
+ "fields": "id,name,campaign_id,status,frequency_control_specs{event,interval_days,max_frequency},daily_budget,lifetime_budget,targeting,bid_amount,bid_strategy,optimization_goal,billing_event,start_time,end_time,created_time,updated_time,attribution_spec,destination_type,promoted_object,pacing_type,budget_remaining"
71
80
  }
72
81
 
73
82
  data = await make_api_request(endpoint, access_token, params)
74
83
 
84
+ # For debugging - check if frequency_control_specs was returned
85
+ if 'frequency_control_specs' not in data:
86
+ data['_meta'] = {
87
+ 'note': 'No frequency_control_specs field was returned by the API. This means either no frequency caps are set or the API did not include this field in the response.'
88
+ }
89
+
75
90
  return json.dumps(data, indent=2)
76
91
 
77
92
 
93
+ @mcp_server.tool()
94
+ @meta_api_tool
95
+ async def create_adset(
96
+ account_id: str = None,
97
+ campaign_id: str = None,
98
+ name: str = None,
99
+ status: str = "PAUSED",
100
+ daily_budget = None,
101
+ lifetime_budget = None,
102
+ targeting: Dict[str, Any] = None,
103
+ optimization_goal: str = None,
104
+ billing_event: str = None,
105
+ bid_amount = None,
106
+ bid_strategy: str = None,
107
+ start_time: str = None,
108
+ end_time: str = None,
109
+ access_token: str = None
110
+ ) -> str:
111
+ """
112
+ Create a new ad set in a Meta Ads account.
113
+
114
+ Args:
115
+ account_id: Meta Ads account ID (format: act_XXXXXXXXX)
116
+ campaign_id: Meta Ads campaign ID this ad set belongs to
117
+ name: Ad set name
118
+ status: Initial ad set status (default: PAUSED)
119
+ daily_budget: Daily budget in account currency (in cents) as a string
120
+ lifetime_budget: Lifetime budget in account currency (in cents) as a string
121
+ targeting: Targeting specifications including age, location, interests, etc.
122
+ Use targeting_automation.advantage_audience=1 for automatic audience finding
123
+ optimization_goal: Conversion optimization goal (e.g., 'LINK_CLICKS', 'REACH', 'CONVERSIONS')
124
+ billing_event: How you're charged (e.g., 'IMPRESSIONS', 'LINK_CLICKS')
125
+ bid_amount: Bid amount in account currency (in cents)
126
+ bid_strategy: Bid strategy (e.g., 'LOWEST_COST', 'LOWEST_COST_WITH_BID_CAP')
127
+ start_time: Start time in ISO 8601 format (e.g., '2023-12-01T12:00:00-0800')
128
+ end_time: End time in ISO 8601 format
129
+ access_token: Meta API access token (optional - will use cached token if not provided)
130
+ """
131
+ # Check required parameters
132
+ if not account_id:
133
+ return json.dumps({"error": "No account ID provided"}, indent=2)
134
+
135
+ if not campaign_id:
136
+ return json.dumps({"error": "No campaign ID provided"}, indent=2)
137
+
138
+ if not name:
139
+ return json.dumps({"error": "No ad set name provided"}, indent=2)
140
+
141
+ if not optimization_goal:
142
+ return json.dumps({"error": "No optimization goal provided"}, indent=2)
143
+
144
+ if not billing_event:
145
+ return json.dumps({"error": "No billing event provided"}, indent=2)
146
+
147
+ # Basic targeting is required if not provided
148
+ if not targeting:
149
+ targeting = {
150
+ "age_min": 18,
151
+ "age_max": 65,
152
+ "geo_locations": {"countries": ["US"]},
153
+ "targeting_automation": {"advantage_audience": 1}
154
+ }
155
+
156
+ endpoint = f"{account_id}/adsets"
157
+
158
+ params = {
159
+ "name": name,
160
+ "campaign_id": campaign_id,
161
+ "status": status,
162
+ "optimization_goal": optimization_goal,
163
+ "billing_event": billing_event,
164
+ "targeting": json.dumps(targeting) # Properly format as JSON string
165
+ }
166
+
167
+ # Convert budget values to strings if they aren't already
168
+ if daily_budget is not None:
169
+ params["daily_budget"] = str(daily_budget)
170
+
171
+ if lifetime_budget is not None:
172
+ params["lifetime_budget"] = str(lifetime_budget)
173
+
174
+ # Add other parameters if provided
175
+ if bid_amount is not None:
176
+ params["bid_amount"] = str(bid_amount)
177
+
178
+ if bid_strategy:
179
+ params["bid_strategy"] = bid_strategy
180
+
181
+ if start_time:
182
+ params["start_time"] = start_time
183
+
184
+ if end_time:
185
+ params["end_time"] = end_time
186
+
187
+ try:
188
+ data = await make_api_request(endpoint, access_token, params, method="POST")
189
+ return json.dumps(data, indent=2)
190
+ except Exception as e:
191
+ error_msg = str(e)
192
+ return json.dumps({
193
+ "error": "Failed to create ad set",
194
+ "details": error_msg,
195
+ "params_sent": params
196
+ }, indent=2)
197
+
198
+
78
199
  @mcp_server.tool()
79
200
  @meta_api_tool
80
201
  async def update_adset(adset_id: str, frequency_control_specs: List[Dict[str, Any]] = None, bid_strategy: str = None,
meta_ads_mcp/core/auth.py CHANGED
@@ -42,8 +42,8 @@ class MetaConfig:
42
42
  if cls._instance is None:
43
43
  logger.debug("Creating new MetaConfig instance")
44
44
  cls._instance = super(MetaConfig, cls).__new__(cls)
45
- cls._instance.app_id = os.environ.get("META_APP_ID", "")
46
- logger.info(f"MetaConfig initialized with app_id from env: {cls._instance.app_id}")
45
+ cls._instance.app_id = os.environ.get("META_APP_ID", "779761636818489")
46
+ logger.info(f"MetaConfig initialized with app_id from env/default: {cls._instance.app_id}")
47
47
  return cls._instance
48
48
 
49
49
  def set_app_id(self, app_id):
@@ -1,7 +1,7 @@
1
1
  """Campaign-related functionality for Meta Ads API."""
2
2
 
3
3
  import json
4
- from typing import List, Optional
4
+ from typing import List, Optional, Dict, Any, Union
5
5
  from .api import meta_api_tool, make_api_request
6
6
  from .accounts import get_ad_accounts
7
7
  from .server import mcp_server
@@ -13,6 +13,12 @@ async def get_campaigns(access_token: str = None, account_id: str = None, limit:
13
13
  """
14
14
  Get campaigns for a Meta Ads account with optional filtering.
15
15
 
16
+ Note: By default, the Meta API returns a subset of available fields.
17
+ Other fields like 'effective_status', 'special_ad_categories',
18
+ 'lifetime_budget', 'spend_cap', 'budget_remaining', 'promoted_object',
19
+ 'source_campaign_id', etc., might be available but require specifying them
20
+ in the API call (currently not exposed by this tool's parameters).
21
+
16
22
  Args:
17
23
  access_token: Meta API access token (optional - will use cached token if not provided)
18
24
  account_id: Meta Ads account ID (format: act_XXXXXXXXX)
@@ -36,7 +42,7 @@ async def get_campaigns(access_token: str = None, account_id: str = None, limit:
36
42
  }
37
43
 
38
44
  if status_filter:
39
- params["effective_status"] = [status_filter]
45
+ params["effective_status"] = status_filter
40
46
 
41
47
  data = await make_api_request(endpoint, access_token, params)
42
48
 
@@ -48,6 +54,10 @@ async def get_campaigns(access_token: str = None, account_id: str = None, limit:
48
54
  async def get_campaign_details(access_token: str = None, campaign_id: str = None) -> str:
49
55
  """
50
56
  Get detailed information about a specific campaign.
57
+
58
+ Note: This function requests a specific set of fields ('id,name,objective,status,...').
59
+ The Meta API offers many other fields for campaigns (e.g., 'effective_status', 'source_campaign_id', etc.)
60
+ that could be added to the 'fields' parameter in the code if needed.
51
61
 
52
62
  Args:
53
63
  access_token: Meta API access token (optional - will use cached token if not provided)
@@ -75,8 +85,14 @@ async def create_campaign(
75
85
  objective: str = None,
76
86
  status: str = "PAUSED",
77
87
  special_ad_categories: List[str] = None,
78
- daily_budget: Optional[int] = None,
79
- lifetime_budget: Optional[int] = None
88
+ daily_budget = None,
89
+ lifetime_budget = None,
90
+ buying_type: str = None,
91
+ bid_strategy: str = None,
92
+ bid_cap = None,
93
+ spend_cap = None,
94
+ campaign_budget_optimization: bool = None,
95
+ ab_test_control_setups: Optional[List[Dict[str, Any]]] = None
80
96
  ) -> str:
81
97
  """
82
98
  Create a new campaign in a Meta Ads account.
@@ -88,8 +104,14 @@ async def create_campaign(
88
104
  objective: Campaign objective (AWARENESS, TRAFFIC, ENGAGEMENT, etc.)
89
105
  status: Initial campaign status (default: PAUSED)
90
106
  special_ad_categories: List of special ad categories if applicable
91
- daily_budget: Daily budget in account currency (in cents)
92
- lifetime_budget: Lifetime budget in account currency (in cents)
107
+ daily_budget: Daily budget in account currency (in cents) as a string
108
+ lifetime_budget: Lifetime budget in account currency (in cents) as a string
109
+ buying_type: Buying type (e.g., 'AUCTION')
110
+ bid_strategy: Bid strategy (e.g., 'LOWEST_COST', 'LOWEST_COST_WITH_BID_CAP', 'COST_CAP')
111
+ bid_cap: Bid cap in account currency (in cents) as a string
112
+ spend_cap: Spending limit for the campaign in account currency (in cents) as a string
113
+ campaign_budget_optimization: Whether to enable campaign budget optimization
114
+ ab_test_control_setups: Settings for A/B testing (e.g., [{"name":"Creative A", "ad_format":"SINGLE_IMAGE"}])
93
115
  """
94
116
  # Check required parameters
95
117
  if not account_id:
@@ -101,23 +123,138 @@ async def create_campaign(
101
123
  if not objective:
102
124
  return json.dumps({"error": "No campaign objective provided"}, indent=2)
103
125
 
126
+ # Special_ad_categories is required by the API, set default if not provided
127
+ if special_ad_categories is None:
128
+ special_ad_categories = []
129
+
130
+ # For this example, we'll add a fixed daily budget if none is provided
131
+ if not daily_budget and not lifetime_budget:
132
+ daily_budget = "1000" # Default to $10 USD
133
+
104
134
  endpoint = f"{account_id}/campaigns"
105
135
 
106
136
  params = {
107
137
  "name": name,
108
138
  "objective": objective,
109
139
  "status": status,
140
+ "special_ad_categories": json.dumps(special_ad_categories) # Properly format as JSON string
110
141
  }
111
142
 
112
- if special_ad_categories:
113
- params["special_ad_categories"] = special_ad_categories
143
+ # Convert budget values to strings if they aren't already
144
+ if daily_budget is not None:
145
+ params["daily_budget"] = str(daily_budget)
146
+
147
+ if lifetime_budget is not None:
148
+ params["lifetime_budget"] = str(lifetime_budget)
149
+
150
+ # Add new parameters
151
+ if buying_type:
152
+ params["buying_type"] = buying_type
153
+
154
+ if bid_strategy:
155
+ params["bid_strategy"] = bid_strategy
156
+
157
+ if bid_cap is not None:
158
+ params["bid_cap"] = str(bid_cap)
114
159
 
115
- if daily_budget:
116
- params["daily_budget"] = daily_budget
160
+ if spend_cap is not None:
161
+ params["spend_cap"] = str(spend_cap)
117
162
 
118
- if lifetime_budget:
119
- params["lifetime_budget"] = lifetime_budget
163
+ if campaign_budget_optimization is not None:
164
+ params["campaign_budget_optimization"] = "true" if campaign_budget_optimization else "false"
165
+
166
+ if ab_test_control_setups:
167
+ params["ab_test_control_setups"] = json.dumps(ab_test_control_setups)
168
+
169
+ try:
170
+ data = await make_api_request(endpoint, access_token, params, method="POST")
171
+ return json.dumps(data, indent=2)
172
+ except Exception as e:
173
+ error_msg = str(e)
174
+ return json.dumps({
175
+ "error": "Failed to create campaign",
176
+ "details": error_msg,
177
+ "params_sent": params
178
+ }, indent=2)
179
+
180
+
181
+ @mcp_server.tool()
182
+ @meta_api_tool
183
+ async def update_campaign(
184
+ access_token: str = None,
185
+ campaign_id: str = None,
186
+ name: str = None,
187
+ status: str = None,
188
+ special_ad_categories: List[str] = None,
189
+ daily_budget = None,
190
+ lifetime_budget = None,
191
+ bid_strategy: str = None,
192
+ bid_cap = None,
193
+ spend_cap = None,
194
+ campaign_budget_optimization: bool = None,
195
+ objective: str = None, # Add objective if it's updatable
196
+ # Add other updatable fields as needed based on API docs
197
+ ) -> str:
198
+ """
199
+ Update an existing campaign in a Meta Ads account.
200
+
201
+ Args:
202
+ access_token: Meta API access token (optional - will use cached token if not provided)
203
+ campaign_id: Meta Ads campaign ID (required)
204
+ name: New campaign name
205
+ status: New campaign status (e.g., 'ACTIVE', 'PAUSED')
206
+ special_ad_categories: List of special ad categories if applicable
207
+ daily_budget: New daily budget in account currency (in cents) as a string
208
+ lifetime_budget: New lifetime budget in account currency (in cents) as a string
209
+ bid_strategy: New bid strategy
210
+ bid_cap: New bid cap in account currency (in cents) as a string
211
+ spend_cap: New spending limit for the campaign in account currency (in cents) as a string
212
+ campaign_budget_optimization: Enable/disable campaign budget optimization
213
+ objective: New campaign objective (Note: May not always be updatable)
214
+ """
215
+ if not campaign_id:
216
+ return json.dumps({"error": "No campaign ID provided"}, indent=2)
217
+
218
+ endpoint = f"{campaign_id}"
120
219
 
121
- data = await make_api_request(endpoint, access_token, params, method="POST")
220
+ params = {}
122
221
 
123
- return json.dumps(data, indent=2)
222
+ # Add parameters to the request only if they are provided
223
+ if name is not None:
224
+ params["name"] = name
225
+ if status is not None:
226
+ params["status"] = status
227
+ if special_ad_categories is not None:
228
+ # Note: Updating special_ad_categories might have specific API rules or might not be allowed after creation.
229
+ # The API might require an empty list `[]` to clear categories. Check Meta Docs.
230
+ params["special_ad_categories"] = json.dumps(special_ad_categories)
231
+ if daily_budget is not None:
232
+ params["daily_budget"] = str(daily_budget)
233
+ if lifetime_budget is not None:
234
+ params["lifetime_budget"] = str(lifetime_budget)
235
+ if bid_strategy is not None:
236
+ params["bid_strategy"] = bid_strategy
237
+ if bid_cap is not None:
238
+ params["bid_cap"] = str(bid_cap)
239
+ if spend_cap is not None:
240
+ params["spend_cap"] = str(spend_cap)
241
+ if campaign_budget_optimization is not None:
242
+ params["campaign_budget_optimization"] = "true" if campaign_budget_optimization else "false"
243
+ if objective is not None:
244
+ params["objective"] = objective # Caution: Objective changes might reset learning or be restricted
245
+
246
+ if not params:
247
+ return json.dumps({"error": "No update parameters provided"}, indent=2)
248
+
249
+ try:
250
+ # Use POST method for updates as per Meta API documentation
251
+ data = await make_api_request(endpoint, access_token, params, method="POST")
252
+ return json.dumps(data, indent=2)
253
+ except Exception as e:
254
+ error_msg = str(e)
255
+ # Include campaign_id in error for better context
256
+ return json.dumps({
257
+ "error": f"Failed to update campaign {campaign_id}",
258
+ "details": error_msg,
259
+ "params_sent": params # Be careful about logging sensitive data if any
260
+ }, indent=2)
@@ -1,7 +1,7 @@
1
1
  """Insights and Reporting functionality for Meta Ads API."""
2
2
 
3
3
  import json
4
- from typing import Optional
4
+ from typing import Optional, Union, Dict
5
5
  from .api import meta_api_tool, make_api_request
6
6
  from .utils import download_image, try_multiple_download_methods, ad_creative_images, create_resource_from_image
7
7
  from .server import mcp_server
@@ -12,7 +12,7 @@ import datetime
12
12
  @mcp_server.tool()
13
13
  @meta_api_tool
14
14
  async def get_insights(access_token: str = None, object_id: str = None,
15
- time_range: str = "maximum", breakdown: str = "",
15
+ time_range: Union[str, Dict[str, str]] = "maximum", breakdown: str = "",
16
16
  level: str = "ad") -> str:
17
17
  """
18
18
  Get performance insights for a campaign, ad set, ad or account.
@@ -20,7 +20,11 @@ async def get_insights(access_token: str = None, object_id: str = None,
20
20
  Args:
21
21
  access_token: Meta API access token (optional - will use cached token if not provided)
22
22
  object_id: ID of the campaign, ad set, ad or account
23
- time_range: Time range for insights (default: last_30_days, options: today, yesterday, this_month, last_month, this_quarter, maximum, data_maximum, last_3d, last_7d, last_14d, last_28d, last_30d, last_90d, last_week_mon_sun, last_week_sun_sat, last_quarter, last_year, this_week_mon_today, this_week_sun_today, this_year)
23
+ time_range: Either a preset time range string or a dictionary with "since" and "until" dates in YYYY-MM-DD format
24
+ Preset options: today, yesterday, this_month, last_month, this_quarter, maximum, data_maximum,
25
+ last_3d, last_7d, last_14d, last_28d, last_30d, last_90d, last_week_mon_sun,
26
+ last_week_sun_sat, last_quarter, last_year, this_week_mon_today, this_week_sun_today, this_year
27
+ Dictionary example: {"since":"2023-01-01","until":"2023-01-31"}
24
28
  breakdown: Optional breakdown dimension (e.g., age, gender, country)
25
29
  level: Level of aggregation (ad, adset, campaign, account)
26
30
  """
@@ -29,11 +33,21 @@ async def get_insights(access_token: str = None, object_id: str = None,
29
33
 
30
34
  endpoint = f"{object_id}/insights"
31
35
  params = {
32
- "date_preset": time_range,
33
36
  "fields": "account_id,account_name,campaign_id,campaign_name,adset_id,adset_name,ad_id,ad_name,impressions,clicks,spend,cpc,cpm,ctr,reach,frequency,actions,conversions,unique_clicks,cost_per_action_type",
34
37
  "level": level
35
38
  }
36
39
 
40
+ # Handle time range based on type
41
+ if isinstance(time_range, dict):
42
+ # Use custom date range with since/until parameters
43
+ if "since" in time_range and "until" in time_range:
44
+ params["time_range"] = json.dumps(time_range)
45
+ else:
46
+ return json.dumps({"error": "Custom time_range must contain both 'since' and 'until' keys in YYYY-MM-DD format"}, indent=2)
47
+ else:
48
+ # Use preset date range
49
+ params["date_preset"] = time_range
50
+
37
51
  if breakdown:
38
52
  params["breakdowns"] = breakdown
39
53
 
@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meta-ads-mcp
3
- Version: 0.2.8
3
+ Version: 0.2.9
4
4
  Summary: Model Calling Protocol (MCP) plugin for interacting with Meta Ads API
5
5
  Project-URL: Homepage, https://github.com/nictuku/meta-ads-mcp
6
6
  Project-URL: Bug Tracker, https://github.com/nictuku/meta-ads-mcp/issues
7
7
  Author-email: Your Name <your.email@example.com>
8
8
  License: MIT
9
+ License-File: LICENSE
9
10
  Keywords: ads,api,claude,facebook,mcp,meta
10
11
  Classifier: License :: OSI Approved :: MIT License
11
12
  Classifier: Operating System :: OS Independent
@@ -26,6 +27,10 @@ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for in
26
27
 
27
28
  > **DISCLAIMER:** This is an unofficial third-party tool and is not associated with, endorsed by, or affiliated with Meta in any way. This project is maintained independently and uses Meta's public APIs according to their terms of service. Meta, Facebook, Instagram, and other Meta brand names are trademarks of their respective owners.
28
29
 
30
+ <a href="https://glama.ai/mcp/servers/@pipeboard-co/meta-ads-mcp">
31
+ <img width="380" height="200" src="https://glama.ai/mcp/servers/@pipeboard-co/meta-ads-mcp/badge" alt="Meta Ads MCP server" />
32
+ </a>
33
+
29
34
  Screenhot: using an LLM to understand your ad performance.
30
35
 
31
36
  ![Meta Ads MCP in action: Visualize ad performance metrics and creative details directly in Claude or your favorite MCP client, with rich insights about campaign reach, engagement, and costs](./images/meta-ads-example.png)
@@ -108,9 +113,9 @@ The easiest way to configure Meta Ads MCP is using Pipeboard authentication:
108
113
 
109
114
  This method provides longer-lived tokens (60 days), simplified setup, and automatic token renewal.
110
115
 
111
- ### Usage with Claude in Cursor
116
+ ### Usage with Cursor or Claude Desktop
112
117
 
113
- Add this to your `claude_desktop_config.json` to integrate with Claude in Cursor:
118
+ Add this to your `claude_desktop_config.json` to integrate with Claude or `~/.cursor/mcp.json` to integrate with Cursor:
114
119
 
115
120
  ```json
116
121
  "mcpServers": {
@@ -1,21 +1,22 @@
1
- meta_ads_mcp/__init__.py,sha256=uoDoht-lvXHE6xVXDhLKtnB4QltQVpDwU5styQ3VtVk,1236
1
+ meta_ads_mcp/__init__.py,sha256=AuAxuPA8inR_eb6d8SiwAh52vOUiDbGysvovx9frbRE,1236
2
2
  meta_ads_mcp/__main__.py,sha256=XaQt3iXftG_7f0Zu7Wop9SeFgrD2WBn0EQOaPMc27d8,207
3
3
  meta_ads_mcp/api.py,sha256=lSyyvvcqZ8sGcd0R9Vk2b_4zDANTiU_z6AojfL7v41s,81984
4
4
  meta_ads_mcp/core/__init__.py,sha256=6T8iqrQrw9VHhKtncLqYWyDk8jeSBPs79hs1CSu-fLU,952
5
5
  meta_ads_mcp/core/accounts.py,sha256=Nmp7lPxO9wmq25jWV7_H0LIqnEbBhpCVBlLGW2HUaq0,2277
6
- meta_ads_mcp/core/ads.py,sha256=jAwa1gD3HpXFhDW53cUMYwnt52jmBjTHTwb9HUk0EaA,16975
7
- meta_ads_mcp/core/adsets.py,sha256=ae6mreRtigqS7TnSI0AQA7wOeZpjwcSJ6q2nKpl1e5Q,7598
6
+ meta_ads_mcp/core/ads.py,sha256=LMZOo6agi6tQl4JJPmrDUn-91n7DzfxG2TChmwcrWOY,12544
7
+ meta_ads_mcp/core/adsets.py,sha256=os8MdPdHO6mGIRQdTlx7PxvcLRq8B9BhybQIHLD37Qg,12382
8
8
  meta_ads_mcp/core/api.py,sha256=ZflFD9WWScuSqi9c9m8b_Ck8S1FSTGENd3sh9rL5HfY,14182
9
- meta_ads_mcp/core/auth.py,sha256=bWJ0sRhowuHzsTHUOZ67jaa-Pjfl46Pbs7igafpi5Xc,20644
9
+ meta_ads_mcp/core/auth.py,sha256=RPfwJxrER9ZdQG0KCKeyhfjOf_pMr5h1Z2b5dEIHKFI,20667
10
10
  meta_ads_mcp/core/authentication.py,sha256=3AHSXslZdxyg0_s2253aQhpOeguMSu2cSYq4H_auywY,6485
11
11
  meta_ads_mcp/core/callback_server.py,sha256=b5TzUz9nEk0i5MWujlls5gAsHru__UjTPJQan1xQ_10,40947
12
- meta_ads_mcp/core/campaigns.py,sha256=u5jNHSxcWlZgzVLNmaeoovA9OcPjSUjTegUMcDLAurs,4381
13
- meta_ads_mcp/core/insights.py,sha256=f4Rs3kG0oqpKl8zBc_nt_zE0z4zvb9i0g12galrDiw8,17376
12
+ meta_ads_mcp/core/campaigns.py,sha256=20DHMwHppZuoZBg0owkf1BfmPBhWzkQcFgh5rQa-pAU,10480
13
+ meta_ads_mcp/core/insights.py,sha256=XAm4uu83gWp84PEGqAJ3GFIqlvg7prh6MdD71JfvBCo,18072
14
14
  meta_ads_mcp/core/pipeboard_auth.py,sha256=hmd9f54fFWwQgf3QIlFhyTKBnK4RetVealh_LwhmFvE,22798
15
15
  meta_ads_mcp/core/resources.py,sha256=-zIIfZulpo76vcKv6jhAlQq91cR2SZ3cjYZt3ek3x0w,1236
16
16
  meta_ads_mcp/core/server.py,sha256=5WofyJZGzeDhbGzLXPhQjT0XnZwo0syeK8TM_XnJo4Q,5507
17
17
  meta_ads_mcp/core/utils.py,sha256=EPmpBX3OZaTWRS_YuEk_PLLyLXj7DeR6Ks8WoaZ5JGQ,6366
18
- meta_ads_mcp-0.2.8.dist-info/METADATA,sha256=GpkZAczdJeBD3-4o2nr-WYbRbp17026oEoGcY2zYXMo,19316
19
- meta_ads_mcp-0.2.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
20
- meta_ads_mcp-0.2.8.dist-info/entry_points.txt,sha256=Dv2RkoBjRJBqj6CyhwqGIiwPCD-SCL1-7B9-zmVRuv0,57
21
- meta_ads_mcp-0.2.8.dist-info/RECORD,,
18
+ meta_ads_mcp-0.2.9.dist-info/METADATA,sha256=nUCswfzP08-tyyfkmvfsXOn8lxAol-bBZkoL57g1qn8,19587
19
+ meta_ads_mcp-0.2.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
20
+ meta_ads_mcp-0.2.9.dist-info/entry_points.txt,sha256=Dv2RkoBjRJBqj6CyhwqGIiwPCD-SCL1-7B9-zmVRuv0,57
21
+ meta_ads_mcp-0.2.9.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
22
+ meta_ads_mcp-0.2.9.dist-info/RECORD,,
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.