diff --git a/servlets/meta-ads/.gitignore b/servlets/meta-ads/.gitignore new file mode 100644 index 0000000..7773828 --- /dev/null +++ b/servlets/meta-ads/.gitignore @@ -0,0 +1 @@ +dist/ \ No newline at end of file diff --git a/servlets/meta-ads/README.md b/servlets/meta-ads/README.md new file mode 100644 index 0000000..6b1a1fc --- /dev/null +++ b/servlets/meta-ads/README.md @@ -0,0 +1,143 @@ +# Meta Ads + + +This project provides a comprehensive suite of tools designed for managing and analyzing advertising campaigns on the Meta advertising platform. By leveraging these tools, users can efficiently create, monitor, and optimize their advertising efforts across various campaigns, ad sets, and ads. + +## Features + +- **Business Management:** Retrieve all businesses associated with a user account for better organization and management. +- **Ad Account Access:** Access all ad accounts available to the user, allowing for streamlined management of advertising resources. +- **Detailed Account Insights:** Obtain comprehensive information about specific ad accounts, including performance metrics and settings. +- **Campaign Management:** Retrieve and manage campaigns linked to ad accounts, including filtering options based on status and limits. +- **Ad Set Management:** Access ad sets associated with specific campaigns or ad accounts, facilitating detailed analysis and organization. +- **Ad Management:** Retrieve ads linked to particular ad sets, campaigns, or accounts, enabling thorough oversight of advertising materials. +- **Performance Insights:** Gain insights into campaign, ad set, and ad performance, including metrics such as impressions, clicks, and conversions. +- **Custom Audience Management:** Manage custom audiences within ad accounts to target advertising more effectively. +- **Creative Management:** Retrieve and manage creative assets used in advertising efforts. +- **Bulk Operations:** Perform bulk updates on multiple campaigns, ad sets, or ads for efficient management. + +## Configuration + +To utilize these tools, authentication via OAuth is required. The tools are configured to interact with the Meta advertising platform, specifically through the Facebook Graph API. A network connection to `graph.facebook.com` is mandatory to access and manage advertising resources effectively. + +## Tool Descriptions + +### Business Management Tool + +- **Description:** Retrieve all businesses associated with the user account. +- **Input Requirements:** None. + +### Ad Account Tool + +- **Description:** Access all ad accounts available to the user. Optionally, filter by a specific business ID. +- **Input Requirements:** + - `business_id`: (optional) The ID of the business to filter ad accounts. + +### Account Info Tool + +- **Description:** Obtain detailed information about a specific ad account. +- **Input Requirements:** + - `account_id`: The ad account ID (format: act_123456789). + +### Campaign Management Tool + +- **Description:** Retrieve campaigns for a specific ad account, with options to filter by status and limit the number of campaigns returned. +- **Input Requirements:** + - `account_id`: The ad account ID (format: act_123456789). + - `limit`: (optional) The maximum number of campaigns to return. + - `status`: (optional) Filter for campaign statuses (ACTIVE, PAUSED, ARCHIVED). + +### Ad Set Management Tool + +- **Description:** Retrieve ad sets for a specific campaign or ad account, with options to filter by status and limit the number returned. +- **Input Requirements:** + - `account_id`: (optional) The ad account ID to filter ad sets. + - `campaign_id`: (optional) The campaign ID to filter ad sets. + - `limit`: (optional) The maximum number of ad sets to return. + - `status`: (optional) Filter for ad set statuses (ACTIVE, PAUSED, ARCHIVED). + +### Ad Management Tool + +- **Description:** Retrieve ads for a specific ad set, campaign, or ad account, with options to filter by status and limit the number returned. +- **Input Requirements:** + - `account_id`: (optional) The ad account ID to filter ads. + - `adset_id`: (optional) The ad set ID to filter ads. + - `campaign_id`: (optional) The campaign ID to filter ads. + - `limit`: (optional) The maximum number of ads to return. + - `status`: (optional) Filter for ad statuses (ACTIVE, PAUSED, ARCHIVED). + +### Performance Insights Tool + +- **Description:** Get performance insights for campaigns, ad sets, and ads, with customizable metrics and time ranges. +- **Input Requirements:** + - **For campaigns:** + - `account_id`: The account ID to get insights for. + - `campaign_ids`: An array of campaign IDs for insights. + - `date_preset`: Preset value for date ranges. + - `metrics`: Metrics to retrieve. + - `time_range`: Optional custom time range. + - **For ad sets:** + - Similar to above but focusing on ad sets. + - **For ads:** + - Similar to above but focusing on ads. + +### Audience Management Tool + +- **Description:** Retrieve custom audiences within a specific ad account. +- **Input Requirements:** + - `account_id`: The ad account ID (format: act_123456789). + - `limit`: (optional) The maximum number of audiences to return. + +### Creative Management Tool + +- **Description:** Retrieve creatives associated with an ad account. +- **Input Requirements:** + - `account_id`: The ad account ID (format: act_123456789). + - `limit`: (optional) The maximum number of creatives to return. + +### Bulk Update Tool + +- **Description:** Perform bulk updates on multiple campaigns, ad sets, or ads for streamlined management. +- **Input Requirements:** + - `operations`: An array of update operations specifying the type of object to update, object ID, and fields to update. + +This suite of tools is designed to enhance the management and analysis of advertising activities on the Meta platform, ensuring users can efficiently optimize their advertising strategies. + +## How to regsiter an oauth client + +### Creating an app +1. Go to https://developers.facebook.com/apps +2. Click on "Create App" +3. Fill in form, click on "Next" +4. In the use cases, click on "Other" +5. Select "Business activity" +6. In Business Portfolio pick the one for mcp.run/dylibso. Note: we will need to verify the business before being able to publish the app for all users, then click on "Create app" + +### Basic settings +1. Click on "App settings" -> "Basic", take note of App ID and App secret +2. Click on "Add platform" -> Website, and then in Site URL write "https://www.mcp.run/" +3. Note that you can start business verification from this page + +![image](https://github.com/user-attachments/assets/17268b36-ff48-4fe5-af93-315b75156b12) + +### Advanced Settings +1. Click on "App settings" -> "Advanced" +2. in "Share Redirect Domain Allow List" add "www.mcp.run" and "mcp.run" + +### Setting up Marketing API +1. Go to `https://developers.facebook.com/apps/:appId/add/` or click on "Add Product" on the sidebar +![image](https://github.com/user-attachments/assets/4cb69641-a223-4260-80b9-51647324d32b) +3. Click on "setup up" on Marketing API +4. you don't need to do anything else here + +### Setting up Facebook Login for Businesses +1. Go back to "Add product" and then set up "Facebook Login for Business" +2. Go to `https://developers.facebook.com/apps/:appId/business-login/settings/` +3. In `Valid OAuth Redirect URIs` add `https://www.mcp.run/api/login/servlet/dylibso/meta-ads/callback` + +## OAuth configuration + +- Scopes: `ads_management`, `ads_read`, `read_insights` +- Allowed domains: `graph.facebook.com` +- Authorize URL: `https://www.facebook.com/dialog/oauth` +- Token URL: `https://graph.facebook.com/oauth/access_token` \ No newline at end of file diff --git a/servlets/meta-ads/go.mod b/servlets/meta-ads/go.mod new file mode 100755 index 0000000..521187b --- /dev/null +++ b/servlets/meta-ads/go.mod @@ -0,0 +1,5 @@ +module meta-ads + +go 1.22.1 + +require github.com/extism/go-pdk v1.1.0 diff --git a/servlets/meta-ads/go.sum b/servlets/meta-ads/go.sum new file mode 100644 index 0000000..e0fb44c --- /dev/null +++ b/servlets/meta-ads/go.sum @@ -0,0 +1,2 @@ +github.com/extism/go-pdk v1.1.0 h1:K2On6XOERxrYdsgu0uLzCxeu/FYRHE8jId/hdEVSYoY= +github.com/extism/go-pdk v1.1.0/go.mod h1:Gz+LIU/YCKnKXhgge8yo5Yu1F/lbv7KtKFkiCSzW/P4= diff --git a/servlets/meta-ads/main.go b/servlets/meta-ads/main.go new file mode 100644 index 0000000..cfc82e5 --- /dev/null +++ b/servlets/meta-ads/main.go @@ -0,0 +1,129 @@ +package main + +import ( + "fmt" + + "github.com/extism/go-pdk" +) + +// Schema related types for tool description +type SchemaProperty struct { + Type string `json:"type"` + Description string `json:"description,omitempty"` + Items *schema `json:"items,omitempty"` +} + +func prop(tpe, description string) SchemaProperty { + return SchemaProperty{Type: tpe, Description: description} +} + +func propWithItems(tpe, description string, itemsSchema schema) SchemaProperty { + return SchemaProperty{ + Type: tpe, + Description: description, + Items: &itemsSchema, + } +} + +type schema = map[string]interface{} +type props = map[string]SchemaProperty + +// Helper function to create a pointer +func some[T any](t T) *T { + return &t +} + +// Called when the tool is invoked +func Call(input CallToolRequest) (CallToolResult, error) { + token, ok := pdk.GetConfig("OAUTH_TOKEN") + if !ok { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("No OAUTH_TOKEN configured"), + }}, + }, nil + } + + args := input.Params.Arguments.(map[string]interface{}) + pdk.Log(pdk.LogDebug, fmt.Sprint("Args: ", args)) + + switch input.Params.Name { + // Account Management + case GetBusinessesTool.Name: + return getBusinesses(token, args) + case GetAdAccountsTool.Name: + return getAdAccounts(token, args) + case GetAccountInfoTool.Name: + return getAccountInfo(token, args) + // Campaign Management + case GetCampaignsTool.Name: + return getCampaigns(token, args) + case CreateCampaignTool.Name: + return createCampaign(token, args) + case UpdateCampaignTool.Name: + return updateCampaign(token, args) + case DeleteCampaignTool.Name: + return deleteCampaign(token, args) + case GetCampaignInfoTool.Name: + return getCampaignInfo(token, args) + // Ad Set Management + case GetAdSetsTool.Name: + return getAdSets(token, args) + case CreateAdSetTool.Name: + return createAdSet(token, args) + case UpdateAdSetTool.Name: + return updateAdSet(token, args) + case DeleteAdSetTool.Name: + return deleteAdSet(token, args) + case GetAdSetInfoTool.Name: + return getAdSetInfo(token, args) + // Ad Management + case GetAdsTool.Name: + return getAds(token, args) + case CreateAdTool.Name: + return createAd(token, args) + case UpdateAdTool.Name: + return updateAd(token, args) + case DeleteAdTool.Name: + return deleteAd(token, args) + case GetAdInfoTool.Name: + return getAdInfo(token, args) + // Insights + case GetCampaignInsightsTool.Name: + return getCampaignInsights(token, args) + case GetAdSetInsightsTool.Name: + return getAdSetInsights(token, args) + case GetAdInsightsTool.Name: + return getAdInsights(token, args) + // Audiences + case GetAudiencesTool.Name: + return getAudiences(token, args) + case CreateAudienceTool.Name: + return createAudience(token, args) + // Creatives + case GetCreativesTool.Name: + return getCreatives(token, args) + case CreateCreativeTool.Name: + return createCreative(token, args) + // Bulk Operations + case BulkUpdateTool.Name: + return bulkUpdate(token, args) + default: + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Unknown tool " + input.Params.Name), + }}, + }, nil + } +} + +// Describe the tools provided by this servlet +func Describe() (ListToolsResult, error) { + return ListToolsResult{ + Tools: MetaAdsReadOnlyTools, + }, nil +} diff --git a/servlets/meta-ads/meta_ads.go b/servlets/meta-ads/meta_ads.go new file mode 100644 index 0000000..572a429 --- /dev/null +++ b/servlets/meta-ads/meta_ads.go @@ -0,0 +1,2059 @@ +package main + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/extism/go-pdk" +) + +// Tool definitions +var ( + // Account Management Tools + GetBusinessesTool = ToolDescription{ + Name: "get-businesses", + Description: "Get all businesses associated with the user account", + InputSchema: schema{ + "type": "object", + "properties": props{}, + }, + } + + GetAdAccountsTool = ToolDescription{ + Name: "get-ad-accounts", + Description: "Get all ad accounts accessible to the user", + InputSchema: schema{ + "type": "object", + "properties": props{ + "business_id": prop("string", "Optional business ID to filter ad accounts"), + }, + }, + } + + GetAccountInfoTool = ToolDescription{ + Name: "get-account-info", + Description: "Get detailed information about a specific ad account", + InputSchema: schema{ + "type": "object", + "properties": props{ + "account_id": prop("string", "The ad account ID (format: act_123456789)"), + }, + "required": []string{"account_id"}, + }, + } + + // Campaign Management Tools + GetCampaignsTool = ToolDescription{ + Name: "get-campaigns", + Description: "Get campaigns for a specific ad account", + InputSchema: schema{ + "type": "object", + "properties": props{ + "account_id": prop("string", "The ad account ID (format: act_123456789)"), + "status": prop("string", "Optional status filter (ACTIVE, PAUSED, ARCHIVED)"), + "limit": prop("integer", "Optional limit for number of campaigns to return (default: 25)"), + }, + "required": []string{"account_id"}, + }, + } + + CreateCampaignTool = ToolDescription{ + Name: "create-campaign", + Description: "Create a new campaign", + InputSchema: schema{ + "type": "object", + "properties": props{ + "account_id": prop("string", "The ad account ID (format: act_123456789)"), + "name": prop("string", "Campaign name"), + "objective": prop("string", "Campaign objective (e.g., LINK_CLICKS, CONVERSIONS, BRAND_AWARENESS)"), + "status": prop("string", "Campaign status (ACTIVE, PAUSED) - defaults to PAUSED"), + "special_ad_categories": propWithItems("array", "Special ad categories if applicable", schema{ + "type": "string", + "enum": []string{"CREDIT", "EMPLOYMENT", "HOUSING", "ISSUES_ELECTIONS_POLITICS"}, + }), + }, + "required": []string{"account_id", "name", "objective"}, + }, + } + + UpdateCampaignTool = ToolDescription{ + Name: "update-campaign", + Description: "Update an existing campaign", + InputSchema: schema{ + "type": "object", + "properties": props{ + "campaign_id": prop("string", "The campaign ID"), + "name": prop("string", "Optional new campaign name"), + "status": prop("string", "Optional new status (ACTIVE, PAUSED, ARCHIVED)"), + }, + "required": []string{"campaign_id"}, + }, + } + + DeleteCampaignTool = ToolDescription{ + Name: "delete-campaign", + Description: "Delete a campaign (moves it to archived state)", + InputSchema: schema{ + "type": "object", + "properties": props{ + "campaign_id": prop("string", "The campaign ID"), + }, + "required": []string{"campaign_id"}, + }, + } + + GetCampaignInfoTool = ToolDescription{ + Name: "get-campaign-info", + Description: "Get detailed information about a specific campaign", + InputSchema: schema{ + "type": "object", + "properties": props{ + "campaign_id": prop("string", "The campaign ID"), + }, + "required": []string{"campaign_id"}, + }, + } + + // Ad Set Management Tools + GetAdSetsTool = ToolDescription{ + Name: "get-adsets", + Description: "Get ad sets for a campaign or account", + InputSchema: schema{ + "type": "object", + "properties": props{ + "campaign_id": prop("string", "Optional campaign ID to filter ad sets"), + "account_id": prop("string", "Optional account ID to get all ad sets"), + "status": prop("string", "Optional status filter (ACTIVE, PAUSED, ARCHIVED)"), + "limit": prop("integer", "Optional limit for number of ad sets to return (default: 25)"), + }, + }, + } + + CreateAdSetTool = ToolDescription{ + Name: "create-adset", + Description: "Create a new ad set", + InputSchema: schema{ + "type": "object", + "properties": props{ + "campaign_id": prop("string", "The campaign ID"), + "name": prop("string", "Ad set name"), + "daily_budget": prop("integer", "Daily budget in cents (e.g., 1000 = $10.00)"), + "lifetime_budget": prop("integer", "Optional lifetime budget in cents"), + "targeting": prop("object", "Targeting specification as JSON object"), + "billing_event": prop("string", "Billing event (IMPRESSIONS, CLICKS, etc.) - defaults to IMPRESSIONS"), + "optimization_goal": prop("string", "Optimization goal - defaults to LINK_CLICKS"), + "status": prop("string", "Ad set status (ACTIVE, PAUSED) - defaults to PAUSED"), + }, + "required": []string{"campaign_id", "name", "targeting"}, + }, + } + + UpdateAdSetTool = ToolDescription{ + Name: "update-adset", + Description: "Update an existing ad set", + InputSchema: schema{ + "type": "object", + "properties": props{ + "adset_id": prop("string", "The ad set ID"), + "name": prop("string", "Optional new ad set name"), + "daily_budget": prop("integer", "Optional new daily budget in cents"), + "lifetime_budget": prop("integer", "Optional new lifetime budget in cents"), + "targeting": prop("object", "Optional new targeting specification"), + "status": prop("string", "Optional new status (ACTIVE, PAUSED, ARCHIVED)"), + }, + "required": []string{"adset_id"}, + }, + } + + DeleteAdSetTool = ToolDescription{ + Name: "delete-adset", + Description: "Delete an ad set (moves it to archived state)", + InputSchema: schema{ + "type": "object", + "properties": props{ + "adset_id": prop("string", "The ad set ID"), + }, + "required": []string{"adset_id"}, + }, + } + + GetAdSetInfoTool = ToolDescription{ + Name: "get-adset-info", + Description: "Get detailed information about a specific ad set", + InputSchema: schema{ + "type": "object", + "properties": props{ + "adset_id": prop("string", "The ad set ID"), + }, + "required": []string{"adset_id"}, + }, + } + + // Ad Management Tools + GetAdsTool = ToolDescription{ + Name: "get-ads", + Description: "Get ads for an ad set, campaign, or account", + InputSchema: schema{ + "type": "object", + "properties": props{ + "adset_id": prop("string", "Optional ad set ID to filter ads"), + "campaign_id": prop("string", "Optional campaign ID to filter ads"), + "account_id": prop("string", "Optional account ID to get all ads"), + "status": prop("string", "Optional status filter (ACTIVE, PAUSED, ARCHIVED)"), + "limit": prop("integer", "Optional limit for number of ads to return (default: 25)"), + }, + }, + } + + CreateAdTool = ToolDescription{ + Name: "create-ad", + Description: "Create a new ad", + InputSchema: schema{ + "type": "object", + "properties": props{ + "adset_id": prop("string", "The ad set ID"), + "name": prop("string", "Ad name"), + "creative_id": prop("string", "Creative ID to use for this ad"), + "status": prop("string", "Ad status (ACTIVE, PAUSED) - defaults to PAUSED"), + }, + "required": []string{"adset_id", "name", "creative_id"}, + }, + } + + UpdateAdTool = ToolDescription{ + Name: "update-ad", + Description: "Update an existing ad", + InputSchema: schema{ + "type": "object", + "properties": props{ + "ad_id": prop("string", "The ad ID"), + "name": prop("string", "Optional new ad name"), + "creative_id": prop("string", "Optional new creative ID"), + "status": prop("string", "Optional new status (ACTIVE, PAUSED, ARCHIVED)"), + }, + "required": []string{"ad_id"}, + }, + } + + DeleteAdTool = ToolDescription{ + Name: "delete-ad", + Description: "Delete an ad (moves it to archived state)", + InputSchema: schema{ + "type": "object", + "properties": props{ + "ad_id": prop("string", "The ad ID"), + }, + "required": []string{"ad_id"}, + }, + } + + GetAdInfoTool = ToolDescription{ + Name: "get-ad-info", + Description: "Get detailed information about a specific ad", + InputSchema: schema{ + "type": "object", + "properties": props{ + "ad_id": prop("string", "The ad ID"), + }, + "required": []string{"ad_id"}, + }, + } + + // Insights Tools + GetCampaignInsightsTool = ToolDescription{ + Name: "get-campaign-insights", + Description: "Get performance insights for campaigns", + InputSchema: schema{ + "type": "object", + "properties": props{ + "campaign_ids": propWithItems("array", "Array of campaign IDs", schema{"type": "string"}), + "account_id": prop("string", "Account ID to get insights for all campaigns"), + "date_preset": prop("string", "Date preset (today, yesterday, last_7d, last_30d, etc.)"), + "time_range": prop("object", "Custom time range with since and until dates"), + "metrics": propWithItems("array", "Metrics to retrieve", schema{ + "type": "string", + "enum": []string{"impressions", "clicks", "spend", "reach", "frequency", "ctr", "cpc", "cpm", "cpp", "cost_per_result", "conversions", "conversion_rate"}, + }), + }, + }, + } + + GetAdSetInsightsTool = ToolDescription{ + Name: "get-adset-insights", + Description: "Get performance insights for ad sets", + InputSchema: schema{ + "type": "object", + "properties": props{ + "adset_ids": propWithItems("array", "Array of ad set IDs", schema{"type": "string"}), + "campaign_id": prop("string", "Campaign ID to get insights for all ad sets"), + "account_id": prop("string", "Account ID to get insights for all ad sets"), + "date_preset": prop("string", "Date preset (today, yesterday, last_7d, last_30d, etc.)"), + "time_range": prop("object", "Custom time range with since and until dates"), + "metrics": propWithItems("array", "Metrics to retrieve", schema{ + "type": "string", + "enum": []string{"impressions", "clicks", "spend", "reach", "frequency", "ctr", "cpc", "cpm", "cpp", "cost_per_result", "conversions", "conversion_rate"}, + }), + }, + }, + } + + GetAdInsightsTool = ToolDescription{ + Name: "get-ad-insights", + Description: "Get performance insights for ads", + InputSchema: schema{ + "type": "object", + "properties": props{ + "ad_ids": propWithItems("array", "Array of ad IDs", schema{"type": "string"}), + "adset_id": prop("string", "Ad set ID to get insights for all ads"), + "campaign_id": prop("string", "Campaign ID to get insights for all ads"), + "account_id": prop("string", "Account ID to get insights for all ads"), + "date_preset": prop("string", "Date preset (today, yesterday, last_7d, last_30d, etc.)"), + "time_range": prop("object", "Custom time range with since and until dates"), + "metrics": propWithItems("array", "Metrics to retrieve", schema{ + "type": "string", + "enum": []string{"impressions", "clicks", "spend", "reach", "frequency", "ctr", "cpc", "cpm", "cpp", "cost_per_result", "conversions", "conversion_rate"}, + }), + }, + }, + } + + // Audience Management Tools + GetAudiencesTool = ToolDescription{ + Name: "get-audiences", + Description: "Get custom audiences for an ad account", + InputSchema: schema{ + "type": "object", + "properties": props{ + "account_id": prop("string", "The ad account ID (format: act_123456789)"), + "limit": prop("integer", "Optional limit for number of audiences to return (default: 25)"), + }, + "required": []string{"account_id"}, + }, + } + + CreateAudienceTool = ToolDescription{ + Name: "create-audience", + Description: "Create a custom audience", + InputSchema: schema{ + "type": "object", + "properties": props{ + "account_id": prop("string", "The ad account ID (format: act_123456789)"), + "name": prop("string", "Audience name"), + "description": prop("string", "Optional audience description"), + "subtype": prop("string", "Audience subtype (CUSTOM, LOOKALIKE, WEBSITE, etc.)"), + }, + "required": []string{"account_id", "name", "subtype"}, + }, + } + + // Creative Management Tools + GetCreativesTool = ToolDescription{ + Name: "get-creatives", + Description: "Get creatives for an ad account", + InputSchema: schema{ + "type": "object", + "properties": props{ + "account_id": prop("string", "The ad account ID (format: act_123456789)"), + "limit": prop("integer", "Optional limit for number of creatives to return (default: 25)"), + }, + "required": []string{"account_id"}, + }, + } + + CreateCreativeTool = ToolDescription{ + Name: "create-creative", + Description: "Create an ad creative", + InputSchema: schema{ + "type": "object", + "properties": props{ + "account_id": prop("string", "The ad account ID (format: act_123456789)"), + "name": prop("string", "Creative name"), + "object_story_spec": prop("object", "Object story specification for the creative"), + "degrees_of_freedom_spec": prop("object", "Optional degrees of freedom specification"), + }, + "required": []string{"account_id", "name", "object_story_spec"}, + }, + } + + // Bulk Operations + BulkUpdateTool = ToolDescription{ + Name: "bulk-update", + Description: "Perform bulk updates on multiple campaigns, ad sets, or ads", + InputSchema: schema{ + "type": "object", + "properties": props{ + "operations": propWithItems("array", "Array of update operations", schema{ + "type": "object", + "properties": props{ + "object_type": prop("string", "Type of object (campaign, adset, ad)"), + "object_id": prop("string", "ID of the object to update"), + "updates": prop("object", "Fields to update with their new values"), + }, + "required": []string{"object_type", "object_id", "updates"}, + }), + }, + "required": []string{"operations"}, + }, + } + + // All tools collection + MetaAdsTools = []ToolDescription{ + GetBusinessesTool, + GetAdAccountsTool, + GetAccountInfoTool, + GetCampaignsTool, + CreateCampaignTool, + UpdateCampaignTool, + DeleteCampaignTool, + GetCampaignInfoTool, + GetAdSetsTool, + CreateAdSetTool, + UpdateAdSetTool, + DeleteAdSetTool, + GetAdSetInfoTool, + GetAdsTool, + CreateAdTool, + UpdateAdTool, + DeleteAdTool, + GetAdInfoTool, + GetCampaignInsightsTool, + GetAdSetInsightsTool, + GetAdInsightsTool, + GetAudiencesTool, + CreateAudienceTool, + GetCreativesTool, + CreateCreativeTool, + BulkUpdateTool, + } + + MetaAdsReadOnlyTools = []ToolDescription{ + GetBusinessesTool, + GetAdAccountsTool, + GetAccountInfoTool, + GetCampaignsTool, + + GetCampaignInfoTool, + GetAdSetsTool, + + GetAdSetInfoTool, + GetAdsTool, + + GetAdInfoTool, + GetCampaignInsightsTool, + GetAdSetInsightsTool, + GetAdInsightsTool, + GetAudiencesTool, + + GetCreativesTool, + + BulkUpdateTool, + } +) + +// Account Management Functions +func getBusinesses(token string, args map[string]any) (CallToolResult, error) { + client := NewMetaClient(token) + + resp, err := client.MakeRequest(pdk.MethodGet, "me/businesses", nil, nil) + if err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to get businesses: %s", err)), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body)), + }}, + }, nil +} + +func getAdAccounts(token string, args map[string]any) (CallToolResult, error) { + client := NewMetaClient(token) + + query := map[string][]string{ + "fields": {"id,name,account_status,currency,timezone_name,business"}, + } + + if businessId, ok := args["business_id"].(string); ok && businessId != "" { + resp, err := client.MakeRequest(pdk.MethodGet, fmt.Sprintf("%s/adaccounts", businessId), query, nil) + if err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to get ad accounts: %s", err)), + }}, + }, nil + } + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body)), + }}, + }, nil + } + + resp, err := client.MakeRequest(pdk.MethodGet, "me/adaccounts", query, nil) + if err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to get ad accounts: %s", err)), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body)), + }}, + }, nil +} + +func getAccountInfo(token string, args map[string]any) (CallToolResult, error) { + accountId, ok := args["account_id"].(string) + if !ok || accountId == "" { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Account ID is required"), + }}, + }, nil + } + + client := NewMetaClient(token) + + query := map[string][]string{ + "fields": {"id,name,account_status,currency,timezone_name,business,spend_cap,balance,amount_spent,created_time"}, + } + + resp, err := client.MakeRequest(pdk.MethodGet, accountId, query, nil) + if err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to get account info: %s", err)), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body)), + }}, + }, nil +} + +// Campaign Management Functions +func getCampaigns(token string, args map[string]any) (CallToolResult, error) { + accountId, ok := args["account_id"].(string) + if !ok || accountId == "" { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Account ID is required"), + }}, + }, nil + } + + client := NewMetaClient(token) + + query := map[string][]string{ + "fields": {"id,name,objective,status,created_time,updated_time,start_time,stop_time,daily_budget,lifetime_budget"}, + } + + if status, ok := args["status"].(string); ok && status != "" { + query["filtering"] = []string{fmt.Sprintf(`[{"field":"status","operator":"EQUAL","value":"%s"}]`, status)} + } + + if limit, ok := args["limit"].(float64); ok && limit > 0 { + query["limit"] = []string{fmt.Sprintf("%.0f", limit)} + } + + resp, err := client.MakeRequest(pdk.MethodGet, fmt.Sprintf("%s/campaigns", accountId), query, nil) + if err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to get campaigns: %s", err)), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body)), + }}, + }, nil +} + +func createCampaign(token string, args map[string]any) (CallToolResult, error) { + accountId, ok := args["account_id"].(string) + if !ok || accountId == "" { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Account ID is required"), + }}, + }, nil + } + + name, ok := args["name"].(string) + if !ok || name == "" { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Campaign name is required"), + }}, + }, nil + } + + objective, ok := args["objective"].(string) + if !ok || objective == "" { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Campaign objective is required"), + }}, + }, nil + } + + client := NewMetaClient(token) + + formData := map[string]string{ + "name": name, + "objective": objective, + "status": "PAUSED", // Default to paused for safety + } + + if status, ok := args["status"].(string); ok && status != "" { + formData["status"] = status + } + + if categories, ok := args["special_ad_categories"].([]any); ok && len(categories) > 0 { + var categoryStrs []string + for _, cat := range categories { + if catStr, ok := cat.(string); ok { + categoryStrs = append(categoryStrs, catStr) + } + } + if len(categoryStrs) > 0 { + categoriesJson, _ := json.Marshal(categoryStrs) + formData["special_ad_categories"] = string(categoriesJson) + } + } + + resp, err := client.MakeFormRequest(fmt.Sprintf("%s/campaigns", accountId), formData) + if err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to create campaign: %s", err)), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body)), + }}, + }, nil +} + +func updateCampaign(token string, args map[string]any) (CallToolResult, error) { + campaignId, ok := args["campaign_id"].(string) + if !ok || campaignId == "" { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Campaign ID is required"), + }}, + }, nil + } + + client := NewMetaClient(token) + formData := map[string]string{} + + if name, ok := args["name"].(string); ok && name != "" { + formData["name"] = name + } + + if status, ok := args["status"].(string); ok && status != "" { + formData["status"] = status + } + + if len(formData) == 0 { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("At least one field to update is required"), + }}, + }, nil + } + + resp, err := client.MakeFormRequest(campaignId, formData) + if err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to update campaign: %s", err)), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body)), + }}, + }, nil +} + +func deleteCampaign(token string, args map[string]any) (CallToolResult, error) { + campaignId, ok := args["campaign_id"].(string) + if !ok || campaignId == "" { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Campaign ID is required"), + }}, + }, nil + } + + client := NewMetaClient(token) + + resp, err := client.MakeRequest(pdk.MethodDelete, campaignId, nil, nil) + if err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to delete campaign: %s", err)), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body)), + }}, + }, nil +} + +func getCampaignInfo(token string, args map[string]any) (CallToolResult, error) { + campaignId, ok := args["campaign_id"].(string) + if !ok || campaignId == "" { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Campaign ID is required"), + }}, + }, nil + } + + client := NewMetaClient(token) + + query := map[string][]string{ + "fields": {"id,name,objective,status,created_time,updated_time,start_time,stop_time,daily_budget,lifetime_budget,account_id,special_ad_categories"}, + } + + resp, err := client.MakeRequest(pdk.MethodGet, campaignId, query, nil) + if err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to get campaign info: %s", err)), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body)), + }}, + }, nil +} + +// Ad Set Management Functions +func getAdSets(token string, args map[string]any) (CallToolResult, error) { + client := NewMetaClient(token) + + query := map[string][]string{ + "fields": {"id,name,status,created_time,updated_time,daily_budget,lifetime_budget,billing_event,optimization_goal,targeting"}, + } + + var endpoint string + if campaignId, ok := args["campaign_id"].(string); ok && campaignId != "" { + endpoint = fmt.Sprintf("%s/adsets", campaignId) + } else if accountId, ok := args["account_id"].(string); ok && accountId != "" { + endpoint = fmt.Sprintf("%s/adsets", accountId) + } else { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Either campaign_id or account_id is required"), + }}, + }, nil + } + + if status, ok := args["status"].(string); ok && status != "" { + query["filtering"] = []string{fmt.Sprintf(`[{"field":"status","operator":"EQUAL","value":"%s"}]`, status)} + } + + if limit, ok := args["limit"].(float64); ok && limit > 0 { + query["limit"] = []string{fmt.Sprintf("%.0f", limit)} + } + + resp, err := client.MakeRequest(pdk.MethodGet, endpoint, query, nil) + if err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to get ad sets: %s", err)), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body)), + }}, + }, nil +} + +func createAdSet(token string, args map[string]any) (CallToolResult, error) { + campaignId, ok := args["campaign_id"].(string) + if !ok || campaignId == "" { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Campaign ID is required"), + }}, + }, nil + } + + name, ok := args["name"].(string) + if !ok || name == "" { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Ad set name is required"), + }}, + }, nil + } + + targeting, ok := args["targeting"].(map[string]any) + if !ok || len(targeting) == 0 { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Targeting is required"), + }}, + }, nil + } + + client := NewMetaClient(token) + + formData := map[string]string{ + "name": name, + "campaign_id": campaignId, + "billing_event": "IMPRESSIONS", + "optimization_goal": "LINK_CLICKS", + "status": "PAUSED", // Default to paused + } + + if dailyBudget, ok := args["daily_budget"].(float64); ok && dailyBudget > 0 { + formData["daily_budget"] = fmt.Sprintf("%.0f", dailyBudget) + } + + if lifetimeBudget, ok := args["lifetime_budget"].(float64); ok && lifetimeBudget > 0 { + formData["lifetime_budget"] = fmt.Sprintf("%.0f", lifetimeBudget) + } + + if billingEvent, ok := args["billing_event"].(string); ok && billingEvent != "" { + formData["billing_event"] = billingEvent + } + + if optimizationGoal, ok := args["optimization_goal"].(string); ok && optimizationGoal != "" { + formData["optimization_goal"] = optimizationGoal + } + + if status, ok := args["status"].(string); ok && status != "" { + formData["status"] = status + } + + // Convert targeting to JSON string + targetingJson, err := json.Marshal(targeting) + if err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to marshal targeting: %s", err)), + }}, + }, nil + } + formData["targeting"] = string(targetingJson) + + // Get account ID from campaign + campaignInfo, err := client.MakeRequest(pdk.MethodGet, campaignId, map[string][]string{"fields": {"account_id"}}, nil) + if err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to get campaign info: %s", err)), + }}, + }, nil + } + + var campaignData map[string]any + if err := json.Unmarshal(campaignInfo.Body, &campaignData); err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to parse campaign data: %s", err)), + }}, + }, nil + } + + accountId, ok := campaignData["account_id"].(string) + if !ok { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Failed to get account ID from campaign"), + }}, + }, nil + } + + resp, err := client.MakeFormRequest(fmt.Sprintf("%s/adsets", accountId), formData) + if err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to create ad set: %s", err)), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body)), + }}, + }, nil +} + +func updateAdSet(token string, args map[string]any) (CallToolResult, error) { + adsetId, ok := args["adset_id"].(string) + if !ok || adsetId == "" { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Ad set ID is required"), + }}, + }, nil + } + + client := NewMetaClient(token) + formData := map[string]string{} + + if name, ok := args["name"].(string); ok && name != "" { + formData["name"] = name + } + + if dailyBudget, ok := args["daily_budget"].(float64); ok && dailyBudget > 0 { + formData["daily_budget"] = fmt.Sprintf("%.0f", dailyBudget) + } + + if lifetimeBudget, ok := args["lifetime_budget"].(float64); ok && lifetimeBudget > 0 { + formData["lifetime_budget"] = fmt.Sprintf("%.0f", lifetimeBudget) + } + + if targeting, ok := args["targeting"].(map[string]any); ok && len(targeting) > 0 { + targetingJson, err := json.Marshal(targeting) + if err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to marshal targeting: %s", err)), + }}, + }, nil + } + formData["targeting"] = string(targetingJson) + } + + if status, ok := args["status"].(string); ok && status != "" { + formData["status"] = status + } + + if len(formData) == 0 { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("At least one field to update is required"), + }}, + }, nil + } + + resp, err := client.MakeFormRequest(adsetId, formData) + if err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to update ad set: %s", err)), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body)), + }}, + }, nil +} + +func deleteAdSet(token string, args map[string]any) (CallToolResult, error) { + adsetId, ok := args["adset_id"].(string) + if !ok || adsetId == "" { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Ad set ID is required"), + }}, + }, nil + } + + client := NewMetaClient(token) + + resp, err := client.MakeRequest(pdk.MethodDelete, adsetId, nil, nil) + if err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to delete ad set: %s", err)), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body)), + }}, + }, nil +} + +func getAdSetInfo(token string, args map[string]any) (CallToolResult, error) { + adsetId, ok := args["adset_id"].(string) + if !ok || adsetId == "" { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Ad set ID is required"), + }}, + }, nil + } + + client := NewMetaClient(token) + + query := map[string][]string{ + "fields": {"id,name,status,created_time,updated_time,daily_budget,lifetime_budget,billing_event,optimization_goal,targeting,campaign_id,account_id"}, + } + + resp, err := client.MakeRequest(pdk.MethodGet, adsetId, query, nil) + if err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to get ad set info: %s", err)), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body)), + }}, + }, nil +} + +// Ad Management Functions +func getAds(token string, args map[string]any) (CallToolResult, error) { + client := NewMetaClient(token) + + query := map[string][]string{ + "fields": {"id,name,status,created_time,updated_time,creative,adset_id,campaign_id,account_id"}, + } + + var endpoint string + if adsetId, ok := args["adset_id"].(string); ok && adsetId != "" { + endpoint = fmt.Sprintf("%s/ads", adsetId) + } else if campaignId, ok := args["campaign_id"].(string); ok && campaignId != "" { + endpoint = fmt.Sprintf("%s/ads", campaignId) + } else if accountId, ok := args["account_id"].(string); ok && accountId != "" { + endpoint = fmt.Sprintf("%s/ads", accountId) + } else { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Either adset_id, campaign_id, or account_id is required"), + }}, + }, nil + } + + if status, ok := args["status"].(string); ok && status != "" { + query["filtering"] = []string{fmt.Sprintf(`[{"field":"status","operator":"EQUAL","value":"%s"}]`, status)} + } + + if limit, ok := args["limit"].(float64); ok && limit > 0 { + query["limit"] = []string{fmt.Sprintf("%.0f", limit)} + } + + resp, err := client.MakeRequest(pdk.MethodGet, endpoint, query, nil) + if err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to get ads: %s", err)), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body)), + }}, + }, nil +} + +func createAd(token string, args map[string]any) (CallToolResult, error) { + adsetId, ok := args["adset_id"].(string) + if !ok || adsetId == "" { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Ad set ID is required"), + }}, + }, nil + } + + name, ok := args["name"].(string) + if !ok || name == "" { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Ad name is required"), + }}, + }, nil + } + + creativeId, ok := args["creative_id"].(string) + if !ok || creativeId == "" { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Creative ID is required"), + }}, + }, nil + } + + client := NewMetaClient(token) + + formData := map[string]string{ + "name": name, + "adset_id": adsetId, + "creative": fmt.Sprintf(`{"creative_id":"%s"}`, creativeId), + "status": "PAUSED", // Default to paused + } + + if status, ok := args["status"].(string); ok && status != "" { + formData["status"] = status + } + + // Get account ID from ad set + adsetInfo, err := client.MakeRequest(pdk.MethodGet, adsetId, map[string][]string{"fields": {"account_id"}}, nil) + if err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to get ad set info: %s", err)), + }}, + }, nil + } + + var adsetData map[string]any + if err := json.Unmarshal(adsetInfo.Body, &adsetData); err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to parse ad set data: %s", err)), + }}, + }, nil + } + + accountId, ok := adsetData["account_id"].(string) + if !ok { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Failed to get account ID from ad set"), + }}, + }, nil + } + + resp, err := client.MakeFormRequest(fmt.Sprintf("%s/ads", accountId), formData) + if err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to create ad: %s", err)), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body)), + }}, + }, nil +} + +func updateAd(token string, args map[string]any) (CallToolResult, error) { + adId, ok := args["ad_id"].(string) + if !ok || adId == "" { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Ad ID is required"), + }}, + }, nil + } + + client := NewMetaClient(token) + formData := map[string]string{} + + if name, ok := args["name"].(string); ok && name != "" { + formData["name"] = name + } + + if creativeId, ok := args["creative_id"].(string); ok && creativeId != "" { + formData["creative"] = fmt.Sprintf(`{"creative_id":"%s"}`, creativeId) + } + + if status, ok := args["status"].(string); ok && status != "" { + formData["status"] = status + } + + if len(formData) == 0 { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("At least one field to update is required"), + }}, + }, nil + } + + resp, err := client.MakeFormRequest(adId, formData) + if err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to update ad: %s", err)), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body)), + }}, + }, nil +} + +func deleteAd(token string, args map[string]any) (CallToolResult, error) { + adId, ok := args["ad_id"].(string) + if !ok || adId == "" { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Ad ID is required"), + }}, + }, nil + } + + client := NewMetaClient(token) + + resp, err := client.MakeRequest(pdk.MethodDelete, adId, nil, nil) + if err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to delete ad: %s", err)), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body)), + }}, + }, nil +} + +func getAdInfo(token string, args map[string]any) (CallToolResult, error) { + adId, ok := args["ad_id"].(string) + if !ok || adId == "" { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Ad ID is required"), + }}, + }, nil + } + + client := NewMetaClient(token) + + query := map[string][]string{ + "fields": {"id,name,status,created_time,updated_time,creative,adset_id,campaign_id,account_id"}, + } + + resp, err := client.MakeRequest(pdk.MethodGet, adId, query, nil) + if err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to get ad info: %s", err)), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body)), + }}, + }, nil +} + +// Insights Functions +func getCampaignInsights(token string, args map[string]any) (CallToolResult, error) { + client := NewMetaClient(token) + + query := map[string][]string{} + + // Set default metrics if none provided + metrics := []string{"impressions", "clicks", "spend", "reach", "ctr", "cpc", "cpm"} + if providedMetrics, ok := args["metrics"].([]any); ok && len(providedMetrics) > 0 { + metrics = []string{} + for _, m := range providedMetrics { + if metricStr, ok := m.(string); ok { + metrics = append(metrics, metricStr) + } + } + } + query["fields"] = []string{strings.Join(metrics, ",")} + + // Handle date range + if datePreset, ok := args["date_preset"].(string); ok && datePreset != "" { + query["date_preset"] = []string{datePreset} + } else if timeRange, ok := args["time_range"].(map[string]any); ok { + if since, ok := timeRange["since"].(string); ok { + if until, ok := timeRange["until"].(string); ok { + query["time_range"] = []string{fmt.Sprintf(`{"since":"%s","until":"%s"}`, since, until)} + } + } + } else { + query["date_preset"] = []string{"last_7d"} // Default + } + + var endpoint string + if campaignIds, ok := args["campaign_ids"].([]any); ok && len(campaignIds) > 0 { + // Handle multiple campaigns - need to make multiple requests + var allInsights []map[string]any + for _, id := range campaignIds { + if campaignId, ok := id.(string); ok { + resp, err := client.MakeRequest(pdk.MethodGet, fmt.Sprintf("%s/insights", campaignId), query, nil) + if err != nil { + continue // Skip failed requests + } + var insights map[string]any + if err := json.Unmarshal(resp.Body, &insights); err == nil { + if data, ok := insights["data"].([]any); ok { + for _, insight := range data { + if insightMap, ok := insight.(map[string]any); ok { + allInsights = append(allInsights, insightMap) + } + } + } + } + } + } + result := map[string]any{ + "data": allInsights, + } + jsonResult, _ := json.Marshal(result) + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(jsonResult)), + }}, + }, nil + } else if accountId, ok := args["account_id"].(string); ok && accountId != "" { + endpoint = fmt.Sprintf("%s/insights", accountId) + query["level"] = []string{"campaign"} + } else { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Either campaign_ids or account_id is required"), + }}, + }, nil + } + + resp, err := client.MakeRequest(pdk.MethodGet, endpoint, query, nil) + if err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to get campaign insights: %s", err)), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body)), + }}, + }, nil +} + +func getAdSetInsights(token string, args map[string]any) (CallToolResult, error) { + client := NewMetaClient(token) + + query := map[string][]string{} + + // Set default metrics if none provided + metrics := []string{"impressions", "clicks", "spend", "reach", "ctr", "cpc", "cpm"} + if providedMetrics, ok := args["metrics"].([]any); ok && len(providedMetrics) > 0 { + metrics = []string{} + for _, m := range providedMetrics { + if metricStr, ok := m.(string); ok { + metrics = append(metrics, metricStr) + } + } + } + query["fields"] = []string{strings.Join(metrics, ",")} + + // Handle date range + if datePreset, ok := args["date_preset"].(string); ok && datePreset != "" { + query["date_preset"] = []string{datePreset} + } else if timeRange, ok := args["time_range"].(map[string]any); ok { + if since, ok := timeRange["since"].(string); ok { + if until, ok := timeRange["until"].(string); ok { + query["time_range"] = []string{fmt.Sprintf(`{"since":"%s","until":"%s"}`, since, until)} + } + } + } else { + query["date_preset"] = []string{"last_7d"} // Default + } + + var endpoint string + if adsetIds, ok := args["adset_ids"].([]any); ok && len(adsetIds) > 0 { + // Handle multiple ad sets + var allInsights []map[string]any + for _, id := range adsetIds { + if adsetId, ok := id.(string); ok { + resp, err := client.MakeRequest(pdk.MethodGet, fmt.Sprintf("%s/insights", adsetId), query, nil) + if err != nil { + continue // Skip failed requests + } + var insights map[string]any + if err := json.Unmarshal(resp.Body, &insights); err == nil { + if data, ok := insights["data"].([]any); ok { + for _, insight := range data { + if insightMap, ok := insight.(map[string]any); ok { + allInsights = append(allInsights, insightMap) + } + } + } + } + } + } + result := map[string]any{ + "data": allInsights, + } + jsonResult, _ := json.Marshal(result) + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(jsonResult)), + }}, + }, nil + } else if campaignId, ok := args["campaign_id"].(string); ok && campaignId != "" { + endpoint = fmt.Sprintf("%s/insights", campaignId) + query["level"] = []string{"adset"} + } else if accountId, ok := args["account_id"].(string); ok && accountId != "" { + endpoint = fmt.Sprintf("%s/insights", accountId) + query["level"] = []string{"adset"} + } else { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Either adset_ids, campaign_id, or account_id is required"), + }}, + }, nil + } + + resp, err := client.MakeRequest(pdk.MethodGet, endpoint, query, nil) + if err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to get ad set insights: %s", err)), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body)), + }}, + }, nil +} + +func getAdInsights(token string, args map[string]any) (CallToolResult, error) { + client := NewMetaClient(token) + + query := map[string][]string{} + + // Set default metrics if none provided + metrics := []string{"impressions", "clicks", "spend", "reach", "ctr", "cpc", "cpm"} + if providedMetrics, ok := args["metrics"].([]any); ok && len(providedMetrics) > 0 { + metrics = []string{} + for _, m := range providedMetrics { + if metricStr, ok := m.(string); ok { + metrics = append(metrics, metricStr) + } + } + } + query["fields"] = []string{strings.Join(metrics, ",")} + + // Handle date range + if datePreset, ok := args["date_preset"].(string); ok && datePreset != "" { + query["date_preset"] = []string{datePreset} + } else if timeRange, ok := args["time_range"].(map[string]any); ok { + if since, ok := timeRange["since"].(string); ok { + if until, ok := timeRange["until"].(string); ok { + query["time_range"] = []string{fmt.Sprintf(`{"since":"%s","until":"%s"}`, since, until)} + } + } + } else { + query["date_preset"] = []string{"last_7d"} // Default + } + + var endpoint string + if adIds, ok := args["ad_ids"].([]any); ok && len(adIds) > 0 { + // Handle multiple ads + var allInsights []map[string]any + for _, id := range adIds { + if adId, ok := id.(string); ok { + resp, err := client.MakeRequest(pdk.MethodGet, fmt.Sprintf("%s/insights", adId), query, nil) + if err != nil { + continue // Skip failed requests + } + var insights map[string]any + if err := json.Unmarshal(resp.Body, &insights); err == nil { + if data, ok := insights["data"].([]any); ok { + for _, insight := range data { + if insightMap, ok := insight.(map[string]any); ok { + allInsights = append(allInsights, insightMap) + } + } + } + } + } + } + result := map[string]any{ + "data": allInsights, + } + jsonResult, _ := json.Marshal(result) + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(jsonResult)), + }}, + }, nil + } else if adsetId, ok := args["adset_id"].(string); ok && adsetId != "" { + endpoint = fmt.Sprintf("%s/insights", adsetId) + query["level"] = []string{"ad"} + } else if campaignId, ok := args["campaign_id"].(string); ok && campaignId != "" { + endpoint = fmt.Sprintf("%s/insights", campaignId) + query["level"] = []string{"ad"} + } else if accountId, ok := args["account_id"].(string); ok && accountId != "" { + endpoint = fmt.Sprintf("%s/insights", accountId) + query["level"] = []string{"ad"} + } else { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Either ad_ids, adset_id, campaign_id, or account_id is required"), + }}, + }, nil + } + + resp, err := client.MakeRequest(pdk.MethodGet, endpoint, query, nil) + if err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to get ad insights: %s", err)), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body)), + }}, + }, nil +} + +// Audience Management Functions +func getAudiences(token string, args map[string]any) (CallToolResult, error) { + accountId, ok := args["account_id"].(string) + if !ok || accountId == "" { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Account ID is required"), + }}, + }, nil + } + + client := NewMetaClient(token) + + query := map[string][]string{ + "fields": {"id,name,description,audience_type,subtype,time_created,time_updated,approximate_count"}, + } + + if limit, ok := args["limit"].(float64); ok && limit > 0 { + query["limit"] = []string{fmt.Sprintf("%.0f", limit)} + } + + resp, err := client.MakeRequest(pdk.MethodGet, fmt.Sprintf("%s/customaudiences", accountId), query, nil) + if err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to get audiences: %s", err)), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body)), + }}, + }, nil +} + +func createAudience(token string, args map[string]any) (CallToolResult, error) { + accountId, ok := args["account_id"].(string) + if !ok || accountId == "" { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Account ID is required"), + }}, + }, nil + } + + name, ok := args["name"].(string) + if !ok || name == "" { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Audience name is required"), + }}, + }, nil + } + + subtype, ok := args["subtype"].(string) + if !ok || subtype == "" { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Audience subtype is required"), + }}, + }, nil + } + + client := NewMetaClient(token) + + formData := map[string]string{ + "name": name, + "subtype": subtype, + } + + if description, ok := args["description"].(string); ok && description != "" { + formData["description"] = description + } + + resp, err := client.MakeFormRequest(fmt.Sprintf("%s/customaudiences", accountId), formData) + if err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to create audience: %s", err)), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body)), + }}, + }, nil +} + +// Creative Management Functions +func getCreatives(token string, args map[string]any) (CallToolResult, error) { + accountId, ok := args["account_id"].(string) + if !ok || accountId == "" { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Account ID is required"), + }}, + }, nil + } + + client := NewMetaClient(token) + + query := map[string][]string{ + "fields": {"id,name,status,object_story_spec,degrees_of_freedom_spec,body,title,image_url,video_id"}, + } + + if limit, ok := args["limit"].(float64); ok && limit > 0 { + query["limit"] = []string{fmt.Sprintf("%.0f", limit)} + } + + resp, err := client.MakeRequest(pdk.MethodGet, fmt.Sprintf("%s/adcreatives", accountId), query, nil) + if err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to get creatives: %s", err)), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body)), + }}, + }, nil +} + +func createCreative(token string, args map[string]any) (CallToolResult, error) { + accountId, ok := args["account_id"].(string) + if !ok || accountId == "" { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Account ID is required"), + }}, + }, nil + } + + name, ok := args["name"].(string) + if !ok || name == "" { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Creative name is required"), + }}, + }, nil + } + + objectStorySpec, ok := args["object_story_spec"].(map[string]any) + if !ok || len(objectStorySpec) == 0 { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Object story spec is required"), + }}, + }, nil + } + + client := NewMetaClient(token) + + formData := map[string]string{ + "name": name, + } + + // Convert object story spec to JSON + objectStorySpecJson, err := json.Marshal(objectStorySpec) + if err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to marshal object story spec: %s", err)), + }}, + }, nil + } + formData["object_story_spec"] = string(objectStorySpecJson) + + if degreesOfFreedomSpec, ok := args["degrees_of_freedom_spec"].(map[string]any); ok && len(degreesOfFreedomSpec) > 0 { + degreesOfFreedomSpecJson, err := json.Marshal(degreesOfFreedomSpec) + if err == nil { + formData["degrees_of_freedom_spec"] = string(degreesOfFreedomSpecJson) + } + } + + resp, err := client.MakeFormRequest(fmt.Sprintf("%s/adcreatives", accountId), formData) + if err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to create creative: %s", err)), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body)), + }}, + }, nil +} + +// Bulk Operations +func bulkUpdate(token string, args map[string]any) (CallToolResult, error) { + operations, ok := args["operations"].([]any) + if !ok || len(operations) == 0 { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Operations array is required"), + }}, + }, nil + } + + client := NewMetaClient(token) + var results []map[string]any + + for i, op := range operations { + opMap, ok := op.(map[string]any) + if !ok { + results = append(results, map[string]any{ + "index": i, + "success": false, + "error": "Invalid operation format", + }) + continue + } + + objectType, ok := opMap["object_type"].(string) + if !ok { + results = append(results, map[string]any{ + "index": i, + "success": false, + "error": "Missing object_type", + }) + continue + } + + objectId, ok := opMap["object_id"].(string) + if !ok { + results = append(results, map[string]any{ + "index": i, + "success": false, + "error": "Missing object_id", + }) + continue + } + + updates, ok := opMap["updates"].(map[string]any) + if !ok { + results = append(results, map[string]any{ + "index": i, + "success": false, + "error": "Missing updates", + }) + continue + } + + // Convert updates to form data + formData := map[string]string{} + for key, value := range updates { + switch v := value.(type) { + case string: + formData[key] = v + case float64: + formData[key] = fmt.Sprintf("%.0f", v) + case bool: + if v { + formData[key] = "true" + } else { + formData[key] = "false" + } + case map[string]any: + jsonValue, err := json.Marshal(v) + if err == nil { + formData[key] = string(jsonValue) + } + } + } + + // Execute the update + resp, err := client.MakeFormRequest(objectId, formData) + if err != nil { + results = append(results, map[string]any{ + "index": i, + "object_id": objectId, + "object_type": objectType, + "success": false, + "error": err.Error(), + }) + } else { + results = append(results, map[string]any{ + "index": i, + "object_id": objectId, + "object_type": objectType, + "success": true, + "response": string(resp.Body), + }) + } + } + + // Return summary of results + summary := map[string]any{ + "total_operations": len(operations), + "successful": 0, + "failed": 0, + "results": results, + } + + for _, result := range results { + if success, ok := result["success"].(bool); ok && success { + summary["successful"] = summary["successful"].(int) + 1 + } else { + summary["failed"] = summary["failed"].(int) + 1 + } + } + + jsonResult, err := json.MarshalIndent(summary, "", " ") + if err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to format results: %s", err)), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(jsonResult)), + }}, + }, nil +} diff --git a/servlets/meta-ads/meta_api.go b/servlets/meta-ads/meta_api.go new file mode 100644 index 0000000..12da159 --- /dev/null +++ b/servlets/meta-ads/meta_api.go @@ -0,0 +1,142 @@ +package main + +import ( + "fmt" + "strings" + + "github.com/extism/go-pdk" +) + +// MetaClient represents a client for Meta Ads APIs +type MetaClient struct { + token string + baseURL string +} + +// NewMetaClient creates a new Meta Ads API client +func NewMetaClient(token string) *MetaClient { + return &MetaClient{ + token: token, + baseURL: "https://graph.facebook.com/v23.0", + } +} + +// Response represents an API response +type Response struct { + StatusCode int + Body []byte +} + +// MakeRequest sends an HTTP request to the Meta API +func (c *MetaClient) MakeRequest(method pdk.HTTPMethod, path string, query map[string][]string, body []byte) (Response, error) { + return c.MakeRequestWithHeaders(method, path, query, body, nil) +} + +// MakeRequestWithHeaders sends an HTTP request with custom headers to the Meta API +func (c *MetaClient) MakeRequestWithHeaders(method pdk.HTTPMethod, path string, query map[string][]string, body []byte, headers map[string]string) (Response, error) { + // Construct the URL + url := fmt.Sprintf("%s/%s", c.baseURL, path) + + // Add access token to query parameters + if query == nil { + query = make(map[string][]string) + } + query["access_token"] = []string{c.token} + + if len(query) > 0 { + parts := []string{} + for key, values := range query { + for _, value := range values { + parts = append(parts, fmt.Sprintf("%s=%s", key, value)) + } + } + url = fmt.Sprintf("%s?%s", url, strings.Join(parts, "&")) + } + + req := pdk.NewHTTPRequest(method, url) + req.SetHeader("Accept", "application/json") + req.SetHeader("Content-Type", "application/json") + + // Add custom headers if provided + if headers != nil { + for key, value := range headers { + req.SetHeader(key, value) + } + } + + if body != nil { + req.SetBody(body) + } + + // Send the request + pdk.Log(pdk.LogDebug, "Sending request...") + resp := req.Send() + statusCode := resp.Status() + responseBody := resp.Body() + + pdk.Log(pdk.LogDebug, fmt.Sprintf("Received response with status: %d", statusCode)) + + // Handle error status codes + if statusCode >= 400 { + errorMsg := fmt.Sprintf("Request failed with status %d: %s", statusCode, string(responseBody)) + pdk.Log(pdk.LogError, errorMsg) + return Response{ + StatusCode: int(statusCode), + Body: resp.Body(), + }, fmt.Errorf(errorMsg) + } + + // Return successful response + return Response{ + StatusCode: int(statusCode), + Body: resp.Body(), + }, nil +} + +// MakeFormRequest sends a form-encoded POST request to the Meta API +func (c *MetaClient) MakeFormRequest(path string, formData map[string]string) (Response, error) { + // Construct the URL + url := fmt.Sprintf("%s/%s", c.baseURL, path) + + // Add access token to form data + if formData == nil { + formData = make(map[string]string) + } + formData["access_token"] = c.token + + // Convert form data to URL-encoded string + parts := []string{} + for key, value := range formData { + parts = append(parts, fmt.Sprintf("%s=%s", key, value)) + } + formBody := strings.Join(parts, "&") + + req := pdk.NewHTTPRequest(pdk.MethodPost, url) + req.SetHeader("Accept", "application/json") + req.SetHeader("Content-Type", "application/x-www-form-urlencoded") + req.SetBody([]byte(formBody)) + + // Send the request + pdk.Log(pdk.LogDebug, "Sending form request...") + resp := req.Send() + statusCode := resp.Status() + responseBody := resp.Body() + + pdk.Log(pdk.LogDebug, fmt.Sprintf("Received response with status: %d", statusCode)) + + // Handle error status codes + if statusCode >= 400 { + errorMsg := fmt.Sprintf("Request failed with status %d: %s", statusCode, string(responseBody)) + pdk.Log(pdk.LogError, errorMsg) + return Response{ + StatusCode: int(statusCode), + Body: resp.Body(), + }, fmt.Errorf(errorMsg) + } + + // Return successful response + return Response{ + StatusCode: int(statusCode), + Body: resp.Body(), + }, nil +} diff --git a/servlets/meta-ads/pdk.gen.go b/servlets/meta-ads/pdk.gen.go new file mode 100755 index 0000000..6c04bdc --- /dev/null +++ b/servlets/meta-ads/pdk.gen.go @@ -0,0 +1,218 @@ +// THIS FILE WAS GENERATED BY `xtp-go-bindgen`. DO NOT EDIT. +package main + +import ( + "errors" + + pdk "github.com/extism/go-pdk" +) + +//export call +func _Call() int32 { + var err error + _ = err + pdk.Log(pdk.LogDebug, "Call: getting JSON input") + var input CallToolRequest + err = pdk.InputJSON(&input) + if err != nil { + pdk.SetError(err) + return -1 + } + + pdk.Log(pdk.LogDebug, "Call: calling implementation function") + output, err := Call(input) + if err != nil { + pdk.SetError(err) + return -1 + } + + pdk.Log(pdk.LogDebug, "Call: setting JSON output") + err = pdk.OutputJSON(output) + if err != nil { + pdk.SetError(err) + return -1 + } + + pdk.Log(pdk.LogDebug, "Call: returning") + return 0 +} + +//export describe +func _Describe() int32 { + var err error + _ = err + output, err := Describe() + if err != nil { + pdk.SetError(err) + return -1 + } + + pdk.Log(pdk.LogDebug, "Describe: setting JSON output") + err = pdk.OutputJSON(output) + if err != nil { + pdk.SetError(err) + return -1 + } + + pdk.Log(pdk.LogDebug, "Describe: returning") + return 0 +} + +// +type BlobResourceContents struct { + // A base64-encoded string representing the binary data of the item. + Blob string `json:"blob"` + // The MIME type of this resource, if known. + MimeType *string `json:"mimeType,omitempty"` + // The URI of this resource. + Uri string `json:"uri"` +} + +// Used by the client to invoke a tool provided by the server. +type CallToolRequest struct { + Method *string `json:"method,omitempty"` + Params Params `json:"params"` +} + +// The server's response to a tool call. +// +// Any errors that originate from the tool SHOULD be reported inside the result +// object, with `isError` set to true, _not_ as an MCP protocol-level error +// response. Otherwise, the LLM would not be able to see that an error occurred +// and self-correct. +// +// However, any errors in _finding_ the tool, an error indicating that the +// server does not support tool calls, or any other exceptional conditions, +// should be reported as an MCP error response. +type CallToolResult struct { + Content []Content `json:"content"` + // Whether the tool call ended in an error. + // + // If not set, this is assumed to be false (the call was successful). + IsError *bool `json:"isError,omitempty"` +} + +// A content response. +// For text content set type to ContentType.Text and set the `text` property +// For image content set type to ContentType.Image and set the `data` and `mimeType` properties +type Content struct { + Annotations *TextAnnotation `json:"annotations,omitempty"` + // The base64-encoded image data. + Data *string `json:"data,omitempty"` + // The MIME type of the image. Different providers may support different image types. + MimeType *string `json:"mimeType,omitempty"` + // The text content of the message. + Text *string `json:"text,omitempty"` + Type ContentType `json:"type"` +} + +// +type ContentType string + +const ( + ContentTypeText ContentType = "text" + ContentTypeImage ContentType = "image" + ContentTypeResource ContentType = "resource" +) + +func (v ContentType) String() string { + switch v { + case ContentTypeText: + return `text` + case ContentTypeImage: + return `image` + case ContentTypeResource: + return `resource` + default: + return "" + } +} + +func stringToContentType(s string) (ContentType, error) { + switch s { + case `text`: + return ContentTypeText, nil + case `image`: + return ContentTypeImage, nil + case `resource`: + return ContentTypeResource, nil + default: + return ContentType(""), errors.New("unable to convert string to ContentType") + } +} + +// Provides one or more descriptions of the tools available in this servlet. +type ListToolsResult struct { + // The list of ToolDescription objects provided by this servlet. + Tools []ToolDescription `json:"tools"` +} + +// +type Params struct { + Arguments interface{} `json:"arguments,omitempty"` + Name string `json:"name"` +} + +// The sender or recipient of messages and data in a conversation. +type Role string + +const ( + RoleAssistant Role = "assistant" + RoleUser Role = "user" +) + +func (v Role) String() string { + switch v { + case RoleAssistant: + return `assistant` + case RoleUser: + return `user` + default: + return "" + } +} + +func stringToRole(s string) (Role, error) { + switch s { + case `assistant`: + return RoleAssistant, nil + case `user`: + return RoleUser, nil + default: + return Role(""), errors.New("unable to convert string to Role") + } +} + +// A text annotation +type TextAnnotation struct { + // Describes who the intended customer of this object or data is. + // + // It can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`). + Audience []Role `json:"audience,omitempty"` + // Describes how important this data is for operating the server. + // + // A value of 1 means "most important," and indicates that the data is + // effectively required, while 0 means "least important," and indicates that + // the data is entirely optional. + Priority float32 `json:"priority,omitempty"` +} + +// +type TextResourceContents struct { + // The MIME type of this resource, if known. + MimeType *string `json:"mimeType,omitempty"` + // The text of the item. This must only be set if the item can actually be represented as text (not binary data). + Text string `json:"text"` + // The URI of this resource. + Uri string `json:"uri"` +} + +// Describes the capabilities and expected paramters of the tool function +type ToolDescription struct { + // A description of the tool + Description string `json:"description"` + // The JSON schema describing the argument input + InputSchema interface{} `json:"inputSchema"` + // The name of the tool. It should match the plugin / binding name. + Name string `json:"name"` +} diff --git a/servlets/meta-ads/prepare.sh b/servlets/meta-ads/prepare.sh new file mode 100644 index 0000000..9fbb93c --- /dev/null +++ b/servlets/meta-ads/prepare.sh @@ -0,0 +1,92 @@ +#!/bin/bash +set -eou pipefail + +# Function to check if a command exists +command_exists () { + command -v "$1" >/dev/null 2>&1 +} + +# Function to compare version numbers for "less than" +version_lt() { + test "$(echo "$@" | tr " " "\n" | sort -V | head -n 1)" = "$1" && test "$1" != "$2" +} + +missing_deps=0 + +# Check for Go +if ! (command_exists go); then + missing_deps=1 + echo "❌ Go (supported version between 1.20 - 1.24) is not installed." + echo "" + echo "To install Go, visit the official download page:" + echo "👉 https://go.dev/dl/" + echo "" + echo "Or install it using a package manager:" + echo "" + echo "🔹 macOS (Homebrew):" + echo " brew install go" + echo "" + echo "🔹 Ubuntu/Debian:" + echo " sudo apt-get -y install golang-go" + echo "" + echo "🔹 Arch Linux:" + echo " sudo pacman -S go" + echo "" + echo "🔹 Windows:" + echo " scoop install go" + echo "" +fi + +# Check for the right version of Go, needed by TinyGo (supports go 1.20 - 1.24) +if (command_exists go); then + compat=0 + for v in `seq 20 24`; do + if (go version | grep -q "go1.$v"); then + compat=1 + fi + done + + if [ $compat -eq 0 ]; then + echo "❌ Supported Go version is not installed. Must be Go 1.20 - 1.24." + echo "" + fi +fi + +ARCH=$(arch) + +# Check for TinyGo and its version +if ! (command_exists tinygo); then + missing_deps=1 + echo "❌ TinyGo is not installed." + echo "" + echo "To install TinyGo, visit the official download page:" + echo "👉 https://tinygo.org/getting-started/install/" + echo "" + echo "Or install it using a package manager:" + echo "" + echo "🔹 macOS (Homebrew):" + echo " brew tap tinygo-org/tools" + echo " brew install tinygo" + echo "" + echo "🔹 Ubuntu/Debian:" + echo " wget https://github.com/tinygo-org/tinygo/releases/download/v0.34.0/tinygo_0.34.0_$ARCH.deb" + echo " sudo dpkg -i tinygo_0.34.0_$ARCH.deb" + echo "" + echo "🔹 Arch Linux:" + echo " pacman -S extra/tinygo" + echo "" + echo "🔹 Windows:" + echo " scoop install tinygo" + echo "" +else + # Check TinyGo version + tinygo_version=$(tinygo version | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' | head -n1) + if version_lt "$tinygo_version" "0.34.0"; then + missing_deps=1 + echo "❌ TinyGo version must be >= 0.34.0 (current version: $tinygo_version)" + echo "Please update TinyGo to a newer version." + echo "" + fi +fi + +go install golang.org/x/tools/cmd/goimports@latest diff --git a/servlets/meta-ads/xtp.toml b/servlets/meta-ads/xtp.toml new file mode 100755 index 0000000..9602b12 --- /dev/null +++ b/servlets/meta-ads/xtp.toml @@ -0,0 +1,17 @@ +app_id = "app_01je4dgpcyfvgrz8f1ys3pbxas" + +# This is where 'xtp plugin push' expects to find the wasm file after the build script has run. +bin = "dist/plugin.wasm" +extension_point_id = "ext_01je4jj1tteaktf0zd0anm8854" +name = "meta-ads" + +[scripts] + + # xtp plugin build runs this script to generate the wasm file + build = "mkdir -p dist && tinygo build -buildmode c-shared -target wasip1 -o dist/plugin.wasm ." + + # xtp plugin init runs this script to format the plugin code + format = "go fmt && go mod tidy && goimports -w main.go" + + # xtp plugin init runs this script before running the format script + prepare = "bash prepare.sh && go get ./..."