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) diff --git a/meta-ads/README.md b/meta-ads/README.md index f390554b..dcbe7f81 100644 --- a/meta-ads/README.md +++ b/meta-ads/README.md @@ -1,13 +1,16 @@ -# 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 and call-to-actions (use existing posts or links) +- **Update/pause/delete** campaigns, ad sets, and ads +- **View performance** metrics with breakdowns by age, gender, country, device, etc. - **Compare performance** between periods - **Analyze ROI and costs** at different levels @@ -32,24 +35,38 @@ 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 (5 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 (1 tool) +| Tool | Description | +|------|-------------| +| `META_ADS_CREATE_AD_CREATIVE` | Create a creative with text and CTA (use existing posts or link ads) | ### Insights (1 tool) | Tool | Description | @@ -151,11 +168,47 @@ 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. 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 + -> META_ADS_CREATE_AD(account_id, adset_id, creative_id, ...) + +5. 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 +222,64 @@ 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. "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", + call_to_action_type: "SHOP_NOW" + ) + +4. "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..541e4223 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: JSON.stringify(params.targeting), // Meta API expects targeting as JSON string + 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 = JSON.stringify(params.targeting); // Meta API expects targeting as JSON string + } + 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/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; 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 db38c02b..1e1abe82 100644 --- a/meta-ads/server/tools/ads.ts +++ b/meta-ads/server/tools/ads.ts @@ -5,6 +5,10 @@ * - 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_CREATE_AD_CREATIVE: Create a new ad creative */ import { createPrivateTool } from "@decocms/runtime/tools"; @@ -12,6 +16,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 @@ -28,7 +33,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"), @@ -168,9 +173,303 @@ 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 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() + .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, + }; + }, + }); + +// 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 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() + .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)"), + 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.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, + createCreateAdCreativeTool, ]; diff --git a/meta-ads/server/tools/adsets.ts b/meta-ads/server/tools/adsets.ts index 1749a59b..fb3ead76 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"; @@ -38,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() @@ -143,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(), }), @@ -196,5 +205,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..9c5b0524 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"; @@ -27,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"]) @@ -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, ]; 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)", ),