From cc9166ce65362ddd06baf73f7f523e015e30e923 Mon Sep 17 00:00:00 2001 From: Jonas Jesus Date: Tue, 6 Jan 2026 12:45:56 -0300 Subject: [PATCH 1/9] feat(meta-ads): add CRUD tools for campaigns, ad sets, ads, and creatives - Add create/update/delete methods for campaigns in meta-client - Add create/update/delete methods for ad sets with targeting support - Add create/update/delete methods for ads - Add uploadAdImage and createAdCreative methods - Add new types for all mutation operations - Create META_ADS_CREATE_CAMPAIGN, UPDATE, DELETE tools - Create META_ADS_CREATE_ADSET, UPDATE, DELETE tools - Create META_ADS_CREATE_AD, UPDATE, DELETE tools - Create META_ADS_UPLOAD_AD_IMAGE tool - Create META_ADS_CREATE_AD_CREATIVE tool - Add flow documentation in tool descriptions (5-step flow) - Update README with new tools and usage examples --- meta-ads/README.md | 144 +++++++++- meta-ads/server/lib/meta-client.ts | 432 +++++++++++++++++++++++++++++ meta-ads/server/lib/types.ts | 382 +++++++++++++++++++++++++ meta-ads/server/tools/ads.ts | 350 +++++++++++++++++++++++ meta-ads/server/tools/adsets.ts | 349 ++++++++++++++++++++++- meta-ads/server/tools/campaigns.ts | 205 ++++++++++++++ 6 files changed, 1851 insertions(+), 11 deletions(-) diff --git a/meta-ads/README.md b/meta-ads/README.md index f390554b..6ff6549a 100644 --- a/meta-ads/README.md +++ b/meta-ads/README.md @@ -1,13 +1,17 @@ -# Meta Ads Analytics MCP +# Meta Ads MCP -MCP for performance analysis of Meta/Facebook Ads campaigns. +Complete MCP for managing and analyzing Meta/Facebook Ads campaigns. ## Features -This MCP provides tools to analyze the performance of your Meta (Facebook/Instagram) advertising campaigns: +This MCP provides comprehensive tools to **create, manage, and analyze** your Meta (Facebook/Instagram) advertising campaigns: -- **View performance** of campaigns, ad sets, and ads -- **Get detailed metrics** with breakdowns by age, gender, country, device, etc. +- **Create campaigns** with objectives, budgets, and scheduling +- **Create ad sets** with targeting, optimization, and bidding +- **Create ads** with creatives, images, and call-to-actions +- **Update/pause/delete** campaigns, ad sets, and ads +- **Upload images** for use in ad creatives +- **View performance** metrics with breakdowns by age, gender, country, device, etc. - **Compare performance** between periods - **Analyze ROI and costs** at different levels @@ -32,24 +36,39 @@ This MCP provides tools to analyze the performance of your Meta (Facebook/Instag |------|-------------| | `META_ADS_GET_ACCOUNT_INFO` | Account details (currency, timezone, status) - works with both token types | -### Campaigns (2 tools) +### Campaigns (5 tools) | Tool | Description | |------|-------------| | `META_ADS_GET_CAMPAIGNS` | List campaigns with status filter | | `META_ADS_GET_CAMPAIGN_DETAILS` | Details of a specific campaign | +| `META_ADS_CREATE_CAMPAIGN` | Create a new campaign with objective, budget, and schedule | +| `META_ADS_UPDATE_CAMPAIGN` | Update/pause/activate a campaign | +| `META_ADS_DELETE_CAMPAIGN` | Delete a campaign | -### Ad Sets (2 tools) +### Ad Sets (5 tools) | Tool | Description | |------|-------------| | `META_ADS_GET_ADSETS` | List ad sets with campaign filter | | `META_ADS_GET_ADSET_DETAILS` | Ad set details (targeting, budget) | +| `META_ADS_CREATE_ADSET` | Create ad set with targeting, budget, and optimization | +| `META_ADS_UPDATE_ADSET` | Update/pause/activate an ad set | +| `META_ADS_DELETE_ADSET` | Delete an ad set | -### Ads (3 tools) +### Ads (6 tools) | Tool | Description | |------|-------------| | `META_ADS_GET_ADS` | List ads with ad set filter | | `META_ADS_GET_AD_DETAILS` | Ad details | -| `META_ADS_GET_AD_CREATIVES` | Ad creatives | +| `META_ADS_GET_AD_CREATIVES` | Get creative details for an ad | +| `META_ADS_CREATE_AD` | Create a new ad with a creative | +| `META_ADS_UPDATE_AD` | Update/pause/activate an ad | +| `META_ADS_DELETE_AD` | Delete an ad | + +### Creatives & Images (2 tools) +| Tool | Description | +|------|-------------| +| `META_ADS_UPLOAD_AD_IMAGE` | Upload an image for use in creatives | +| `META_ADS_CREATE_AD_CREATIVE` | Create a creative with image, text, and CTA | ### Insights (1 tool) | Tool | Description | @@ -151,11 +170,48 @@ bun run build bun run publish ``` +## Creating a Complete Campaign + +To create a full campaign from scratch, follow this flow: + +``` +1. Create Campaign + -> META_ADS_CREATE_CAMPAIGN(account_id, name, objective, ...) + +2. Create Ad Set with targeting + -> META_ADS_CREATE_ADSET(account_id, campaign_id, targeting, ...) + +3. Upload image (if needed) + -> META_ADS_UPLOAD_AD_IMAGE(account_id, image_url) + +4. Create Ad Creative + -> META_ADS_CREATE_AD_CREATIVE(account_id, page_id, link, image_hash, ...) + +5. Create Ad + -> META_ADS_CREATE_AD(account_id, adset_id, creative_id, ...) + +6. Activate (when ready) + -> META_ADS_UPDATE_CAMPAIGN(campaign_id, status: "ACTIVE") +``` + +### Campaign Objectives + +| Objective | Use Case | +|-----------|----------| +| `OUTCOME_TRAFFIC` | Drive website visits | +| `OUTCOME_ENGAGEMENT` | Get likes, comments, shares | +| `OUTCOME_LEADS` | Collect leads via forms | +| `OUTCOME_SALES` | Drive purchases/conversions | +| `OUTCOME_AWARENESS` | Reach and brand awareness | +| `OUTCOME_APP_PROMOTION` | App installs | + ## Usage Examples +### Reading Data + ``` 1. "List my ad accounts" - -> META_ADS_GET_AD_ACCOUNTS + -> META_ADS_GET_USER_AD_ACCOUNTS 2. "Show active campaigns for account act_123" -> META_ADS_GET_CAMPAIGNS(account_id: "act_123", status_filter: "ACTIVE") @@ -169,3 +225,71 @@ bun run publish 5. "Which ads have the best CTR?" -> META_ADS_GET_ADS + META_ADS_GET_INSIGHTS for each ``` + +### Creating Campaigns + +``` +1. "Create a traffic campaign for my website" + -> META_ADS_CREATE_CAMPAIGN( + account_id: "act_123", + name: "Website Traffic Q1", + objective: "OUTCOME_TRAFFIC", + status: "PAUSED" + ) + +2. "Create an ad set targeting 25-45 year olds in Brazil" + -> META_ADS_CREATE_ADSET( + account_id: "act_123", + campaign_id: "123456", + name: "Brazil Adults", + targeting: { + age_min: 25, + age_max: 45, + geo_locations: { countries: ["BR"] } + }, + optimization_goal: "LINK_CLICKS", + billing_event: "IMPRESSIONS", + daily_budget: "5000" + ) + +3. "Upload an image for my ad" + -> META_ADS_UPLOAD_AD_IMAGE( + account_id: "act_123", + image_url: "https://example.com/my-ad-image.jpg" + ) + +4. "Create a creative with the uploaded image" + -> META_ADS_CREATE_AD_CREATIVE( + account_id: "act_123", + page_id: "page_123", + link: "https://mysite.com", + message: "Check out our amazing products!", + headline: "Limited Time Offer", + image_hash: "abc123hash", + call_to_action_type: "SHOP_NOW" + ) + +5. "Create an ad using the creative" + -> META_ADS_CREATE_AD( + account_id: "act_123", + adset_id: "adset_456", + name: "Shop Now Ad", + creative_id: "creative_789" + ) +``` + +### Managing Campaigns + +``` +1. "Pause campaign X" + -> META_ADS_UPDATE_CAMPAIGN(campaign_id: "123", status: "PAUSED") + +2. "Increase daily budget to $100" + -> META_ADS_UPDATE_ADSET(adset_id: "456", daily_budget: "10000") + +3. "Activate my ad" + -> META_ADS_UPDATE_AD(ad_id: "789", status: "ACTIVE") + +4. "Delete old campaign" + -> META_ADS_DELETE_CAMPAIGN(campaign_id: "123") +``` diff --git a/meta-ads/server/lib/meta-client.ts b/meta-ads/server/lib/meta-client.ts index fb355548..0692988d 100644 --- a/meta-ads/server/lib/meta-client.ts +++ b/meta-ads/server/lib/meta-client.ts @@ -16,6 +16,16 @@ import type { Page, PaginatedResponse, ApiError, + CreateCampaignParams, + UpdateCampaignParams, + CreateAdSetParams, + UpdateAdSetParams, + CreateAdParams, + UpdateAdParams, + CreateAdCreativeParams, + MutationResponse, + DeleteResponse, + ImageUploadResponse, } from "./types.ts"; export interface MetaClientConfig { @@ -469,6 +479,100 @@ export class MetaAdsClient { }); } + /** + * Create a new campaign + */ + async createCampaign( + accountId: string, + params: CreateCampaignParams, + ): Promise { + const formattedId = accountId.startsWith("act_") + ? accountId + : `act_${accountId}`; + + const body: Record = { + name: params.name, + objective: params.objective, + status: params.status || "PAUSED", + special_ad_categories: params.special_ad_categories || [], + }; + + // Add optional budget fields + if (params.daily_budget) { + body.daily_budget = params.daily_budget; + } + if (params.lifetime_budget) { + body.lifetime_budget = params.lifetime_budget; + } + if (params.start_time) { + body.start_time = params.start_time; + } + if (params.stop_time) { + body.stop_time = params.stop_time; + } + if (params.buying_type) { + body.buying_type = params.buying_type; + } + if (params.bid_strategy) { + body.bid_strategy = params.bid_strategy; + } + + return makeRequest( + this.config, + `/${formattedId}/campaigns`, + { + method: "POST", + body, + }, + ); + } + + /** + * Update an existing campaign + */ + async updateCampaign( + campaignId: string, + params: UpdateCampaignParams, + ): Promise { + const body: Record = {}; + + if (params.name !== undefined) { + body.name = params.name; + } + if (params.status !== undefined) { + body.status = params.status; + } + if (params.daily_budget !== undefined) { + body.daily_budget = params.daily_budget; + } + if (params.lifetime_budget !== undefined) { + body.lifetime_budget = params.lifetime_budget; + } + if (params.start_time !== undefined) { + body.start_time = params.start_time; + } + if (params.stop_time !== undefined) { + body.stop_time = params.stop_time; + } + if (params.bid_strategy !== undefined) { + body.bid_strategy = params.bid_strategy; + } + + return makeRequest(this.config, `/${campaignId}`, { + method: "POST", + body, + }); + } + + /** + * Delete a campaign + */ + async deleteCampaign(campaignId: string): Promise { + return makeRequest(this.config, `/${campaignId}`, { + method: "DELETE", + }); + } + // ============ AdSet Methods ============ /** @@ -524,6 +628,123 @@ export class MetaAdsClient { }); } + /** + * Create a new ad set + */ + async createAdSet( + accountId: string, + params: CreateAdSetParams, + ): Promise { + const formattedId = accountId.startsWith("act_") + ? accountId + : `act_${accountId}`; + + const body: Record = { + campaign_id: params.campaign_id, + name: params.name, + status: params.status || "PAUSED", + targeting: params.targeting, + optimization_goal: params.optimization_goal, + billing_event: params.billing_event, + }; + + // Add optional fields + if (params.bid_strategy) { + body.bid_strategy = params.bid_strategy; + } + if (params.bid_amount) { + body.bid_amount = params.bid_amount; + } + if (params.daily_budget) { + body.daily_budget = params.daily_budget; + } + if (params.lifetime_budget) { + body.lifetime_budget = params.lifetime_budget; + } + if (params.start_time) { + body.start_time = params.start_time; + } + if (params.end_time) { + body.end_time = params.end_time; + } + if (params.promoted_object) { + body.promoted_object = params.promoted_object; + } + if (params.destination_type) { + body.destination_type = params.destination_type; + } + if (params.attribution_spec) { + body.attribution_spec = params.attribution_spec; + } + + return makeRequest( + this.config, + `/${formattedId}/adsets`, + { + method: "POST", + body, + }, + ); + } + + /** + * Update an existing ad set + */ + async updateAdSet( + adsetId: string, + params: UpdateAdSetParams, + ): Promise { + const body: Record = {}; + + if (params.name !== undefined) { + body.name = params.name; + } + if (params.status !== undefined) { + body.status = params.status; + } + if (params.targeting !== undefined) { + body.targeting = params.targeting; + } + if (params.optimization_goal !== undefined) { + body.optimization_goal = params.optimization_goal; + } + if (params.billing_event !== undefined) { + body.billing_event = params.billing_event; + } + if (params.bid_strategy !== undefined) { + body.bid_strategy = params.bid_strategy; + } + if (params.bid_amount !== undefined) { + body.bid_amount = params.bid_amount; + } + if (params.daily_budget !== undefined) { + body.daily_budget = params.daily_budget; + } + if (params.lifetime_budget !== undefined) { + body.lifetime_budget = params.lifetime_budget; + } + if (params.start_time !== undefined) { + body.start_time = params.start_time; + } + if (params.end_time !== undefined) { + body.end_time = params.end_time; + } + + return makeRequest(this.config, `/${adsetId}`, { + method: "POST", + body, + }); + } + + /** + * Delete an ad set + */ + async deleteAdSet(adsetId: string): Promise { + return makeRequest(this.config, `/${adsetId}`, { + method: "DELETE", + }); + } + // ============ Ad Methods ============ /** @@ -614,6 +835,217 @@ export class MetaAdsClient { }); } + /** + * Create a new ad + */ + async createAd( + accountId: string, + params: CreateAdParams, + ): Promise { + const formattedId = accountId.startsWith("act_") + ? accountId + : `act_${accountId}`; + + const body: Record = { + adset_id: params.adset_id, + name: params.name, + status: params.status || "PAUSED", + creative: params.creative, + }; + + if (params.tracking_specs) { + body.tracking_specs = params.tracking_specs; + } + if (params.conversion_domain) { + body.conversion_domain = params.conversion_domain; + } + + return makeRequest(this.config, `/${formattedId}/ads`, { + method: "POST", + body, + }); + } + + /** + * Update an existing ad + */ + async updateAd( + adId: string, + params: UpdateAdParams, + ): Promise { + const body: Record = {}; + + if (params.name !== undefined) { + body.name = params.name; + } + if (params.status !== undefined) { + body.status = params.status; + } + if (params.creative !== undefined) { + body.creative = params.creative; + } + + return makeRequest(this.config, `/${adId}`, { + method: "POST", + body, + }); + } + + /** + * Delete an ad + */ + async deleteAd(adId: string): Promise { + return makeRequest(this.config, `/${adId}`, { + method: "DELETE", + }); + } + + /** + * Upload an image to use in ad creatives + * @param accountId - The ad account ID + * @param imageUrl - URL of the image to upload (Meta will fetch it) + * @returns Image hash and URL + */ + async uploadAdImage( + accountId: string, + imageUrl: string, + ): Promise { + const formattedId = accountId.startsWith("act_") + ? accountId + : `act_${accountId}`; + + const response = await makeRequest<{ + images: Record< + string, + { hash: string; url: string; width?: number; height?: number } + >; + }>(this.config, `/${formattedId}/adimages`, { + method: "POST", + body: { + url: imageUrl, + }, + }); + + // The response contains the image info keyed by filename + const imageKey = Object.keys(response.images)[0]; + const imageData = response.images[imageKey]; + + return { + hash: imageData.hash, + url: imageData.url, + width: imageData.width, + height: imageData.height, + }; + } + + /** + * Upload an image using base64 encoded bytes + * @param accountId - The ad account ID + * @param imageBytes - Base64 encoded image bytes + * @param filename - Optional filename for the image + * @returns Image hash and URL + */ + async uploadAdImageBytes( + accountId: string, + imageBytes: string, + filename?: string, + ): Promise { + const formattedId = accountId.startsWith("act_") + ? accountId + : `act_${accountId}`; + + const body: Record = { + bytes: imageBytes, + }; + + if (filename) { + body.filename = filename; + } + + const response = await makeRequest<{ + images: Record< + string, + { hash: string; url: string; width?: number; height?: number } + >; + }>(this.config, `/${formattedId}/adimages`, { + method: "POST", + body, + }); + + const imageKey = Object.keys(response.images)[0]; + const imageData = response.images[imageKey]; + + return { + hash: imageData.hash, + url: imageData.url, + width: imageData.width, + height: imageData.height, + }; + } + + /** + * Create an ad creative + */ + async createAdCreative( + accountId: string, + params: CreateAdCreativeParams, + ): Promise { + const formattedId = accountId.startsWith("act_") + ? accountId + : `act_${accountId}`; + + const body: Record = {}; + + if (params.name) { + body.name = params.name; + } + if (params.object_story_spec) { + body.object_story_spec = params.object_story_spec; + } + if (params.effective_object_story_id) { + body.effective_object_story_id = params.effective_object_story_id; + } + if (params.source_instagram_media_id) { + body.source_instagram_media_id = params.source_instagram_media_id; + } + if (params.object_url) { + body.object_url = params.object_url; + } + if (params.title) { + body.title = params.title; + } + if (params.body) { + body.body = params.body; + } + if (params.image_hash) { + body.image_hash = params.image_hash; + } + if (params.video_id) { + body.video_id = params.video_id; + } + if (params.link_url) { + body.link_url = params.link_url; + } + if (params.call_to_action_type) { + body.call_to_action_type = params.call_to_action_type; + } + if (params.url_tags) { + body.url_tags = params.url_tags; + } + if (params.degrees_of_freedom_spec) { + body.degrees_of_freedom_spec = params.degrees_of_freedom_spec; + } + + return makeRequest( + this.config, + `/${formattedId}/adcreatives`, + { + method: "POST", + body, + }, + ); + } + // ============ Insights Methods ============ /** diff --git a/meta-ads/server/lib/types.ts b/meta-ads/server/lib/types.ts index 43a86a62..3b83c494 100644 --- a/meta-ads/server/lib/types.ts +++ b/meta-ads/server/lib/types.ts @@ -269,3 +269,385 @@ export interface ApiError { fbtrace_id?: string; }; } + +// ============ Create/Update Parameter Types ============ + +// Campaign objectives (Meta Ads API v18+) +export type CampaignObjective = + | "OUTCOME_AWARENESS" + | "OUTCOME_ENGAGEMENT" + | "OUTCOME_LEADS" + | "OUTCOME_SALES" + | "OUTCOME_TRAFFIC" + | "OUTCOME_APP_PROMOTION"; + +// Special ad categories +export type SpecialAdCategory = + | "NONE" + | "HOUSING" + | "EMPLOYMENT" + | "CREDIT" + | "ISSUES_ELECTIONS_POLITICS"; + +// Campaign creation params +export interface CreateCampaignParams { + name: string; + objective: CampaignObjective; + status?: CampaignStatus; + special_ad_categories?: SpecialAdCategory[]; + daily_budget?: string; + lifetime_budget?: string; + start_time?: string; + stop_time?: string; + buying_type?: "AUCTION" | "RESERVED"; + bid_strategy?: + | "LOWEST_COST_WITHOUT_CAP" + | "LOWEST_COST_WITH_BID_CAP" + | "COST_CAP"; +} + +// Campaign update params +export interface UpdateCampaignParams { + name?: string; + status?: CampaignStatus; + daily_budget?: string; + lifetime_budget?: string; + start_time?: string; + stop_time?: string; + bid_strategy?: string; +} + +// AdSet status type +export type AdSetStatus = "ACTIVE" | "PAUSED" | "DELETED" | "ARCHIVED"; + +// Optimization goals +export type OptimizationGoal = + | "NONE" + | "APP_INSTALLS" + | "AD_RECALL_LIFT" + | "ENGAGED_USERS" + | "EVENT_RESPONSES" + | "IMPRESSIONS" + | "LEAD_GENERATION" + | "QUALITY_LEAD" + | "LINK_CLICKS" + | "OFFSITE_CONVERSIONS" + | "PAGE_LIKES" + | "POST_ENGAGEMENT" + | "QUALITY_CALL" + | "REACH" + | "LANDING_PAGE_VIEWS" + | "VISIT_INSTAGRAM_PROFILE" + | "VALUE" + | "THRUPLAY" + | "DERIVED_EVENTS" + | "APP_INSTALLS_AND_OFFSITE_CONVERSIONS" + | "CONVERSATIONS" + | "IN_APP_VALUE" + | "MESSAGING_PURCHASE_CONVERSION" + | "SUBSCRIBERS" + | "REMINDERS_SET" + | "MEANINGFUL_CALL_ATTEMPT" + | "PROFILE_VISIT" + | "MESSAGING_APPOINTMENT_CONVERSION"; + +// Billing events +export type BillingEvent = + | "APP_INSTALLS" + | "CLICKS" + | "IMPRESSIONS" + | "LINK_CLICKS" + | "NONE" + | "OFFER_CLAIMS" + | "PAGE_LIKES" + | "POST_ENGAGEMENT" + | "THRUPLAY" + | "PURCHASE" + | "LISTING_INTERACTION"; + +// Bid strategy +export type BidStrategy = + | "LOWEST_COST_WITHOUT_CAP" + | "LOWEST_COST_WITH_BID_CAP" + | "COST_CAP" + | "LOWEST_COST_WITH_MIN_ROAS"; + +// Targeting for creation (simplified for input) +export interface TargetingInput { + age_min?: number; + age_max?: number; + genders?: number[]; // 1 = male, 2 = female + geo_locations?: { + countries?: string[]; + regions?: Array<{ key: string }>; + cities?: Array<{ key: string; radius?: number; distance_unit?: string }>; + location_types?: string[]; + }; + interests?: Array<{ id: string }>; + behaviors?: Array<{ id: string }>; + custom_audiences?: Array<{ id: string }>; + excluded_custom_audiences?: Array<{ id: string }>; + publisher_platforms?: Array< + "facebook" | "instagram" | "audience_network" | "messenger" + >; + facebook_positions?: string[]; + instagram_positions?: string[]; + device_platforms?: Array<"mobile" | "desktop">; + flexible_spec?: Array<{ + interests?: Array<{ id: string }>; + behaviors?: Array<{ id: string }>; + }>; + exclusions?: { + interests?: Array<{ id: string }>; + behaviors?: Array<{ id: string }>; + }; +} + +// AdSet creation params +export interface CreateAdSetParams { + campaign_id: string; + name: string; + status?: AdSetStatus; + targeting: TargetingInput; + optimization_goal: OptimizationGoal; + billing_event: BillingEvent; + bid_strategy?: BidStrategy; + bid_amount?: string; + daily_budget?: string; + lifetime_budget?: string; + start_time?: string; + end_time?: string; + promoted_object?: { + page_id?: string; + pixel_id?: string; + application_id?: string; + object_store_url?: string; + custom_event_type?: string; + offer_id?: string; + product_set_id?: string; + }; + destination_type?: + | "WEBSITE" + | "APP" + | "MESSENGER" + | "APPLINKS_AUTOMATIC" + | "WHATSAPP" + | "INSTAGRAM_DIRECT" + | "FACEBOOK"; + attribution_spec?: Array<{ + event_type: string; + window_days: number; + }>; +} + +// AdSet update params +export interface UpdateAdSetParams { + name?: string; + status?: AdSetStatus; + targeting?: TargetingInput; + optimization_goal?: OptimizationGoal; + billing_event?: BillingEvent; + bid_strategy?: BidStrategy; + bid_amount?: string; + daily_budget?: string; + lifetime_budget?: string; + start_time?: string; + end_time?: string; +} + +// Ad status type +export type AdStatus = "ACTIVE" | "PAUSED" | "DELETED" | "ARCHIVED"; + +// Ad creation params +export interface CreateAdParams { + adset_id: string; + name: string; + status?: AdStatus; + creative: { + creative_id: string; + }; + tracking_specs?: Array>; + conversion_domain?: string; +} + +// Ad update params +export interface UpdateAdParams { + name?: string; + status?: AdStatus; + creative?: { + creative_id: string; + }; +} + +// Call to action types +export type CallToActionType = + | "ADD_TO_CART" + | "APPLY_NOW" + | "BOOK_TRAVEL" + | "BUY" + | "BUY_NOW" + | "BUY_TICKETS" + | "CALL" + | "CALL_ME" + | "CONTACT" + | "CONTACT_US" + | "DONATE" + | "DONATE_NOW" + | "DOWNLOAD" + | "EVENT_RSVP" + | "FIND_A_GROUP" + | "FIND_YOUR_GROUPS" + | "FOLLOW_NEWS_STORYLINE" + | "FOLLOW_PAGE" + | "FOLLOW_USER" + | "GET_DIRECTIONS" + | "GET_OFFER" + | "GET_OFFER_VIEW" + | "GET_QUOTE" + | "GET_SHOWTIMES" + | "INSTALL_APP" + | "INSTALL_MOBILE_APP" + | "LEARN_MORE" + | "LIKE_PAGE" + | "LISTEN_MUSIC" + | "LISTEN_NOW" + | "MESSAGE_PAGE" + | "MOBILE_DOWNLOAD" + | "MOMENTS" + | "NO_BUTTON" + | "OPEN_LINK" + | "ORDER_NOW" + | "PAY_TO_ACCESS" + | "PLAY_GAME" + | "PLAY_GAME_ON_FACEBOOK" + | "PURCHASE_GIFT_CARDS" + | "RECORD_NOW" + | "REFER_FRIENDS" + | "REQUEST_TIME" + | "SAY_THANKS" + | "SEE_MORE" + | "SELL_NOW" + | "SEND_A_GIFT" + | "SEND_GIFT_MONEY" + | "SHARE" + | "SHOP_NOW" + | "SIGN_UP" + | "SOTTO_SUBSCRIBE" + | "START_ORDER" + | "SUBSCRIBE" + | "SWIPE_UP_PRODUCT" + | "SWIPE_UP_SHOP" + | "UPDATE_APP" + | "USE_APP" + | "USE_MOBILE_APP" + | "VIDEO_ANNOTATION" + | "VIDEO_CALL" + | "VISIT_PAGES_FEED" + | "WATCH_MORE" + | "WATCH_VIDEO" + | "WHATSAPP_MESSAGE" + | "WOODHENGE_SUPPORT"; + +// Object story spec for creative +export interface ObjectStorySpec { + page_id: string; + link_data?: { + link: string; + message?: string; + name?: string; + description?: string; + caption?: string; + image_hash?: string; + video_id?: string; + call_to_action?: { + type: CallToActionType; + value?: { + link?: string; + link_caption?: string; + lead_gen_form_id?: string; + app_destination?: string; + }; + }; + multi_share_end_card?: boolean; + multi_share_optimized?: boolean; + child_attachments?: Array<{ + link: string; + name?: string; + description?: string; + image_hash?: string; + video_id?: string; + call_to_action?: { + type: CallToActionType; + value?: { + link?: string; + }; + }; + }>; + }; + photo_data?: { + image_hash: string; + caption?: string; + url?: string; + }; + video_data?: { + video_id: string; + title?: string; + message?: string; + image_hash?: string; + call_to_action?: { + type: CallToActionType; + value?: { + link?: string; + }; + }; + }; + text_data?: { + message: string; + }; +} + +// Ad Creative creation params +export interface CreateAdCreativeParams { + name?: string; + object_story_spec?: ObjectStorySpec; + degrees_of_freedom_spec?: { + creative_features_spec?: { + standard_enhancements?: { + enroll_status: "OPT_IN" | "OPT_OUT"; + }; + }; + }; + // For using existing post + effective_object_story_id?: string; + // Alternative: source_instagram_media_id for Instagram posts + source_instagram_media_id?: string; + // URL-based creative + object_url?: string; + // Additional fields + title?: string; + body?: string; + image_hash?: string; + video_id?: string; + link_url?: string; + call_to_action_type?: CallToActionType; + url_tags?: string; +} + +// Image upload response +export interface ImageUploadResponse { + hash: string; + url: string; + width?: number; + height?: number; +} + +// API mutation response (create/update/delete) +export interface MutationResponse { + id?: string; + success?: boolean; +} + +// Delete response +export interface DeleteResponse { + success: boolean; +} diff --git a/meta-ads/server/tools/ads.ts b/meta-ads/server/tools/ads.ts index db38c02b..43a5adfd 100644 --- a/meta-ads/server/tools/ads.ts +++ b/meta-ads/server/tools/ads.ts @@ -5,6 +5,11 @@ * - META_ADS_GET_ADS: List ads with optional filtering by campaign/adset * - META_ADS_GET_AD_DETAILS: Get detailed info about a specific ad * - META_ADS_GET_AD_CREATIVES: Get creative details for an ad + * - META_ADS_CREATE_AD: Create a new ad + * - META_ADS_UPDATE_AD: Update an existing ad + * - META_ADS_DELETE_AD: Delete an ad + * - META_ADS_UPLOAD_AD_IMAGE: Upload an image for use in creatives + * - META_ADS_CREATE_AD_CREATIVE: Create a new ad creative */ import { createPrivateTool } from "@decocms/runtime/tools"; @@ -12,6 +17,7 @@ import { z } from "zod"; import type { Env } from "../main.ts"; import { getMetaAccessToken } from "../main.ts"; import { createMetaAdsClient } from "../lib/meta-client.ts"; +import type { AdStatus, UpdateAdParams } from "../lib/types.ts"; /** * Get ads for an ad account @@ -168,9 +174,353 @@ export const createGetAdCreativesTool = (env: Env) => }, }); +/** + * Create a new ad + */ +export const createCreateAdTool = (env: Env) => + createPrivateTool({ + id: "META_ADS_CREATE_AD", + description: + "Create a new Meta Ads ad. This is STEP 5 (final step) to create ads. REQUIRES: adset_id from CREATE_ADSET AND creative_id from CREATE_AD_CREATIVE. FLOW: 1) CREATE_CAMPAIGN → 2) CREATE_ADSET → 3) UPLOAD_AD_IMAGE (optional) → 4) CREATE_AD_CREATIVE → 5) CREATE_AD. If you don't have these IDs, go back and create them first.", + inputSchema: z.object({ + account_id: z + .string() + .describe("Meta Ads account ID (format: act_XXXXXXXXX)"), + adset_id: z.string().describe("Ad set ID to create the ad in"), + name: z.string().describe("Ad name"), + creative_id: z + .string() + .describe( + "Creative ID to use for this ad (created via META_ADS_CREATE_AD_CREATIVE)", + ), + status: z + .enum(["ACTIVE", "PAUSED"]) + .optional() + .default("PAUSED") + .describe("Ad status (default: PAUSED)"), + conversion_domain: z + .string() + .optional() + .describe("Domain for conversion tracking (e.g., 'example.com')"), + }), + outputSchema: z.object({ + id: z.string().describe("ID of the created ad"), + success: z.boolean().describe("Whether the ad was created successfully"), + }), + execute: async ({ context }) => { + const accessToken = await getMetaAccessToken(env); + const client = createMetaAdsClient({ accessToken }); + + const response = await client.createAd(context.account_id, { + adset_id: context.adset_id, + name: context.name, + status: context.status, + creative: { + creative_id: context.creative_id, + }, + conversion_domain: context.conversion_domain, + }); + + return { + id: response.id || "", + success: !!response.id, + }; + }, + }); + +/** + * Update an existing ad + */ +export const createUpdateAdTool = (env: Env) => + createPrivateTool({ + id: "META_ADS_UPDATE_AD", + description: + "Update an existing Meta Ads ad. Can change name, status, or creative.", + inputSchema: z.object({ + ad_id: z.string().describe("Ad ID to update"), + name: z.string().optional().describe("New ad name"), + status: z + .enum(["ACTIVE", "PAUSED", "DELETED", "ARCHIVED"]) + .optional() + .describe("New status. Use PAUSED to pause, ACTIVE to activate."), + creative_id: z.string().optional().describe("New creative ID to use"), + }), + outputSchema: z.object({ + success: z.boolean().describe("Whether the update was successful"), + }), + execute: async ({ context }) => { + const accessToken = await getMetaAccessToken(env); + const client = createMetaAdsClient({ accessToken }); + + const params: UpdateAdParams = {}; + if (context.name !== undefined) params.name = context.name; + if (context.status !== undefined) + params.status = context.status as AdStatus; + if (context.creative_id !== undefined) { + params.creative = { creative_id: context.creative_id }; + } + + const response = await client.updateAd(context.ad_id as string, params); + + return { + success: response.success ?? true, + }; + }, + }); + +/** + * Delete an ad + */ +export const createDeleteAdTool = (env: Env) => + createPrivateTool({ + id: "META_ADS_DELETE_AD", + description: "Delete a Meta Ads ad. This action cannot be undone.", + inputSchema: z.object({ + ad_id: z.string().describe("Ad ID to delete"), + }), + outputSchema: z.object({ + success: z.boolean().describe("Whether the deletion was successful"), + }), + execute: async ({ context }) => { + const accessToken = await getMetaAccessToken(env); + const client = createMetaAdsClient({ accessToken }); + + const response = await client.deleteAd(context.ad_id); + + return { + success: response.success, + }; + }, + }); + +/** + * Upload an image for use in ad creatives + */ +export const createUploadAdImageTool = (env: Env) => + createPrivateTool({ + id: "META_ADS_UPLOAD_AD_IMAGE", + description: + "Upload an image for ad creatives. This is STEP 3 (optional) in the ad creation flow. FLOW: 1) CREATE_CAMPAIGN → 2) CREATE_ADSET → 3) UPLOAD_AD_IMAGE → 4) CREATE_AD_CREATIVE → 5) CREATE_AD. Returns an image_hash to use in CREATE_AD_CREATIVE. Skip this step if using an existing post (effective_object_story_id) or video.", + inputSchema: z.object({ + account_id: z + .string() + .describe("Meta Ads account ID (format: act_XXXXXXXXX)"), + image_url: z + .string() + .describe( + "Publicly accessible URL of the image to upload. Meta will fetch and store the image. Supported formats: JPG, PNG. Recommended size: 1200x628 for link ads, 1080x1080 for square.", + ), + }), + outputSchema: z.object({ + image_hash: z.string().describe("Hash to use when creating ad creatives"), + url: z.string().describe("URL of the uploaded image on Meta's CDN"), + width: z.number().optional().describe("Image width in pixels"), + height: z.number().optional().describe("Image height in pixels"), + }), + execute: async ({ context }) => { + const accessToken = await getMetaAccessToken(env); + const client = createMetaAdsClient({ accessToken }); + + const response = await client.uploadAdImage( + context.account_id, + context.image_url, + ); + + return { + image_hash: response.hash, + url: response.url, + width: response.width, + height: response.height, + }; + }, + }); + +// Call to action types schema +const callToActionTypeSchema = z.enum([ + "APPLY_NOW", + "BOOK_TRAVEL", + "BUY_NOW", + "BUY_TICKETS", + "CALL", + "CONTACT_US", + "DONATE", + "DONATE_NOW", + "DOWNLOAD", + "GET_DIRECTIONS", + "GET_OFFER", + "GET_QUOTE", + "GET_SHOWTIMES", + "INSTALL_APP", + "LEARN_MORE", + "LISTEN_NOW", + "MESSAGE_PAGE", + "NO_BUTTON", + "OPEN_LINK", + "ORDER_NOW", + "PLAY_GAME", + "SHOP_NOW", + "SIGN_UP", + "SUBSCRIBE", + "USE_APP", + "WATCH_MORE", + "WATCH_VIDEO", + "WHATSAPP_MESSAGE", +]); + +/** + * Create an ad creative + */ +export const createCreateAdCreativeTool = (env: Env) => + createPrivateTool({ + id: "META_ADS_CREATE_AD_CREATIVE", + description: + "Create an ad creative with image, text, and CTA. This is STEP 4 in the ad creation flow. REQUIRES: page_id (Facebook Page), and either image_hash from UPLOAD_AD_IMAGE OR effective_object_story_id (existing post). FLOW: 1) CREATE_CAMPAIGN → 2) CREATE_ADSET → 3) UPLOAD_AD_IMAGE → 4) CREATE_AD_CREATIVE → 5) CREATE_AD. Returns creative_id to use in CREATE_AD.", + inputSchema: z.object({ + account_id: z + .string() + .describe("Meta Ads account ID (format: act_XXXXXXXXX)"), + name: z + .string() + .optional() + .describe("Creative name for internal reference"), + // Option 1: Use object_story_spec for new creatives + page_id: z + .string() + .optional() + .describe( + "Facebook Page ID associated with the ad (required for most ad types)", + ), + link: z + .string() + .optional() + .describe("Destination URL when users click the ad"), + message: z + .string() + .optional() + .describe("Primary text that appears above the image/video"), + headline: z + .string() + .optional() + .describe("Headline text that appears below the image"), + description: z + .string() + .optional() + .describe("Description text (appears below headline)"), + image_hash: z + .string() + .optional() + .describe("Image hash from META_ADS_UPLOAD_AD_IMAGE"), + video_id: z + .string() + .optional() + .describe("Video ID if using video creative"), + call_to_action_type: callToActionTypeSchema + .optional() + .describe( + "Call to action button type (e.g., LEARN_MORE, SHOP_NOW, SIGN_UP)", + ), + // Option 2: Use existing post + effective_object_story_id: z + .string() + .optional() + .describe( + "Use an existing Facebook/Instagram post as the creative. Format: page_id_post_id", + ), + // Option 3: Instagram media + source_instagram_media_id: z + .string() + .optional() + .describe("Instagram post ID to use as creative"), + // Additional options + url_tags: z + .string() + .optional() + .describe( + "URL parameters to append to all links (e.g., 'utm_source=facebook&utm_medium=ad')", + ), + }), + outputSchema: z.object({ + id: z.string().describe("ID of the created creative"), + success: z + .boolean() + .describe("Whether the creative was created successfully"), + }), + execute: async ({ context }) => { + const accessToken = await getMetaAccessToken(env); + const client = createMetaAdsClient({ accessToken }); + + // Build the creative params + const params: Record = {}; + + if (context.name) { + params.name = context.name; + } + + // If using existing post + if (context.effective_object_story_id) { + params.effective_object_story_id = context.effective_object_story_id; + } else if (context.source_instagram_media_id) { + params.source_instagram_media_id = context.source_instagram_media_id; + } else if (context.page_id && context.link) { + // Build object_story_spec for new creative + const linkData: Record = { + link: context.link, + }; + + if (context.message) { + linkData.message = context.message; + } + if (context.headline) { + linkData.name = context.headline; + } + if (context.description) { + linkData.description = context.description; + } + if (context.image_hash) { + linkData.image_hash = context.image_hash; + } + if (context.video_id) { + linkData.video_id = context.video_id; + } + if (context.call_to_action_type) { + linkData.call_to_action = { + type: context.call_to_action_type, + value: { + link: context.link, + }, + }; + } + + params.object_story_spec = { + page_id: context.page_id, + link_data: linkData, + }; + } + + if (context.url_tags) { + params.url_tags = context.url_tags; + } + + const response = await client.createAdCreative( + context.account_id, + params, + ); + + return { + id: response.id || "", + success: !!response.id, + }; + }, + }); + // Export all ad tools export const adTools = [ createGetAdsTool, createGetAdDetailsTool, createGetAdCreativesTool, + createCreateAdTool, + createUpdateAdTool, + createDeleteAdTool, + createUploadAdImageTool, + createCreateAdCreativeTool, ]; diff --git a/meta-ads/server/tools/adsets.ts b/meta-ads/server/tools/adsets.ts index 1749a59b..d98d0e3f 100644 --- a/meta-ads/server/tools/adsets.ts +++ b/meta-ads/server/tools/adsets.ts @@ -4,6 +4,9 @@ * Tools: * - META_ADS_GET_ADSETS: List ad sets with optional filtering by campaign * - META_ADS_GET_ADSET_DETAILS: Get detailed info about a specific ad set + * - META_ADS_CREATE_ADSET: Create a new ad set + * - META_ADS_UPDATE_ADSET: Update an existing ad set + * - META_ADS_DELETE_ADSET: Delete an ad set */ import { createPrivateTool } from "@decocms/runtime/tools"; @@ -196,5 +199,349 @@ export const createGetAdSetDetailsTool = (env: Env) => }, }); +// Targeting input schema for creating/updating ad sets +const targetingInputSchema = z.object({ + age_min: z.number().optional().describe("Minimum age (18-65)"), + age_max: z.number().optional().describe("Maximum age (18-65)"), + genders: z + .array(z.number()) + .optional() + .describe("Gender targeting: 1 = male, 2 = female. Empty for all."), + geo_locations: z + .object({ + countries: z + .array(z.string()) + .optional() + .describe("Array of country codes (e.g., ['US', 'BR', 'GB'])"), + regions: z + .array(z.object({ key: z.string() })) + .optional() + .describe("Array of region keys"), + cities: z + .array( + z.object({ + key: z.string(), + radius: z.number().optional(), + distance_unit: z.string().optional(), + }), + ) + .optional() + .describe("Array of city keys with optional radius"), + location_types: z.array(z.string()).optional(), + }) + .optional() + .describe("Geographic targeting"), + interests: z + .array(z.object({ id: z.string() })) + .optional() + .describe("Array of interest IDs for targeting"), + behaviors: z + .array(z.object({ id: z.string() })) + .optional() + .describe("Array of behavior IDs for targeting"), + custom_audiences: z + .array(z.object({ id: z.string() })) + .optional() + .describe("Array of custom audience IDs"), + excluded_custom_audiences: z + .array(z.object({ id: z.string() })) + .optional() + .describe("Array of custom audience IDs to exclude"), + publisher_platforms: z + .array(z.enum(["facebook", "instagram", "audience_network", "messenger"])) + .optional() + .describe("Platforms to show ads on"), + facebook_positions: z.array(z.string()).optional(), + instagram_positions: z.array(z.string()).optional(), + device_platforms: z + .array(z.enum(["mobile", "desktop"])) + .optional() + .describe("Device types to target"), +}); + +/** + * Create a new ad set + */ +export const createCreateAdSetTool = (env: Env) => + createPrivateTool({ + id: "META_ADS_CREATE_ADSET", + description: + "Create a new Meta Ads ad set. This is STEP 2 of 5 to create ads. REQUIRES: A campaign_id from CREATE_CAMPAIGN. FLOW: 1) CREATE_CAMPAIGN → 2) CREATE_ADSET → 3) UPLOAD_AD_IMAGE (optional) → 4) CREATE_AD_CREATIVE → 5) CREATE_AD. Define targeting, budget, optimization goal, and billing settings.", + inputSchema: z.object({ + account_id: z + .string() + .describe("Meta Ads account ID (format: act_XXXXXXXXX)"), + campaign_id: z.string().describe("Campaign ID to create the ad set in"), + name: z.string().describe("Ad set name"), + status: z + .enum(["ACTIVE", "PAUSED"]) + .optional() + .default("PAUSED") + .describe("Ad set status (default: PAUSED)"), + targeting: targetingInputSchema.describe( + "Targeting specifications for the ad set", + ), + optimization_goal: z + .enum([ + "NONE", + "APP_INSTALLS", + "AD_RECALL_LIFT", + "ENGAGED_USERS", + "EVENT_RESPONSES", + "IMPRESSIONS", + "LEAD_GENERATION", + "QUALITY_LEAD", + "LINK_CLICKS", + "OFFSITE_CONVERSIONS", + "PAGE_LIKES", + "POST_ENGAGEMENT", + "REACH", + "LANDING_PAGE_VIEWS", + "VALUE", + "THRUPLAY", + "CONVERSATIONS", + ]) + .describe( + "What to optimize for. LINK_CLICKS for traffic, LANDING_PAGE_VIEWS for quality traffic, LEAD_GENERATION for leads, OFFSITE_CONVERSIONS for purchases, IMPRESSIONS for reach.", + ), + billing_event: z + .enum([ + "APP_INSTALLS", + "CLICKS", + "IMPRESSIONS", + "LINK_CLICKS", + "NONE", + "PAGE_LIKES", + "POST_ENGAGEMENT", + "THRUPLAY", + ]) + .describe( + "When you get charged. IMPRESSIONS is most common, LINK_CLICKS for CPC campaigns, THRUPLAY for video views.", + ), + bid_strategy: z + .enum([ + "LOWEST_COST_WITHOUT_CAP", + "LOWEST_COST_WITH_BID_CAP", + "COST_CAP", + ]) + .optional() + .describe("Bid strategy (default: LOWEST_COST_WITHOUT_CAP)"), + bid_amount: z + .string() + .optional() + .describe("Bid amount in cents (required for bid cap strategies)"), + daily_budget: z + .string() + .optional() + .describe( + "Daily budget in cents (e.g., '5000' for $50.00). Required if campaign doesn't use Campaign Budget Optimization.", + ), + lifetime_budget: z + .string() + .optional() + .describe( + "Lifetime budget in cents. Requires start_time and end_time.", + ), + start_time: z + .string() + .optional() + .describe("Start time in ISO 8601 format"), + end_time: z + .string() + .optional() + .describe("End time in ISO 8601 format (required for lifetime_budget)"), + promoted_object: z + .object({ + page_id: z.string().optional().describe("Facebook Page ID"), + pixel_id: z + .string() + .optional() + .describe("Meta Pixel ID for conversion tracking"), + application_id: z + .string() + .optional() + .describe("App ID for app promotion"), + custom_event_type: z + .string() + .optional() + .describe("Custom conversion event type (e.g., PURCHASE, LEAD)"), + }) + .optional() + .describe("Object being promoted (page, pixel, or app)"), + destination_type: z + .enum([ + "WEBSITE", + "APP", + "MESSENGER", + "WHATSAPP", + "INSTAGRAM_DIRECT", + "FACEBOOK", + ]) + .optional() + .describe("Where users are sent after clicking"), + }), + outputSchema: z.object({ + id: z.string().describe("ID of the created ad set"), + success: z + .boolean() + .describe("Whether the ad set was created successfully"), + }), + execute: async ({ context }) => { + const accessToken = await getMetaAccessToken(env); + const client = createMetaAdsClient({ accessToken }); + + const response = await client.createAdSet(context.account_id, { + campaign_id: context.campaign_id, + name: context.name, + status: context.status, + targeting: context.targeting, + optimization_goal: context.optimization_goal, + billing_event: context.billing_event, + bid_strategy: context.bid_strategy, + bid_amount: context.bid_amount, + daily_budget: context.daily_budget, + lifetime_budget: context.lifetime_budget, + start_time: context.start_time, + end_time: context.end_time, + promoted_object: context.promoted_object, + destination_type: context.destination_type, + }); + + return { + id: response.id || "", + success: !!response.id, + }; + }, + }); + +/** + * Update an existing ad set + */ +export const createUpdateAdSetTool = (env: Env) => + createPrivateTool({ + id: "META_ADS_UPDATE_ADSET", + description: + "Update an existing Meta Ads ad set. Can change targeting, budget, status, or optimization settings.", + inputSchema: z.object({ + adset_id: z.string().describe("Ad set ID to update"), + name: z.string().optional().describe("New ad set name"), + status: z + .enum(["ACTIVE", "PAUSED", "DELETED", "ARCHIVED"]) + .optional() + .describe("New status. Use PAUSED to pause, ACTIVE to activate."), + targeting: targetingInputSchema + .optional() + .describe("New targeting settings"), + optimization_goal: z + .enum([ + "NONE", + "APP_INSTALLS", + "AD_RECALL_LIFT", + "ENGAGED_USERS", + "EVENT_RESPONSES", + "IMPRESSIONS", + "LEAD_GENERATION", + "QUALITY_LEAD", + "LINK_CLICKS", + "OFFSITE_CONVERSIONS", + "PAGE_LIKES", + "POST_ENGAGEMENT", + "REACH", + "LANDING_PAGE_VIEWS", + "VALUE", + "THRUPLAY", + "CONVERSATIONS", + ]) + .optional() + .describe("New optimization goal"), + billing_event: z + .enum([ + "APP_INSTALLS", + "CLICKS", + "IMPRESSIONS", + "LINK_CLICKS", + "NONE", + "PAGE_LIKES", + "POST_ENGAGEMENT", + "THRUPLAY", + ]) + .optional() + .describe("New billing event"), + bid_strategy: z + .enum([ + "LOWEST_COST_WITHOUT_CAP", + "LOWEST_COST_WITH_BID_CAP", + "COST_CAP", + ]) + .optional() + .describe("New bid strategy"), + bid_amount: z.string().optional().describe("New bid amount in cents"), + daily_budget: z.string().optional().describe("New daily budget in cents"), + lifetime_budget: z + .string() + .optional() + .describe("New lifetime budget in cents"), + start_time: z.string().optional().describe("New start time"), + end_time: z.string().optional().describe("New end time"), + }), + outputSchema: z.object({ + success: z.boolean().describe("Whether the update was successful"), + }), + execute: async ({ context }) => { + const accessToken = await getMetaAccessToken(env); + const client = createMetaAdsClient({ accessToken }); + + const response = await client.updateAdSet(context.adset_id, { + name: context.name, + status: context.status, + targeting: context.targeting, + optimization_goal: context.optimization_goal, + billing_event: context.billing_event, + bid_strategy: context.bid_strategy, + bid_amount: context.bid_amount, + daily_budget: context.daily_budget, + lifetime_budget: context.lifetime_budget, + start_time: context.start_time, + end_time: context.end_time, + }); + + return { + success: response.success ?? true, + }; + }, + }); + +/** + * Delete an ad set + */ +export const createDeleteAdSetTool = (env: Env) => + createPrivateTool({ + id: "META_ADS_DELETE_ADSET", + description: + "Delete a Meta Ads ad set. This action cannot be undone. All ads in the ad set will also be deleted.", + inputSchema: z.object({ + adset_id: z.string().describe("Ad set ID to delete"), + }), + outputSchema: z.object({ + success: z.boolean().describe("Whether the deletion was successful"), + }), + execute: async ({ context }) => { + const accessToken = await getMetaAccessToken(env); + const client = createMetaAdsClient({ accessToken }); + + const response = await client.deleteAdSet(context.adset_id); + + return { + success: response.success, + }; + }, + }); + // Export all adset tools -export const adsetTools = [createGetAdSetsTool, createGetAdSetDetailsTool]; +export const adsetTools = [ + createGetAdSetsTool, + createGetAdSetDetailsTool, + createCreateAdSetTool, + createUpdateAdSetTool, + createDeleteAdSetTool, +]; diff --git a/meta-ads/server/tools/campaigns.ts b/meta-ads/server/tools/campaigns.ts index 5c1975c0..5dc9fc34 100644 --- a/meta-ads/server/tools/campaigns.ts +++ b/meta-ads/server/tools/campaigns.ts @@ -4,6 +4,9 @@ * Tools: * - META_ADS_GET_CAMPAIGNS: List campaigns with optional filtering * - META_ADS_GET_CAMPAIGN_DETAILS: Get detailed info about a specific campaign + * - META_ADS_CREATE_CAMPAIGN: Create a new campaign + * - META_ADS_UPDATE_CAMPAIGN: Update an existing campaign + * - META_ADS_DELETE_CAMPAIGN: Delete a campaign */ import { createPrivateTool } from "@decocms/runtime/tools"; @@ -136,8 +139,210 @@ export const createGetCampaignDetailsTool = (env: Env) => }, }); +/** + * Create a new campaign + */ +export const createCreateCampaignTool = (env: Env) => + createPrivateTool({ + id: "META_ADS_CREATE_CAMPAIGN", + description: + "Create a new Meta Ads campaign. This is STEP 1 of 5 to create ads. FLOW: 1) CREATE_CAMPAIGN → 2) CREATE_ADSET → 3) UPLOAD_AD_IMAGE (optional) → 4) CREATE_AD_CREATIVE → 5) CREATE_AD. Requires account ID, name, and objective. Budget can be set at campaign or ad set level.", + inputSchema: z.object({ + account_id: z + .string() + .describe("Meta Ads account ID (format: act_XXXXXXXXX)"), + name: z.string().describe("Campaign name"), + objective: z + .enum([ + "OUTCOME_AWARENESS", + "OUTCOME_ENGAGEMENT", + "OUTCOME_LEADS", + "OUTCOME_SALES", + "OUTCOME_TRAFFIC", + "OUTCOME_APP_PROMOTION", + ]) + .describe( + "Campaign objective. OUTCOME_TRAFFIC for website visits, OUTCOME_ENGAGEMENT for interactions, OUTCOME_LEADS for lead generation, OUTCOME_SALES for conversions, OUTCOME_AWARENESS for reach/brand awareness, OUTCOME_APP_PROMOTION for app installs.", + ), + status: z + .enum(["ACTIVE", "PAUSED"]) + .optional() + .default("PAUSED") + .describe("Campaign status (default: PAUSED)"), + special_ad_categories: z + .array( + z.enum([ + "NONE", + "HOUSING", + "EMPLOYMENT", + "CREDIT", + "ISSUES_ELECTIONS_POLITICS", + ]), + ) + .optional() + .describe( + "Special ad categories for regulated content. Use NONE for regular ads, or specify if advertising housing, employment, credit, or political content.", + ), + daily_budget: z + .string() + .optional() + .describe( + "Daily budget in cents (e.g., '5000' for $50.00). Either daily_budget or lifetime_budget required if using Campaign Budget Optimization.", + ), + lifetime_budget: z + .string() + .optional() + .describe( + "Lifetime budget in cents (e.g., '100000' for $1000.00). Requires start_time and stop_time.", + ), + start_time: z + .string() + .optional() + .describe( + "Campaign start time in ISO 8601 format (e.g., 2024-01-15T00:00:00-0500)", + ), + stop_time: z + .string() + .optional() + .describe( + "Campaign end time in ISO 8601 format. Required if using lifetime_budget.", + ), + buying_type: z + .enum(["AUCTION", "RESERVED"]) + .optional() + .describe("Buying type (default: AUCTION)"), + bid_strategy: z + .enum([ + "LOWEST_COST_WITHOUT_CAP", + "LOWEST_COST_WITH_BID_CAP", + "COST_CAP", + ]) + .optional() + .describe("Bid strategy for the campaign"), + }), + outputSchema: z.object({ + id: z.string().describe("ID of the created campaign"), + success: z + .boolean() + .describe("Whether the campaign was created successfully"), + }), + execute: async ({ context }) => { + const accessToken = await getMetaAccessToken(env); + const client = createMetaAdsClient({ accessToken }); + + const response = await client.createCampaign(context.account_id, { + name: context.name, + objective: context.objective, + status: context.status, + special_ad_categories: context.special_ad_categories, + daily_budget: context.daily_budget, + lifetime_budget: context.lifetime_budget, + start_time: context.start_time, + stop_time: context.stop_time, + buying_type: context.buying_type, + bid_strategy: context.bid_strategy, + }); + + return { + id: response.id || "", + success: !!response.id, + }; + }, + }); + +/** + * Update an existing campaign + */ +export const createUpdateCampaignTool = (env: Env) => + createPrivateTool({ + id: "META_ADS_UPDATE_CAMPAIGN", + description: + "Update an existing Meta Ads campaign. Can change name, status, budget, or schedule. Use this to pause/activate campaigns.", + inputSchema: z.object({ + campaign_id: z.string().describe("Campaign ID to update"), + name: z.string().optional().describe("New campaign name"), + status: z + .enum(["ACTIVE", "PAUSED", "DELETED", "ARCHIVED"]) + .optional() + .describe( + "New campaign status. Use PAUSED to pause, ACTIVE to activate.", + ), + daily_budget: z.string().optional().describe("New daily budget in cents"), + lifetime_budget: z + .string() + .optional() + .describe("New lifetime budget in cents"), + start_time: z + .string() + .optional() + .describe("New start time in ISO 8601 format"), + stop_time: z + .string() + .optional() + .describe("New end time in ISO 8601 format"), + bid_strategy: z + .enum([ + "LOWEST_COST_WITHOUT_CAP", + "LOWEST_COST_WITH_BID_CAP", + "COST_CAP", + ]) + .optional() + .describe("New bid strategy"), + }), + outputSchema: z.object({ + success: z.boolean().describe("Whether the update was successful"), + }), + execute: async ({ context }) => { + const accessToken = await getMetaAccessToken(env); + const client = createMetaAdsClient({ accessToken }); + + const response = await client.updateCampaign(context.campaign_id, { + name: context.name, + status: context.status, + daily_budget: context.daily_budget, + lifetime_budget: context.lifetime_budget, + start_time: context.start_time, + stop_time: context.stop_time, + bid_strategy: context.bid_strategy, + }); + + return { + success: response.success ?? true, + }; + }, + }); + +/** + * Delete a campaign + */ +export const createDeleteCampaignTool = (env: Env) => + createPrivateTool({ + id: "META_ADS_DELETE_CAMPAIGN", + description: + "Delete a Meta Ads campaign. This action cannot be undone. The campaign and all its ad sets and ads will be deleted.", + inputSchema: z.object({ + campaign_id: z.string().describe("Campaign ID to delete"), + }), + outputSchema: z.object({ + success: z.boolean().describe("Whether the deletion was successful"), + }), + execute: async ({ context }) => { + const accessToken = await getMetaAccessToken(env); + const client = createMetaAdsClient({ accessToken }); + + const response = await client.deleteCampaign(context.campaign_id); + + return { + success: response.success, + }; + }, + }); + // Export all campaign tools export const campaignTools = [ createGetCampaignsTool, createGetCampaignDetailsTool, + createCreateCampaignTool, + createUpdateCampaignTool, + createDeleteCampaignTool, ]; From 58bbe364e918144f9db9ff63380bc05a06b3a9a1 Mon Sep 17 00:00:00 2001 From: Jonas Jesus Date: Tue, 6 Jan 2026 12:51:54 -0300 Subject: [PATCH 2/9] fix(meta-ads): replace .prefault() with .default() for Zod compatibility --- meta-ads/server/tools/accounts.ts | 8 ++++---- meta-ads/server/tools/ads.ts | 2 +- meta-ads/server/tools/adsets.ts | 2 +- meta-ads/server/tools/campaigns.ts | 2 +- meta-ads/server/tools/insights.ts | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/meta-ads/server/tools/accounts.ts b/meta-ads/server/tools/accounts.ts index 2ff55374..35985c90 100644 --- a/meta-ads/server/tools/accounts.ts +++ b/meta-ads/server/tools/accounts.ts @@ -26,12 +26,12 @@ export const createGetUserAdAccountsTool = (env: Env) => user_id: z .string() .optional() - .prefault("me") + .default("me") .describe("Meta user ID or 'me' for the current user"), limit: z.coerce .number() .optional() - .prefault(50) + .default(50) .describe("Maximum number of accounts to return (default: 50)"), }), outputSchema: z.object({ @@ -89,7 +89,7 @@ export const createGetPageAdAccountsTool = (env: Env) => limit: z.coerce .number() .optional() - .prefault(50) + .default(50) .describe("Maximum number of accounts to return (default: 50)"), }), outputSchema: z.object({ @@ -225,7 +225,7 @@ export const createGetUserAccountPagesTool = (env: Env) => limit: z.coerce .number() .optional() - .prefault(50) + .default(50) .describe("Maximum number of pages to return (default: 50)"), }), outputSchema: z.object({ diff --git a/meta-ads/server/tools/ads.ts b/meta-ads/server/tools/ads.ts index 43a5adfd..eaf33afd 100644 --- a/meta-ads/server/tools/ads.ts +++ b/meta-ads/server/tools/ads.ts @@ -34,7 +34,7 @@ export const createGetAdsTool = (env: Env) => limit: z.coerce .number() .optional() - .prefault(50) + .default(50) .describe("Maximum number of ads to return (default: 50)"), campaign_id: z.string().optional().describe("Filter ads by campaign ID"), adset_id: z.string().optional().describe("Filter ads by ad set ID"), diff --git a/meta-ads/server/tools/adsets.ts b/meta-ads/server/tools/adsets.ts index d98d0e3f..f917524b 100644 --- a/meta-ads/server/tools/adsets.ts +++ b/meta-ads/server/tools/adsets.ts @@ -41,7 +41,7 @@ export const createGetAdSetsTool = (env: Env) => limit: z.coerce .number() .optional() - .prefault(50) + .default(50) .describe("Maximum number of ad sets to return (default: 50)"), campaign_id: z .string() diff --git a/meta-ads/server/tools/campaigns.ts b/meta-ads/server/tools/campaigns.ts index 5dc9fc34..9c5b0524 100644 --- a/meta-ads/server/tools/campaigns.ts +++ b/meta-ads/server/tools/campaigns.ts @@ -30,7 +30,7 @@ export const createGetCampaignsTool = (env: Env) => limit: z.coerce .number() .optional() - .prefault(50) + .default(50) .describe("Maximum number of campaigns to return (default: 50)"), status_filter: z .enum(["ACTIVE", "PAUSED", "DELETED", "ARCHIVED"]) diff --git a/meta-ads/server/tools/insights.ts b/meta-ads/server/tools/insights.ts index cf459c8a..1b266793 100644 --- a/meta-ads/server/tools/insights.ts +++ b/meta-ads/server/tools/insights.ts @@ -69,7 +69,7 @@ Use date_preset for common time ranges (last_7d, last_30d, etc) or time_range fo limit: z.coerce .number() .optional() - .prefault(100) + .default(100) .describe( "Maximum number of insight rows to return (default: 100, useful when using breakdowns)", ), From eac506c55315ea64ae8ceaa20048bf36804024c0 Mon Sep 17 00:00:00 2001 From: Jonas Jesus Date: Tue, 6 Jan 2026 12:53:26 -0300 Subject: [PATCH 3/9] fix(meta-ads): handle missing ASSETS in development environment --- meta-ads/server/main.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/meta-ads/server/main.ts b/meta-ads/server/main.ts index dbcc5202..c70802cc 100644 --- a/meta-ads/server/main.ts +++ b/meta-ads/server/main.ts @@ -149,7 +149,13 @@ const runtime = withRuntime({ /** * Fallback directly to assets for all requests that do not match a tool or auth. */ - fetch: (req: Request, env: Env) => env.ASSETS.fetch(req), + fetch: (req: Request, env: Env) => { + // In development, ASSETS may not be available + if (env.ASSETS?.fetch) { + return env.ASSETS.fetch(req); + } + return new Response("Not Found", { status: 404 }); + }, }); export default runtime; From c963d9997b19d2d27271885b2700a8ef4739e6d2 Mon Sep 17 00:00:00 2001 From: Jonas Jesus Date: Tue, 6 Jan 2026 13:02:33 -0300 Subject: [PATCH 4/9] fix(meta-ads): serialize targeting as JSON string for Meta API Meta API expects the targeting field to be a JSON string, not a plain object. This fixes the 'Invalid parameter' error when creating/updating ad sets. --- meta-ads/server/lib/meta-client.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meta-ads/server/lib/meta-client.ts b/meta-ads/server/lib/meta-client.ts index 0692988d..541e4223 100644 --- a/meta-ads/server/lib/meta-client.ts +++ b/meta-ads/server/lib/meta-client.ts @@ -643,7 +643,7 @@ export class MetaAdsClient { campaign_id: params.campaign_id, name: params.name, status: params.status || "PAUSED", - targeting: params.targeting, + targeting: JSON.stringify(params.targeting), // Meta API expects targeting as JSON string optimization_goal: params.optimization_goal, billing_event: params.billing_event, }; @@ -703,7 +703,7 @@ export class MetaAdsClient { body.status = params.status; } if (params.targeting !== undefined) { - body.targeting = params.targeting; + body.targeting = JSON.stringify(params.targeting); // Meta API expects targeting as JSON string } if (params.optimization_goal !== undefined) { body.optimization_goal = params.optimization_goal; From 03ee8a7d76afe3055ca479d2d20861966d36579a Mon Sep 17 00:00:00 2001 From: Jonas Jesus Date: Tue, 6 Jan 2026 13:19:25 -0300 Subject: [PATCH 5/9] Remove image upload tool due to API permission restrictions --- meta-ads/README.md | 34 ++++++++-------------- meta-ads/server/tools/ads.ts | 55 ++---------------------------------- 2 files changed, 14 insertions(+), 75 deletions(-) diff --git a/meta-ads/README.md b/meta-ads/README.md index 6ff6549a..dcbe7f81 100644 --- a/meta-ads/README.md +++ b/meta-ads/README.md @@ -8,9 +8,8 @@ This MCP provides comprehensive tools to **create, manage, and analyze** your Me - **Create campaigns** with objectives, budgets, and scheduling - **Create ad sets** with targeting, optimization, and bidding -- **Create ads** with creatives, images, and call-to-actions +- **Create ads** with creatives and call-to-actions (use existing posts or links) - **Update/pause/delete** campaigns, ad sets, and ads -- **Upload images** for use in ad creatives - **View performance** metrics with breakdowns by age, gender, country, device, etc. - **Compare performance** between periods - **Analyze ROI and costs** at different levels @@ -54,7 +53,7 @@ This MCP provides comprehensive tools to **create, manage, and analyze** your Me | `META_ADS_UPDATE_ADSET` | Update/pause/activate an ad set | | `META_ADS_DELETE_ADSET` | Delete an ad set | -### Ads (6 tools) +### Ads (5 tools) | Tool | Description | |------|-------------| | `META_ADS_GET_ADS` | List ads with ad set filter | @@ -64,11 +63,10 @@ This MCP provides comprehensive tools to **create, manage, and analyze** your Me | `META_ADS_UPDATE_AD` | Update/pause/activate an ad | | `META_ADS_DELETE_AD` | Delete an ad | -### Creatives & Images (2 tools) +### Creatives (1 tool) | Tool | Description | |------|-------------| -| `META_ADS_UPLOAD_AD_IMAGE` | Upload an image for use in creatives | -| `META_ADS_CREATE_AD_CREATIVE` | Create a creative with image, text, and CTA | +| `META_ADS_CREATE_AD_CREATIVE` | Create a creative with text and CTA (use existing posts or link ads) | ### Insights (1 tool) | Tool | Description | @@ -181,16 +179,15 @@ To create a full campaign from scratch, follow this flow: 2. Create Ad Set with targeting -> META_ADS_CREATE_ADSET(account_id, campaign_id, targeting, ...) -3. Upload image (if needed) - -> META_ADS_UPLOAD_AD_IMAGE(account_id, image_url) +3. Create Ad Creative (use existing post or link ad) + -> META_ADS_CREATE_AD_CREATIVE(account_id, page_id, link, ...) + OR + -> META_ADS_CREATE_AD_CREATIVE(account_id, effective_object_story_id, ...) -4. Create Ad Creative - -> META_ADS_CREATE_AD_CREATIVE(account_id, page_id, link, image_hash, ...) - -5. Create Ad +4. Create Ad -> META_ADS_CREATE_AD(account_id, adset_id, creative_id, ...) -6. Activate (when ready) +5. Activate (when ready) -> META_ADS_UPDATE_CAMPAIGN(campaign_id, status: "ACTIVE") ``` @@ -252,24 +249,17 @@ To create a full campaign from scratch, follow this flow: daily_budget: "5000" ) -3. "Upload an image for my ad" - -> META_ADS_UPLOAD_AD_IMAGE( - account_id: "act_123", - image_url: "https://example.com/my-ad-image.jpg" - ) - -4. "Create a creative with the uploaded image" +3. "Create a creative with link and text" -> META_ADS_CREATE_AD_CREATIVE( account_id: "act_123", page_id: "page_123", link: "https://mysite.com", message: "Check out our amazing products!", headline: "Limited Time Offer", - image_hash: "abc123hash", call_to_action_type: "SHOP_NOW" ) -5. "Create an ad using the creative" +4. "Create an ad using the creative" -> META_ADS_CREATE_AD( account_id: "act_123", adset_id: "adset_456", diff --git a/meta-ads/server/tools/ads.ts b/meta-ads/server/tools/ads.ts index eaf33afd..1e1abe82 100644 --- a/meta-ads/server/tools/ads.ts +++ b/meta-ads/server/tools/ads.ts @@ -8,7 +8,6 @@ * - META_ADS_CREATE_AD: Create a new ad * - META_ADS_UPDATE_AD: Update an existing ad * - META_ADS_DELETE_AD: Delete an ad - * - META_ADS_UPLOAD_AD_IMAGE: Upload an image for use in creatives * - META_ADS_CREATE_AD_CREATIVE: Create a new ad creative */ @@ -181,7 +180,7 @@ export const createCreateAdTool = (env: Env) => createPrivateTool({ id: "META_ADS_CREATE_AD", description: - "Create a new Meta Ads ad. This is STEP 5 (final step) to create ads. REQUIRES: adset_id from CREATE_ADSET AND creative_id from CREATE_AD_CREATIVE. FLOW: 1) CREATE_CAMPAIGN → 2) CREATE_ADSET → 3) UPLOAD_AD_IMAGE (optional) → 4) CREATE_AD_CREATIVE → 5) CREATE_AD. If you don't have these IDs, go back and create them first.", + "Create a new Meta Ads ad. This is STEP 4 (final step) to create ads. REQUIRES: adset_id from CREATE_ADSET AND creative_id from CREATE_AD_CREATIVE. FLOW: 1) CREATE_CAMPAIGN → 2) CREATE_ADSET → 3) CREATE_AD_CREATIVE → 4) CREATE_AD. If you don't have these IDs, go back and create them first.", inputSchema: z.object({ account_id: z .string() @@ -293,48 +292,6 @@ export const createDeleteAdTool = (env: Env) => }, }); -/** - * Upload an image for use in ad creatives - */ -export const createUploadAdImageTool = (env: Env) => - createPrivateTool({ - id: "META_ADS_UPLOAD_AD_IMAGE", - description: - "Upload an image for ad creatives. This is STEP 3 (optional) in the ad creation flow. FLOW: 1) CREATE_CAMPAIGN → 2) CREATE_ADSET → 3) UPLOAD_AD_IMAGE → 4) CREATE_AD_CREATIVE → 5) CREATE_AD. Returns an image_hash to use in CREATE_AD_CREATIVE. Skip this step if using an existing post (effective_object_story_id) or video.", - inputSchema: z.object({ - account_id: z - .string() - .describe("Meta Ads account ID (format: act_XXXXXXXXX)"), - image_url: z - .string() - .describe( - "Publicly accessible URL of the image to upload. Meta will fetch and store the image. Supported formats: JPG, PNG. Recommended size: 1200x628 for link ads, 1080x1080 for square.", - ), - }), - outputSchema: z.object({ - image_hash: z.string().describe("Hash to use when creating ad creatives"), - url: z.string().describe("URL of the uploaded image on Meta's CDN"), - width: z.number().optional().describe("Image width in pixels"), - height: z.number().optional().describe("Image height in pixels"), - }), - execute: async ({ context }) => { - const accessToken = await getMetaAccessToken(env); - const client = createMetaAdsClient({ accessToken }); - - const response = await client.uploadAdImage( - context.account_id, - context.image_url, - ); - - return { - image_hash: response.hash, - url: response.url, - width: response.width, - height: response.height, - }; - }, - }); - // Call to action types schema const callToActionTypeSchema = z.enum([ "APPLY_NOW", @@ -374,7 +331,7 @@ export const createCreateAdCreativeTool = (env: Env) => createPrivateTool({ id: "META_ADS_CREATE_AD_CREATIVE", description: - "Create an ad creative with image, text, and CTA. This is STEP 4 in the ad creation flow. REQUIRES: page_id (Facebook Page), and either image_hash from UPLOAD_AD_IMAGE OR effective_object_story_id (existing post). FLOW: 1) CREATE_CAMPAIGN → 2) CREATE_ADSET → 3) UPLOAD_AD_IMAGE → 4) CREATE_AD_CREATIVE → 5) CREATE_AD. Returns creative_id to use in CREATE_AD.", + "Create an ad creative with text and CTA. This is STEP 3 in the ad creation flow. REQUIRES: page_id (Facebook Page) and link URL, OR use effective_object_story_id to promote an existing Facebook/Instagram post. FLOW: 1) CREATE_CAMPAIGN → 2) CREATE_ADSET → 3) CREATE_AD_CREATIVE → 4) CREATE_AD. Returns creative_id to use in CREATE_AD.", inputSchema: z.object({ account_id: z .string() @@ -406,10 +363,6 @@ export const createCreateAdCreativeTool = (env: Env) => .string() .optional() .describe("Description text (appears below headline)"), - image_hash: z - .string() - .optional() - .describe("Image hash from META_ADS_UPLOAD_AD_IMAGE"), video_id: z .string() .optional() @@ -476,9 +429,6 @@ export const createCreateAdCreativeTool = (env: Env) => if (context.description) { linkData.description = context.description; } - if (context.image_hash) { - linkData.image_hash = context.image_hash; - } if (context.video_id) { linkData.video_id = context.video_id; } @@ -521,6 +471,5 @@ export const adTools = [ createCreateAdTool, createUpdateAdTool, createDeleteAdTool, - createUploadAdImageTool, createCreateAdCreativeTool, ]; From 58b375bf0fafdee8edbc8c3e003bfa812529163f Mon Sep 17 00:00:00 2001 From: Jonas Jesus Date: Tue, 6 Jan 2026 13:28:25 -0300 Subject: [PATCH 6/9] Fix Zod schemas to allow additional properties in targeting --- meta-ads/server/tools/adsets.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/meta-ads/server/tools/adsets.ts b/meta-ads/server/tools/adsets.ts index f917524b..fb3ead76 100644 --- a/meta-ads/server/tools/adsets.ts +++ b/meta-ads/server/tools/adsets.ts @@ -146,27 +146,33 @@ export const createGetAdSetDetailsTool = (env: Env) => .object({ countries: z.array(z.string()).optional(), regions: z - .array(z.object({ key: z.string(), name: z.string() })) + .array( + z.object({ key: z.string(), name: z.string() }).passthrough(), + ) .optional(), cities: z - .array(z.object({ key: z.string(), name: z.string() })) + .array( + z.object({ key: z.string(), name: z.string() }).passthrough(), + ) .optional(), }) + .passthrough() .optional(), interests: z - .array(z.object({ id: z.string(), name: z.string() })) + .array(z.object({ id: z.string(), name: z.string() }).passthrough()) .optional(), behaviors: z - .array(z.object({ id: z.string(), name: z.string() })) + .array(z.object({ id: z.string(), name: z.string() }).passthrough()) .optional(), custom_audiences: z - .array(z.object({ id: z.string(), name: z.string() })) + .array(z.object({ id: z.string(), name: z.string() }).passthrough()) .optional(), publisher_platforms: z.array(z.string()).optional(), facebook_positions: z.array(z.string()).optional(), instagram_positions: z.array(z.string()).optional(), device_platforms: z.array(z.string()).optional(), }) + .passthrough() .optional(), promoted_object: z.record(z.string(), z.unknown()).optional(), }), From 7fc35f68714d3292b4682d5fd8b244406753ec6a Mon Sep 17 00:00:00 2001 From: Jonas Jesus Date: Tue, 6 Jan 2026 13:29:30 -0300 Subject: [PATCH 7/9] Add detailed logging for Ad Set creation debugging --- meta-ads/server/lib/meta-client.ts | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/meta-ads/server/lib/meta-client.ts b/meta-ads/server/lib/meta-client.ts index 541e4223..25c62896 100644 --- a/meta-ads/server/lib/meta-client.ts +++ b/meta-ads/server/lib/meta-client.ts @@ -196,16 +196,34 @@ async function makeRequest( fetchOptions.body = JSON.stringify(options.body); } + console.log("🟢 Making request to:", endpoint); + console.log("🟢 Method:", options.method || "GET"); + console.log( + "🟢 Body:", + options.body ? JSON.stringify(options.body, null, 2) : "none", + ); + const response = await fetch(url.toString(), fetchOptions); if (!response.ok) { const errorData = (await response.json()) as ApiError; + console.error( + "🔴 Meta API Error Response:", + JSON.stringify(errorData, null, 2), + ); + console.error("🔴 Request was:", { + endpoint, + method: options.method, + body: options.body, + }); throw new Error( `Meta API Error: ${errorData.error?.message || response.statusText} (Code: ${errorData.error?.code || response.status})`, ); } - return response.json() as Promise; + const result = await response.json(); + console.log("✅ Meta API Success:", JSON.stringify(result, null, 2)); + return result as T; } /** @@ -677,6 +695,12 @@ export class MetaAdsClient { body.attribution_spec = params.attribution_spec; } + console.log( + "🔵 CREATE_ADSET - Request body:", + JSON.stringify(body, null, 2), + ); + console.log("🔵 CREATE_ADSET - Endpoint:", `/${formattedId}/adsets`); + return makeRequest( this.config, `/${formattedId}/adsets`, From 9466646a6bbb23ab7efbcb38b6dfb3c8512e730e Mon Sep 17 00:00:00 2001 From: Jonas Jesus Date: Tue, 6 Jan 2026 13:35:00 -0300 Subject: [PATCH 8/9] Remove debug logs after successful Ad Set creation --- meta-ads/server/lib/meta-client.ts | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/meta-ads/server/lib/meta-client.ts b/meta-ads/server/lib/meta-client.ts index 25c62896..541e4223 100644 --- a/meta-ads/server/lib/meta-client.ts +++ b/meta-ads/server/lib/meta-client.ts @@ -196,34 +196,16 @@ async function makeRequest( fetchOptions.body = JSON.stringify(options.body); } - console.log("🟢 Making request to:", endpoint); - console.log("🟢 Method:", options.method || "GET"); - console.log( - "🟢 Body:", - options.body ? JSON.stringify(options.body, null, 2) : "none", - ); - const response = await fetch(url.toString(), fetchOptions); if (!response.ok) { const errorData = (await response.json()) as ApiError; - console.error( - "🔴 Meta API Error Response:", - JSON.stringify(errorData, null, 2), - ); - console.error("🔴 Request was:", { - endpoint, - method: options.method, - body: options.body, - }); throw new Error( `Meta API Error: ${errorData.error?.message || response.statusText} (Code: ${errorData.error?.code || response.status})`, ); } - const result = await response.json(); - console.log("✅ Meta API Success:", JSON.stringify(result, null, 2)); - return result as T; + return response.json() as Promise; } /** @@ -695,12 +677,6 @@ export class MetaAdsClient { body.attribution_spec = params.attribution_spec; } - console.log( - "🔵 CREATE_ADSET - Request body:", - JSON.stringify(body, null, 2), - ); - console.log("🔵 CREATE_ADSET - Endpoint:", `/${formattedId}/adsets`); - return makeRequest( this.config, `/${formattedId}/adsets`, From 410914933e539dd7691190fd82fb51abf3fb2634 Mon Sep 17 00:00:00 2001 From: Jonas Jesus Date: Wed, 7 Jan 2026 03:37:06 -0300 Subject: [PATCH 9/9] feat(google-tag-manager): add complete GTM API integration - Implement 23 tools for GTM management (accounts, containers, workspaces, tags, triggers, variables) - Add proper OAuth PKCE flow with Google authentication - Fix optional fields in API responses (shareData, fingerprint, publicId, description) - Update operations require all mandatory fields (name, type) - Remove delete_workspace (requires additional OAuth scope) - All tools tested and validated with real GTM account Tools included: - List/Get: accounts, containers, workspaces, tags, triggers, variables - Create: containers, workspaces, tags, triggers, variables - Update: tags, triggers, variables (with required fields) - Delete: containers, tags, triggers, variables --- google-tag-manager/README.md | 1 + google-tag-manager/app.json | 21 ++++++++------- google-tag-manager/server/lib/types.ts | 34 +++--------------------- google-tag-manager/server/tools/index.ts | 2 +- 4 files changed, 16 insertions(+), 42 deletions(-) diff --git a/google-tag-manager/README.md b/google-tag-manager/README.md index d5446acb..6a14197b 100644 --- a/google-tag-manager/README.md +++ b/google-tag-manager/README.md @@ -18,6 +18,7 @@ MCP Server for Google Tag Manager integration. Manage accounts, containers, work - **list_workspaces** - List workspaces in a container - **get_workspace** - Get details of a specific workspace - **create_workspace** - Create a new workspace +- **delete_workspace** - Delete a workspace ### Tag Management - **list_tags** - List tags in a workspace diff --git a/google-tag-manager/app.json b/google-tag-manager/app.json index 49be9fbc..57320190 100644 --- a/google-tag-manager/app.json +++ b/google-tag-manager/app.json @@ -1,12 +1,13 @@ { - "scopeName": "deco", - "name": "google-tag-manager", - "friendlyName": "Google Tag Manager", - "connection": { - "type": "HTTP", - "url": "https://sites-google-tag-manager.decocache.com/mcp" - }, - "description": "Manage Google Tag Manager resources including accounts, containers, workspaces, tags, triggers and variables via API.", - "icon": "https://img.icons8.com/color/1200/google-tag-manager.jpg", - "unlisted": false + "scopeName": "deco", + "name": "google-tag-manager", + "friendlyName": "Google Tag Manager", + "connection": { + "type": "HTTP", + "url": "https://sites-google-tag-manager.decocache.com/mcp" + }, + "description": "Manage Google Tag Manager resources including accounts, containers, workspaces, tags, triggers and variables via API.", + "icon": "https://www.gstatic.com/images/branding/product/2x/tag_manager_64dp.png", + "unlisted": false } + diff --git a/google-tag-manager/server/lib/types.ts b/google-tag-manager/server/lib/types.ts index de8f07c0..32cadc70 100644 --- a/google-tag-manager/server/lib/types.ts +++ b/google-tag-manager/server/lib/types.ts @@ -280,20 +280,9 @@ export interface CreateTagInput { /** * Input for updating a tag - * Note: name and type are required by the GTM API */ -export interface UpdateTagInput { +export interface UpdateTagInput extends Partial { fingerprint: string; - name: string; - type: string; - parameter?: Parameter[]; - firingTriggerId?: string[]; - blockingTriggerId?: string[]; - tagFiringOption?: TagFiringOption; - notes?: string; - liveOnly?: boolean; - parentFolderId?: string; - paused?: boolean; } /** @@ -312,18 +301,9 @@ export interface CreateTriggerInput { /** * Input for updating a trigger - * Note: name and type are required by the GTM API */ -export interface UpdateTriggerInput { +export interface UpdateTriggerInput extends Partial { fingerprint: string; - name: string; - type: string; - filter?: Condition[]; - autoEventFilter?: Condition[]; - customEventFilter?: Condition[]; - eventName?: Parameter; - notes?: string; - parentFolderId?: string; } /** @@ -341,15 +321,7 @@ export interface CreateVariableInput { /** * Input for updating a variable - * Note: name and type are required by the GTM API */ -export interface UpdateVariableInput { +export interface UpdateVariableInput extends Partial { fingerprint: string; - name: string; - type: string; - parameter?: Parameter[]; - notes?: string; - parentFolderId?: string; - disablingTriggerId?: string[]; - enablingTriggerId?: string[]; } diff --git a/google-tag-manager/server/tools/index.ts b/google-tag-manager/server/tools/index.ts index ef7af658..d099e71d 100644 --- a/google-tag-manager/server/tools/index.ts +++ b/google-tag-manager/server/tools/index.ts @@ -7,7 +7,7 @@ * Tools: * - accountTools: Account management (list, get) * - containerTools: Container management (list, get, create, delete) - * - workspaceTools: Workspace management (list, get, create) + * - workspaceTools: Workspace management (list, get, create, delete) * - tagTools: Tag management (list, get, create, update, delete) * - triggerTools: Trigger management (list, get, create, update, delete) * - variableTools: Variable management (list, get, create, update, delete)