diff --git a/tiktok-ads/.gitignore b/tiktok-ads/.gitignore new file mode 100644 index 00000000..573ecc5f --- /dev/null +++ b/tiktok-ads/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +.env +.env.local + diff --git a/tiktok-ads/README.md b/tiktok-ads/README.md new file mode 100644 index 00000000..4277e26e --- /dev/null +++ b/tiktok-ads/README.md @@ -0,0 +1,278 @@ +# TikTok Ads MCP + +MCP Server for TikTok Marketing API integration. Manage campaigns, ad groups, ads and analyze performance using the TikTok Business API. + +## Features + +### Campaign Management +- **list_campaigns** - List all campaigns with filters +- **get_campaign** - Get details of a specific campaign +- **create_campaign** - Create a new campaign +- **update_campaign** - Update existing campaign + +### Ad Group Management +- **list_adgroups** - List ad groups with filters +- **get_adgroup** - Get details of a specific ad group +- **create_adgroup** - Create a new ad group +- **update_adgroup** - Update existing ad group + +### Ad Management +- **list_ads** - List ads with filters +- **get_ad** - Get details of a specific ad +- **create_ad** - Create a new ad +- **update_ad** - Update existing ad + +### Reports & Analytics +- **get_report** - Get custom performance reports +- **get_campaign_report** - Get campaign performance metrics +- **get_adgroup_report** - Get ad group performance metrics +- **get_ad_report** - Get ad performance metrics +- **get_advertiser_info** - Get advertiser account information + +## Setup + +### 1. Create App in TikTok for Business + +1. Go to [TikTok for Business Developer Portal](https://business-api.tiktok.com/portal/apps/) +2. Create a new app or use an existing one +3. Request the following permissions: + - `ad.operation.read` - Read campaigns, ad groups, and ads + - `ad.operation.write` - Create and modify campaigns + - `report.read` - Access performance reports + +### 2. Get Access Token + +1. In the TikTok Developer Portal, go to your app +2. Navigate to "Tools" > "Access Token" +3. Generate a long-lived access token +4. Copy the token + +> **Nota:** O token direto funciona sem precisar de aprovação do app pelo TikTok. É ideal para desenvolvimento e produção inicial. + +### 3. Configure na Instalação do MCP + +Ao instalar o MCP, você será solicitado a preencher: + +- **Access Token**: O token gerado no passo anterior + +## Development + +```bash +# Install dependencies (from monorepo root) +bun install + +# Run in development (hot reload) +bun run dev + +# Type check +bun run check + +# Build for production +bun run build +``` + +## Usage Examples + +### List all campaigns + +```json +{ + "tool": "list_campaigns", + "input": { + "advertiser_id": "123456789" + } +} +``` + +### Create a new campaign + +```json +{ + "tool": "create_campaign", + "input": { + "advertiser_id": "123456789", + "campaign_name": "Summer Sale 2024", + "objective_type": "WEB_CONVERSIONS", + "budget_mode": "BUDGET_MODE_DAY", + "budget": 100 + } +} +``` + +### Create an ad group + +```json +{ + "tool": "create_adgroup", + "input": { + "advertiser_id": "123456789", + "campaign_id": "987654321", + "adgroup_name": "US Adults 25-45", + "optimization_goal": "CONVERT", + "placements": ["PLACEMENT_TIKTOK"], + "budget_mode": "BUDGET_MODE_DAY", + "budget": 50, + "location_ids": ["6252001"], + "gender": "GENDER_UNLIMITED", + "age_groups": ["AGE_25_34", "AGE_35_44"] + } +} +``` + +### Create an ad + +```json +{ + "tool": "create_ad", + "input": { + "advertiser_id": "123456789", + "adgroup_id": "111222333", + "ad_name": "Summer Sale Video", + "ad_format": "SINGLE_VIDEO", + "ad_text": "Shop our summer collection! 🌴", + "call_to_action": "Shop Now", + "landing_page_url": "https://example.com/summer-sale", + "video_id": "v123456789" + } +} +``` + +### Get campaign performance report + +```json +{ + "tool": "get_campaign_report", + "input": { + "advertiser_id": "123456789", + "start_date": "2024-01-01", + "end_date": "2024-01-31" + } +} +``` + +### Get custom report with specific metrics + +```json +{ + "tool": "get_report", + "input": { + "advertiser_id": "123456789", + "data_level": "AUCTION_AD", + "start_date": "2024-01-01", + "end_date": "2024-01-07", + "dimensions": ["ad_id", "stat_time_day"], + "metrics": ["spend", "impressions", "clicks", "ctr", "video_play_actions", "likes", "shares"] + } +} +``` + +### Update campaign status + +```json +{ + "tool": "update_campaign", + "input": { + "advertiser_id": "123456789", + "campaign_id": "987654321", + "operation_status": "DISABLE" + } +} +``` + +## Project Structure + +``` +tiktok-ads/ +├── server/ +│ ├── main.ts # Entry point com StateSchema +│ ├── constants.ts # API URLs and constants +│ ├── lib/ +│ │ ├── tiktok-client.ts # API client +│ │ ├── types.ts # TypeScript types +│ │ └── env.ts # Helper para obter access token do state +│ └── tools/ +│ ├── index.ts # Exports all tools +│ ├── campaigns.ts # Campaign tools +│ ├── adgroups.ts # Ad Group tools +│ ├── ads.ts # Ad tools +│ └── reports.ts # Report tools +├── shared/ +│ └── deco.gen.ts # Types e StateSchema +├── app.json # MCP configuration +├── package.json +├── tsconfig.json +└── README.md +``` + +## Campaign Objectives + +| Objective | Description | +|-----------|-------------| +| TRAFFIC | Drive traffic to your website | +| APP_PROMOTION | Promote app installs and engagement | +| WEB_CONVERSIONS | Drive website conversions | +| PRODUCT_SALES | Sell products from a catalog | +| REACH | Maximize reach to your audience | +| VIDEO_VIEWS | Get more video views | +| LEAD_GENERATION | Collect leads from forms | +| COMMUNITY_INTERACTION | Increase profile engagement | + +## Optimization Goals + +| Goal | Description | +|------|-------------| +| CLICK | Optimize for clicks | +| CONVERT | Optimize for conversions | +| SHOW | Optimize for impressions | +| REACH | Optimize for unique reach | +| VIDEO_VIEW | Optimize for video views | +| LEAD_GENERATION | Optimize for lead form submissions | +| ENGAGEMENT | Optimize for profile engagement | + +## Report Metrics + +Common metrics available in reports: + +- **spend** - Total amount spent +- **impressions** - Number of ad impressions +- **clicks** - Number of clicks +- **ctr** - Click-through rate +- **cpc** - Cost per click +- **cpm** - Cost per 1000 impressions +- **reach** - Number of unique users reached +- **frequency** - Average times ad shown per user +- **conversion** - Number of conversions +- **cost_per_conversion** - Cost per conversion +- **video_play_actions** - Video play actions +- **video_watched_2s** - 2-second video views +- **video_watched_6s** - 6-second video views +- **likes** - Number of likes +- **comments** - Number of comments +- **shares** - Number of shares +- **follows** - Number of new followers + +## Authentication Notes + +### Access Token Direto + +Este MCP usa autenticação via Access Token direto, gerado no TikTok Developer Portal. Essa abordagem: + +- ✅ **Funciona imediatamente** - Não precisa de aprovação do app pelo TikTok +- ✅ **Token de longa duração** - Expira em meses, não horas +- ✅ **Simples de configurar** - Basta gerar o token e colar na instalação + +Para gerar o token: +1. Acesse [TikTok Developer Portal](https://business-api.tiktok.com/portal/apps/) +2. Vá em "Tools" > "Access Token" +3. Gere e copie o token + +## API Reference + +This MCP uses the TikTok Marketing API v1.3: +- Base URL: `https://business-api.tiktok.com/open_api/v1.3/` +- [Official Documentation](https://business-api.tiktok.com/marketing_api/docs) + +## License + +MIT + diff --git a/tiktok-ads/app.json b/tiktok-ads/app.json new file mode 100644 index 00000000..99f796b8 --- /dev/null +++ b/tiktok-ads/app.json @@ -0,0 +1,20 @@ +{ + "scopeName": "deco", + "name": "tiktok-ads", + "friendlyName": "TikTok Ads", + "connection": { + "type": "HTTP", + "url": "https://sites-tiktok-ads.decocache.com/mcp" + }, + "description": "Integrate and manage your TikTok Ads campaigns. Create, edit and analyze campaigns, ad groups, ads and performance reports.", + "icon": "https://assets.decocache.com/mcp/tiktok-ads-icon.svg", + "unlisted": false, + "metadata": { + "categories": ["Marketing", "Advertising"], + "official": false, + "tags": ["tiktok", "ads", "marketing", "campaigns", "advertising", "social-media", "analytics"], + "short_description": "Integrate and manage your TikTok Ads campaigns. Create, edit and analyze campaigns, ad groups, ads and performance reports.", + "mesh_description": "The TikTok Ads MCP provides comprehensive integration with TikTok Marketing API, enabling full programmatic control over advertising campaigns. This MCP allows AI agents to create, read, update campaigns, ad groups, and individual ads, analyze performance metrics, manage audiences, and generate detailed reports. It supports advanced advertising features including campaign budget optimization, targeting configuration, bid strategies, and performance tracking across multiple advertisers. The integration is perfect for building intelligent advertising assistants, automated campaign managers, and performance optimization tools. Ideal for marketers and agencies who need to automate TikTok advertising workflows, integrate campaign management into business processes, or build advertising-aware applications. Provides secure authentication for API access." + } +} + diff --git a/tiktok-ads/package.json b/tiktok-ads/package.json new file mode 100644 index 00000000..fd29bdc2 --- /dev/null +++ b/tiktok-ads/package.json @@ -0,0 +1,28 @@ +{ + "name": "tiktok-ads", + "version": "1.0.0", + "description": "TikTok Ads MCP Server - Manage campaigns, ad groups, ads and reports", + "private": true, + "type": "module", + "scripts": { + "dev": "bun run --hot server/main.ts", + "build:server": "NODE_ENV=production bun build server/main.ts --target=bun --outfile=dist/server/main.js", + "build": "bun run build:server", + "publish": "cat app.json | deco registry publish -w /shared/deco -y", + "check": "tsc --noEmit" + }, + "dependencies": { + "@decocms/runtime": "^1.1.3", + "zod": "^4.0.0" + }, + "devDependencies": { + "@decocms/mcps-shared": "workspace:*", + "@modelcontextprotocol/sdk": "1.25.1", + "deco-cli": "^0.28.0", + "typescript": "^5.7.2" + }, + "engines": { + "node": ">=22.0.0" + } +} + diff --git a/tiktok-ads/server/constants.ts b/tiktok-ads/server/constants.ts new file mode 100644 index 00000000..57c2606e --- /dev/null +++ b/tiktok-ads/server/constants.ts @@ -0,0 +1,140 @@ +/** + * TikTok Marketing API constants and configuration + */ + +export const TIKTOK_API_BASE = "https://business-api.tiktok.com/open_api/v1.3"; + +// API Endpoints +export const ENDPOINTS = { + // Campaign endpoints + CAMPAIGN_GET: `${TIKTOK_API_BASE}/campaign/get/`, + CAMPAIGN_CREATE: `${TIKTOK_API_BASE}/campaign/create/`, + CAMPAIGN_UPDATE: `${TIKTOK_API_BASE}/campaign/update/`, + + // Ad Group endpoints + ADGROUP_GET: `${TIKTOK_API_BASE}/adgroup/get/`, + ADGROUP_CREATE: `${TIKTOK_API_BASE}/adgroup/create/`, + ADGROUP_UPDATE: `${TIKTOK_API_BASE}/adgroup/update/`, + + // Ad endpoints + AD_GET: `${TIKTOK_API_BASE}/ad/get/`, + AD_CREATE: `${TIKTOK_API_BASE}/ad/create/`, + AD_UPDATE: `${TIKTOK_API_BASE}/ad/update/`, + + // Report endpoints + REPORT_INTEGRATED: `${TIKTOK_API_BASE}/report/integrated/get/`, + + // Advertiser endpoints + ADVERTISER_INFO: `${TIKTOK_API_BASE}/advertiser/info/`, +}; + +// Default pagination +export const DEFAULT_PAGE_SIZE = 50; +export const MAX_PAGE_SIZE = 1000; + +// Campaign objectives +export const CAMPAIGN_OBJECTIVES = { + TRAFFIC: "TRAFFIC", + APP_PROMOTION: "APP_PROMOTION", + WEB_CONVERSIONS: "WEB_CONVERSIONS", + PRODUCT_SALES: "PRODUCT_SALES", + REACH: "REACH", + VIDEO_VIEWS: "VIDEO_VIEWS", + LEAD_GENERATION: "LEAD_GENERATION", + COMMUNITY_INTERACTION: "COMMUNITY_INTERACTION", +} as const; + +// Campaign status +export const CAMPAIGN_STATUS = { + ENABLE: "ENABLE", + DISABLE: "DISABLE", + DELETE: "DELETE", +} as const; + +// Ad Group status +export const ADGROUP_STATUS = { + ENABLE: "ENABLE", + DISABLE: "DISABLE", + DELETE: "DELETE", +} as const; + +// Ad status +export const AD_STATUS = { + ENABLE: "ENABLE", + DISABLE: "DISABLE", + DELETE: "DELETE", +} as const; + +// Budget modes +export const BUDGET_MODE = { + BUDGET_MODE_INFINITE: "BUDGET_MODE_INFINITE", + BUDGET_MODE_DAY: "BUDGET_MODE_DAY", + BUDGET_MODE_TOTAL: "BUDGET_MODE_TOTAL", +} as const; + +// Bid types +export const BID_TYPE = { + BID_TYPE_NO_BID: "BID_TYPE_NO_BID", + BID_TYPE_CUSTOM: "BID_TYPE_CUSTOM", +} as const; + +// Optimization goals +export const OPTIMIZATION_GOAL = { + CLICK: "CLICK", + CONVERT: "CONVERT", + SHOW: "SHOW", + REACH: "REACH", + VIDEO_VIEW: "VIDEO_VIEW", + LEAD_GENERATION: "LEAD_GENERATION", + ENGAGEMENT: "ENGAGEMENT", +} as const; + +// Placement types +export const PLACEMENTS = { + PLACEMENT_TIKTOK: "PLACEMENT_TIKTOK", + PLACEMENT_PANGLE: "PLACEMENT_PANGLE", + PLACEMENT_GLOBAL_APP_BUNDLE: "PLACEMENT_GLOBAL_APP_BUNDLE", +} as const; + +// Report data levels +export const REPORT_DATA_LEVEL = { + AUCTION_ADVERTISER: "AUCTION_ADVERTISER", + AUCTION_CAMPAIGN: "AUCTION_CAMPAIGN", + AUCTION_ADGROUP: "AUCTION_ADGROUP", + AUCTION_AD: "AUCTION_AD", +} as const; + +// Report dimensions +export const REPORT_DIMENSIONS = { + ADVERTISER_ID: "advertiser_id", + CAMPAIGN_ID: "campaign_id", + ADGROUP_ID: "adgroup_id", + AD_ID: "ad_id", + STAT_TIME_DAY: "stat_time_day", + STAT_TIME_HOUR: "stat_time_hour", +} as const; + +// Common metrics for reports +export const REPORT_METRICS = [ + "spend", + "impressions", + "clicks", + "ctr", + "cpc", + "cpm", + "reach", + "frequency", + "conversion", + "cost_per_conversion", + "conversion_rate", + "video_play_actions", + "video_watched_2s", + "video_watched_6s", + "average_video_play", + "average_video_play_per_user", + "profile_visits", + "likes", + "comments", + "shares", + "follows", +] as const; diff --git a/tiktok-ads/server/lib/env.ts b/tiktok-ads/server/lib/env.ts new file mode 100644 index 00000000..498cdb31 --- /dev/null +++ b/tiktok-ads/server/lib/env.ts @@ -0,0 +1,19 @@ +import type { Env } from "../../shared/deco.gen.ts"; + +/** + * Get TikTok access token from the state configured during MCP installation + * @param env - The environment containing the deco request context with state + * @returns The access token + * @throws Error if not configured + */ +export const getTikTokAccessToken = (env: Env): string => { + const accessToken = env.DECO_REQUEST_CONTEXT?.state?.accessToken; + + if (!accessToken) { + throw new Error( + "TikTok Access Token não configurado. Configure o token na instalação do MCP.", + ); + } + + return accessToken; +}; diff --git a/tiktok-ads/server/lib/tiktok-client.ts b/tiktok-ads/server/lib/tiktok-client.ts new file mode 100644 index 00000000..54a0e91f --- /dev/null +++ b/tiktok-ads/server/lib/tiktok-client.ts @@ -0,0 +1,516 @@ +/** + * TikTok Marketing API client + * Handles all communication with the TikTok Marketing API + */ + +import { ENDPOINTS, DEFAULT_PAGE_SIZE } from "../constants.ts"; +import type { + Campaign, + CampaignListResponse, + CreateCampaignInput, + UpdateCampaignInput, + ListCampaignsInput, + AdGroup, + AdGroupListResponse, + CreateAdGroupInput, + UpdateAdGroupInput, + ListAdGroupsInput, + Ad, + AdListResponse, + CreateAdInput, + UpdateAdInput, + ListAdsInput, + ReportResponse, + GetReportInput, + Advertiser, + AdvertiserInfoResponse, + GetAdvertiserInfoInput, + ApiResponse, +} from "./types.ts"; + +export interface TikTokClientConfig { + accessToken: string; +} + +export class TikTokClient { + private accessToken: string; + + constructor(config: TikTokClientConfig) { + this.accessToken = config.accessToken; + } + + private async request(url: string, options: RequestInit = {}): Promise { + const response = await fetch(url, { + ...options, + headers: { + "Access-Token": this.accessToken, + "Content-Type": "application/json", + ...options.headers, + }, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error( + `TikTok Marketing API error: ${response.status} - ${error}`, + ); + } + + const result = (await response.json()) as ApiResponse; + + if (result.code !== 0) { + throw new Error(`TikTok API error: ${result.code} - ${result.message}`); + } + + return result.data; + } + + // ==================== Campaign Methods ==================== + + /** + * List campaigns for an advertiser + */ + async listCampaigns(input: ListCampaignsInput): Promise<{ + campaigns: Campaign[]; + page_info: { + page: number; + page_size: number; + total_number: number; + total_page: number; + }; + }> { + const body: any = { + advertiser_id: input.advertiser_id, + page: input.page || 1, + page_size: input.page_size || DEFAULT_PAGE_SIZE, + }; + + if (input.campaign_ids?.length) { + body.filtering = { ...body.filtering, campaign_ids: input.campaign_ids }; + } + if (input.filtering) { + body.filtering = { ...body.filtering, ...input.filtering }; + } + + const response = await fetch(ENDPOINTS.CAMPAIGN_GET, { + method: "POST", + headers: { + "Access-Token": this.accessToken, + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`TikTok API error: ${response.status} - ${error}`); + } + + const result = (await response.json()) as CampaignListResponse; + + if (result.code !== 0) { + throw new Error(`TikTok API error: ${result.code} - ${result.message}`); + } + + return { + campaigns: result.data.list, + page_info: result.data.page_info, + }; + } + + /** + * Create a new campaign + */ + async createCampaign( + input: CreateCampaignInput, + ): Promise<{ campaign_id: string }> { + const response = await fetch(ENDPOINTS.CAMPAIGN_CREATE, { + method: "POST", + headers: { + "Access-Token": this.accessToken, + "Content-Type": "application/json", + }, + body: JSON.stringify(input), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`TikTok API error: ${response.status} - ${error}`); + } + + const result = (await response.json()) as ApiResponse<{ + campaign_id: string; + }>; + + if (result.code !== 0) { + throw new Error(`TikTok API error: ${result.code} - ${result.message}`); + } + + return result.data; + } + + /** + * Update an existing campaign + */ + async updateCampaign( + input: UpdateCampaignInput, + ): Promise<{ campaign_id: string }> { + const response = await fetch(ENDPOINTS.CAMPAIGN_UPDATE, { + method: "POST", + headers: { + "Access-Token": this.accessToken, + "Content-Type": "application/json", + }, + body: JSON.stringify(input), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`TikTok API error: ${response.status} - ${error}`); + } + + const result = (await response.json()) as ApiResponse<{ + campaign_id: string; + }>; + + if (result.code !== 0) { + throw new Error(`TikTok API error: ${result.code} - ${result.message}`); + } + + return result.data; + } + + // ==================== Ad Group Methods ==================== + + /** + * List ad groups for an advertiser + */ + async listAdGroups(input: ListAdGroupsInput): Promise<{ + adgroups: AdGroup[]; + page_info: { + page: number; + page_size: number; + total_number: number; + total_page: number; + }; + }> { + const body: any = { + advertiser_id: input.advertiser_id, + page: input.page || 1, + page_size: input.page_size || DEFAULT_PAGE_SIZE, + }; + + if (input.campaign_ids?.length) { + body.filtering = { ...body.filtering, campaign_ids: input.campaign_ids }; + } + if (input.adgroup_ids?.length) { + body.filtering = { ...body.filtering, adgroup_ids: input.adgroup_ids }; + } + if (input.filtering) { + body.filtering = { ...body.filtering, ...input.filtering }; + } + + const response = await fetch(ENDPOINTS.ADGROUP_GET, { + method: "POST", + headers: { + "Access-Token": this.accessToken, + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`TikTok API error: ${response.status} - ${error}`); + } + + const result = (await response.json()) as AdGroupListResponse; + + if (result.code !== 0) { + throw new Error(`TikTok API error: ${result.code} - ${result.message}`); + } + + return { + adgroups: result.data.list, + page_info: result.data.page_info, + }; + } + + /** + * Create a new ad group + */ + async createAdGroup( + input: CreateAdGroupInput, + ): Promise<{ adgroup_id: string }> { + const response = await fetch(ENDPOINTS.ADGROUP_CREATE, { + method: "POST", + headers: { + "Access-Token": this.accessToken, + "Content-Type": "application/json", + }, + body: JSON.stringify(input), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`TikTok API error: ${response.status} - ${error}`); + } + + const result = (await response.json()) as ApiResponse<{ + adgroup_id: string; + }>; + + if (result.code !== 0) { + throw new Error(`TikTok API error: ${result.code} - ${result.message}`); + } + + return result.data; + } + + /** + * Update an existing ad group + */ + async updateAdGroup( + input: UpdateAdGroupInput, + ): Promise<{ adgroup_id: string }> { + const response = await fetch(ENDPOINTS.ADGROUP_UPDATE, { + method: "POST", + headers: { + "Access-Token": this.accessToken, + "Content-Type": "application/json", + }, + body: JSON.stringify(input), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`TikTok API error: ${response.status} - ${error}`); + } + + const result = (await response.json()) as ApiResponse<{ + adgroup_id: string; + }>; + + if (result.code !== 0) { + throw new Error(`TikTok API error: ${result.code} - ${result.message}`); + } + + return result.data; + } + + // ==================== Ad Methods ==================== + + /** + * List ads for an advertiser + */ + async listAds(input: ListAdsInput): Promise<{ + ads: Ad[]; + page_info: { + page: number; + page_size: number; + total_number: number; + total_page: number; + }; + }> { + const body: any = { + advertiser_id: input.advertiser_id, + page: input.page || 1, + page_size: input.page_size || DEFAULT_PAGE_SIZE, + }; + + if (input.campaign_ids?.length) { + body.filtering = { ...body.filtering, campaign_ids: input.campaign_ids }; + } + if (input.adgroup_ids?.length) { + body.filtering = { ...body.filtering, adgroup_ids: input.adgroup_ids }; + } + if (input.ad_ids?.length) { + body.filtering = { ...body.filtering, ad_ids: input.ad_ids }; + } + if (input.filtering) { + body.filtering = { ...body.filtering, ...input.filtering }; + } + + const response = await fetch(ENDPOINTS.AD_GET, { + method: "POST", + headers: { + "Access-Token": this.accessToken, + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`TikTok API error: ${response.status} - ${error}`); + } + + const result = (await response.json()) as AdListResponse; + + if (result.code !== 0) { + throw new Error(`TikTok API error: ${result.code} - ${result.message}`); + } + + return { + ads: result.data.list, + page_info: result.data.page_info, + }; + } + + /** + * Create a new ad + */ + async createAd(input: CreateAdInput): Promise<{ ad_id: string }> { + const response = await fetch(ENDPOINTS.AD_CREATE, { + method: "POST", + headers: { + "Access-Token": this.accessToken, + "Content-Type": "application/json", + }, + body: JSON.stringify(input), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`TikTok API error: ${response.status} - ${error}`); + } + + const result = (await response.json()) as ApiResponse<{ ad_id: string }>; + + if (result.code !== 0) { + throw new Error(`TikTok API error: ${result.code} - ${result.message}`); + } + + return result.data; + } + + /** + * Update an existing ad + */ + async updateAd(input: UpdateAdInput): Promise<{ ad_id: string }> { + const response = await fetch(ENDPOINTS.AD_UPDATE, { + method: "POST", + headers: { + "Access-Token": this.accessToken, + "Content-Type": "application/json", + }, + body: JSON.stringify(input), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`TikTok API error: ${response.status} - ${error}`); + } + + const result = (await response.json()) as ApiResponse<{ ad_id: string }>; + + if (result.code !== 0) { + throw new Error(`TikTok API error: ${result.code} - ${result.message}`); + } + + return result.data; + } + + // ==================== Report Methods ==================== + + /** + * Get integrated report data + */ + async getReport(input: GetReportInput): Promise<{ + rows: Array<{ + dimensions: Record; + metrics: Record; + }>; + page_info: { + page: number; + page_size: number; + total_number: number; + total_page: number; + }; + }> { + const body: any = { + advertiser_id: input.advertiser_id, + data_level: input.data_level, + dimensions: input.dimensions, + metrics: input.metrics, + start_date: input.start_date, + end_date: input.end_date, + page: input.page || 1, + page_size: input.page_size || DEFAULT_PAGE_SIZE, + }; + + if (input.filters) { + body.filtering = input.filters; + } + + const response = await fetch(ENDPOINTS.REPORT_INTEGRATED, { + method: "POST", + headers: { + "Access-Token": this.accessToken, + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`TikTok API error: ${response.status} - ${error}`); + } + + const result = (await response.json()) as ReportResponse; + + if (result.code !== 0) { + throw new Error(`TikTok API error: ${result.code} - ${result.message}`); + } + + return { + rows: result.data.list.map((row) => ({ + dimensions: row.dimensions, + metrics: row as unknown as Record, + })), + page_info: result.data.page_info, + }; + } + + // ==================== Advertiser Methods ==================== + + /** + * Get advertiser information + */ + async getAdvertiserInfo( + input: GetAdvertiserInfoInput, + ): Promise { + // For GET requests, we need to use query params + const url = new URL(ENDPOINTS.ADVERTISER_INFO); + url.searchParams.set( + "advertiser_ids", + JSON.stringify(input.advertiser_ids), + ); + if (input.fields?.length) { + url.searchParams.set("fields", JSON.stringify(input.fields)); + } + + const getResponse = await fetch(url.toString(), { + method: "GET", + headers: { + "Access-Token": this.accessToken, + }, + }); + + if (!getResponse.ok) { + const error = await getResponse.text(); + throw new Error(`TikTok API error: ${getResponse.status} - ${error}`); + } + + const result = (await getResponse.json()) as AdvertiserInfoResponse; + + if (result.code !== 0) { + throw new Error(`TikTok API error: ${result.code} - ${result.message}`); + } + + return result.data.list; + } +} + +// Re-export getTikTokAccessToken from env.ts for convenience +export { getTikTokAccessToken as getAccessToken } from "./env.ts"; diff --git a/tiktok-ads/server/lib/types.ts b/tiktok-ads/server/lib/types.ts new file mode 100644 index 00000000..50895486 --- /dev/null +++ b/tiktok-ads/server/lib/types.ts @@ -0,0 +1,437 @@ +/** + * TikTok Marketing API types + */ + +// ============================================================================ +// Common Types +// ============================================================================ + +export type OperationStatus = "ENABLE" | "DISABLE" | "DELETE"; + +export type BudgetMode = + | "BUDGET_MODE_INFINITE" + | "BUDGET_MODE_DAY" + | "BUDGET_MODE_TOTAL"; + +export type BidType = "BID_TYPE_NO_BID" | "BID_TYPE_CUSTOM"; + +export type OptimizationGoal = + | "CLICK" + | "CONVERT" + | "SHOW" + | "REACH" + | "VIDEO_VIEW" + | "LEAD_GENERATION" + | "ENGAGEMENT"; + +export type Placement = + | "PLACEMENT_TIKTOK" + | "PLACEMENT_PANGLE" + | "PLACEMENT_GLOBAL_APP_BUNDLE"; + +// ============================================================================ +// Campaign Types +// ============================================================================ + +export type CampaignObjective = + | "TRAFFIC" + | "APP_PROMOTION" + | "WEB_CONVERSIONS" + | "PRODUCT_SALES" + | "REACH" + | "VIDEO_VIEWS" + | "LEAD_GENERATION" + | "COMMUNITY_INTERACTION"; + +export type CampaignStatus = + | "CAMPAIGN_STATUS_ENABLE" + | "CAMPAIGN_STATUS_DISABLE" + | "CAMPAIGN_STATUS_DELETE" + | "CAMPAIGN_STATUS_ADVERTISER_AUDIT_DENY" + | "CAMPAIGN_STATUS_ADVERTISER_AUDIT" + | "CAMPAIGN_STATUS_BUDGET_EXCEED" + | "CAMPAIGN_STATUS_ALL"; + +export interface Campaign { + campaign_id: string; + campaign_name: string; + advertiser_id: string; + objective_type: CampaignObjective; + operation_status: OperationStatus; + secondary_status: CampaignStatus; + budget_mode: BudgetMode; + budget?: number; + is_smart_performance_campaign?: boolean; + create_time: string; + modify_time: string; +} + +export interface CreateCampaignInput { + advertiser_id: string; + campaign_name: string; + objective_type: CampaignObjective; + budget_mode?: BudgetMode; + budget?: number; + operation_status?: OperationStatus; +} + +export interface UpdateCampaignInput { + advertiser_id: string; + campaign_id: string; + campaign_name?: string; + budget_mode?: BudgetMode; + budget?: number; + operation_status?: OperationStatus; +} + +export interface ListCampaignsInput { + advertiser_id: string; + campaign_ids?: string[]; + filtering?: { + campaign_name?: string; + objective_type?: CampaignObjective; + secondary_status?: CampaignStatus; + }; + page?: number; + page_size?: number; +} + +export interface CampaignListResponse { + code: number; + message: string; + data: { + list: Campaign[]; + page_info: PageInfo; + }; +} + +// ============================================================================ +// Ad Group Types +// ============================================================================ + +export type AdGroupStatus = + | "ADGROUP_STATUS_DELIVERY_OK" + | "ADGROUP_STATUS_DISABLE" + | "ADGROUP_STATUS_DELETE" + | "ADGROUP_STATUS_NOT_DELIVER" + | "ADGROUP_STATUS_TIME_DONE" + | "ADGROUP_STATUS_NO_SCHEDULE" + | "ADGROUP_STATUS_CAMPAIGN_DISABLE" + | "ADGROUP_STATUS_CAMPAIGN_EXCEED" + | "ADGROUP_STATUS_BALANCE_EXCEED" + | "ADGROUP_STATUS_AUDIT" + | "ADGROUP_STATUS_REAUDIT" + | "ADGROUP_STATUS_AUDIT_DENY" + | "ADGROUP_STATUS_ALL"; + +export type ScheduleType = "SCHEDULE_START_END" | "SCHEDULE_FROM_NOW"; + +export interface AdGroup { + adgroup_id: string; + adgroup_name: string; + advertiser_id: string; + campaign_id: string; + operation_status: OperationStatus; + secondary_status: AdGroupStatus; + placement_type: string; + placements?: Placement[]; + optimization_goal: OptimizationGoal; + bid_type: BidType; + bid_price?: number; + budget_mode: BudgetMode; + budget?: number; + schedule_type: ScheduleType; + schedule_start_time?: string; + schedule_end_time?: string; + dayparting?: string; + pacing?: string; + create_time: string; + modify_time: string; +} + +export interface CreateAdGroupInput { + advertiser_id: string; + campaign_id: string; + adgroup_name: string; + placement_type?: string; + placements?: Placement[]; + optimization_goal: OptimizationGoal; + bid_type?: BidType; + bid_price?: number; + budget_mode?: BudgetMode; + budget?: number; + schedule_type?: ScheduleType; + schedule_start_time?: string; + schedule_end_time?: string; + operation_status?: OperationStatus; + location_ids?: string[]; + gender?: "GENDER_UNLIMITED" | "GENDER_MALE" | "GENDER_FEMALE"; + age_groups?: string[]; + languages?: string[]; +} + +export interface UpdateAdGroupInput { + advertiser_id: string; + adgroup_id: string; + adgroup_name?: string; + bid_price?: number; + budget?: number; + schedule_end_time?: string; + operation_status?: OperationStatus; +} + +export interface ListAdGroupsInput { + advertiser_id: string; + campaign_ids?: string[]; + adgroup_ids?: string[]; + filtering?: { + adgroup_name?: string; + secondary_status?: AdGroupStatus; + }; + page?: number; + page_size?: number; +} + +export interface AdGroupListResponse { + code: number; + message: string; + data: { + list: AdGroup[]; + page_info: PageInfo; + }; +} + +// ============================================================================ +// Ad Types +// ============================================================================ + +export type AdStatus = + | "AD_STATUS_DELIVERY_OK" + | "AD_STATUS_DISABLE" + | "AD_STATUS_DELETE" + | "AD_STATUS_NOT_DELIVER" + | "AD_STATUS_CAMPAIGN_DISABLE" + | "AD_STATUS_ADGROUP_DISABLE" + | "AD_STATUS_CAMPAIGN_EXCEED" + | "AD_STATUS_BALANCE_EXCEED" + | "AD_STATUS_AUDIT" + | "AD_STATUS_REAUDIT" + | "AD_STATUS_AUDIT_DENY" + | "AD_STATUS_ALL"; + +export type AdFormat = + | "SINGLE_VIDEO" + | "SINGLE_IMAGE" + | "VIDEO_CAROUSEL" + | "IMAGE_CAROUSEL" + | "SPARK_ADS"; + +export interface Ad { + ad_id: string; + ad_name: string; + advertiser_id: string; + campaign_id: string; + adgroup_id: string; + operation_status: OperationStatus; + secondary_status: AdStatus; + ad_format: AdFormat; + ad_text?: string; + call_to_action?: string; + call_to_action_id?: string; + landing_page_url?: string; + display_name?: string; + profile_image?: string; + video_id?: string; + image_ids?: string[]; + create_time: string; + modify_time: string; +} + +export interface CreateAdInput { + advertiser_id: string; + adgroup_id: string; + ad_name: string; + ad_format: AdFormat; + ad_text?: string; + call_to_action?: string; + landing_page_url?: string; + display_name?: string; + video_id?: string; + image_ids?: string[]; + operation_status?: OperationStatus; + identity_id?: string; + identity_type?: string; +} + +export interface UpdateAdInput { + advertiser_id: string; + ad_id: string; + ad_name?: string; + ad_text?: string; + call_to_action?: string; + landing_page_url?: string; + operation_status?: OperationStatus; +} + +export interface ListAdsInput { + advertiser_id: string; + campaign_ids?: string[]; + adgroup_ids?: string[]; + ad_ids?: string[]; + filtering?: { + ad_name?: string; + secondary_status?: AdStatus; + }; + page?: number; + page_size?: number; +} + +export interface AdListResponse { + code: number; + message: string; + data: { + list: Ad[]; + page_info: PageInfo; + }; +} + +// ============================================================================ +// Report Types +// ============================================================================ + +export type ReportDataLevel = + | "AUCTION_ADVERTISER" + | "AUCTION_CAMPAIGN" + | "AUCTION_ADGROUP" + | "AUCTION_AD"; + +export type ReportDimension = + | "advertiser_id" + | "campaign_id" + | "adgroup_id" + | "ad_id" + | "stat_time_day" + | "stat_time_hour"; + +export interface ReportMetrics { + spend?: number; + impressions?: number; + clicks?: number; + ctr?: number; + cpc?: number; + cpm?: number; + reach?: number; + frequency?: number; + conversion?: number; + cost_per_conversion?: number; + conversion_rate?: number; + video_play_actions?: number; + video_watched_2s?: number; + video_watched_6s?: number; + average_video_play?: number; + average_video_play_per_user?: number; + profile_visits?: number; + likes?: number; + comments?: number; + shares?: number; + follows?: number; +} + +export interface ReportRow extends ReportMetrics { + dimensions: { + advertiser_id?: string; + campaign_id?: string; + adgroup_id?: string; + ad_id?: string; + stat_time_day?: string; + stat_time_hour?: string; + }; +} + +export interface GetReportInput { + advertiser_id: string; + data_level: ReportDataLevel; + dimensions: ReportDimension[]; + metrics: string[]; + start_date: string; + end_date: string; + filters?: { + campaign_ids?: string[]; + adgroup_ids?: string[]; + ad_ids?: string[]; + }; + page?: number; + page_size?: number; +} + +export interface ReportResponse { + code: number; + message: string; + data: { + list: ReportRow[]; + page_info: PageInfo; + }; +} + +// ============================================================================ +// Advertiser Types +// ============================================================================ + +export type AdvertiserStatus = + | "STATUS_ENABLE" + | "STATUS_DISABLE" + | "STATUS_PENDING_CONFIRM" + | "STATUS_PENDING_VERIFIED" + | "STATUS_CONFIRM_FAIL" + | "STATUS_CONFIRM_FAIL_END" + | "STATUS_LIMIT" + | "STATUS_WAIT_FOR_BPM_AUDIT" + | "STATUS_WAIT_FOR_PUBLIC_AUTH" + | "STATUS_SELF_SERVICE_UNAUDITED"; + +export interface Advertiser { + advertiser_id: string; + advertiser_name: string; + status: AdvertiserStatus; + company?: string; + contacter?: string; + email?: string; + telephone?: string; + address?: string; + industry?: string; + balance?: number; + currency?: string; + timezone?: string; + create_time: string; +} + +export interface GetAdvertiserInfoInput { + advertiser_ids: string[]; + fields?: string[]; +} + +export interface AdvertiserInfoResponse { + code: number; + message: string; + data: { + list: Advertiser[]; + }; +} + +// ============================================================================ +// Common Types +// ============================================================================ + +export interface PageInfo { + page: number; + page_size: number; + total_number: number; + total_page: number; +} + +export interface ApiResponse { + code: number; + message: string; + request_id: string; + data: T; +} diff --git a/tiktok-ads/server/main.ts b/tiktok-ads/server/main.ts new file mode 100644 index 00000000..e557a02b --- /dev/null +++ b/tiktok-ads/server/main.ts @@ -0,0 +1,31 @@ +/** + * TikTok Ads MCP Server + * + * This MCP provides tools for interacting with TikTok Marketing API, + * including campaign management, ad groups, ads, and performance reports. + */ +import { DefaultEnv, withRuntime } from "@decocms/runtime"; +import { serve } from "@decocms/mcps-shared/serve"; + +import { tools } from "./tools/index.ts"; +import { type Env as DecoEnv, StateSchema } from "../shared/deco.gen.ts"; + +/** + * Environment type for TikTok Ads MCP + * Extends process env with Deco runtime context + */ +export type Env = DefaultEnv & DecoEnv; + +const runtime = withRuntime({ + /** + * The state schema defines what users fill when installing the App. + * For TikTok Ads, we need the access token that users generate + * in the TikTok Developer Portal. + */ + oauth: { + state: StateSchema, + }, + tools, +}); + +serve(runtime.fetch); diff --git a/tiktok-ads/server/tools/adgroups.ts b/tiktok-ads/server/tools/adgroups.ts new file mode 100644 index 00000000..0a07ed7d --- /dev/null +++ b/tiktok-ads/server/tools/adgroups.ts @@ -0,0 +1,334 @@ +/** + * Ad Group Management Tools + * + * Tools for listing, creating, and updating ad groups + */ + +import { createPrivateTool } from "@decocms/runtime/tools"; +import { z } from "zod"; +import type { Env } from "../main.ts"; +import { TikTokClient, getAccessToken } from "../lib/tiktok-client.ts"; + +// ============================================================================ +// Schema Definitions +// ============================================================================ + +const OperationStatusSchema = z.enum(["ENABLE", "DISABLE", "DELETE"]); + +const BudgetModeSchema = z.enum([ + "BUDGET_MODE_INFINITE", + "BUDGET_MODE_DAY", + "BUDGET_MODE_TOTAL", +]); + +const BidTypeSchema = z.enum(["BID_TYPE_NO_BID", "BID_TYPE_CUSTOM"]); + +const OptimizationGoalSchema = z.enum([ + "CLICK", + "CONVERT", + "SHOW", + "REACH", + "VIDEO_VIEW", + "LEAD_GENERATION", + "ENGAGEMENT", +]); + +const PlacementSchema = z.enum([ + "PLACEMENT_TIKTOK", + "PLACEMENT_PANGLE", + "PLACEMENT_GLOBAL_APP_BUNDLE", +]); + +const ScheduleTypeSchema = z.enum(["SCHEDULE_START_END", "SCHEDULE_FROM_NOW"]); + +const GenderSchema = z.enum([ + "GENDER_UNLIMITED", + "GENDER_MALE", + "GENDER_FEMALE", +]); + +const AdGroupSchema = z.object({ + adgroup_id: z.string().describe("Ad Group ID"), + adgroup_name: z.string().describe("Ad Group name"), + advertiser_id: z.string().describe("Advertiser ID"), + campaign_id: z.string().describe("Campaign ID"), + operation_status: OperationStatusSchema.describe("Operation status"), + secondary_status: z.string().describe("Secondary status"), + placement_type: z.string().describe("Placement type"), + placements: z.array(PlacementSchema).optional().describe("Placements"), + optimization_goal: OptimizationGoalSchema.describe("Optimization goal"), + bid_type: BidTypeSchema.describe("Bid type"), + bid_price: z.number().optional().describe("Bid price"), + budget_mode: BudgetModeSchema.describe("Budget mode"), + budget: z.number().optional().describe("Budget amount"), + schedule_type: ScheduleTypeSchema.describe("Schedule type"), + schedule_start_time: z.string().optional().describe("Schedule start time"), + schedule_end_time: z.string().optional().describe("Schedule end time"), + create_time: z.string().describe("Creation timestamp"), + modify_time: z.string().describe("Last modification timestamp"), +}); + +const PageInfoSchema = z.object({ + page: z.number().describe("Current page number"), + page_size: z.number().describe("Items per page"), + total_number: z.number().describe("Total number of items"), + total_page: z.number().describe("Total number of pages"), +}); + +// ============================================================================ +// List Ad Groups Tool +// ============================================================================ + +export const createListAdGroupsTool = (env: Env) => + createPrivateTool({ + id: "list_adgroups", + description: + "List all ad groups for an advertiser with optional filters for campaign, name, and status.", + inputSchema: z.object({ + advertiser_id: z.string().describe("Advertiser ID (required)"), + campaign_ids: z + .array(z.string()) + .optional() + .describe("Filter by campaign IDs"), + adgroup_ids: z + .array(z.string()) + .optional() + .describe("Filter by specific ad group IDs"), + adgroup_name: z + .string() + .optional() + .describe("Filter by ad group name (partial match)"), + page: z.coerce + .number() + .int() + .min(1) + .optional() + .describe("Page number (default: 1)"), + page_size: z.coerce + .number() + .int() + .min(1) + .max(1000) + .optional() + .describe("Items per page (default: 50, max: 1000)"), + }), + outputSchema: z.object({ + adgroups: z.array(AdGroupSchema).describe("List of ad groups"), + page_info: PageInfoSchema.describe("Pagination info"), + }), + execute: async ({ context }) => { + const client = new TikTokClient({ + accessToken: getAccessToken(env), + }); + + const result = await client.listAdGroups({ + advertiser_id: context.advertiser_id, + campaign_ids: context.campaign_ids, + adgroup_ids: context.adgroup_ids, + filtering: { + adgroup_name: context.adgroup_name, + }, + page: context.page, + page_size: context.page_size, + }); + + return { + adgroups: result.adgroups, + page_info: result.page_info, + }; + }, + }); + +// ============================================================================ +// Get Ad Group Tool +// ============================================================================ + +export const createGetAdGroupTool = (env: Env) => + createPrivateTool({ + id: "get_adgroup", + description: + "Get detailed information about a specific ad group by its ID.", + inputSchema: z.object({ + advertiser_id: z.string().describe("Advertiser ID (required)"), + adgroup_id: z.string().describe("Ad Group ID to retrieve"), + }), + outputSchema: z.object({ + adgroup: AdGroupSchema.nullable().describe("Ad Group details"), + }), + execute: async ({ context }) => { + const client = new TikTokClient({ + accessToken: getAccessToken(env), + }); + + const result = await client.listAdGroups({ + advertiser_id: context.advertiser_id, + adgroup_ids: [context.adgroup_id], + }); + + return { + adgroup: result.adgroups[0] || null, + }; + }, + }); + +// ============================================================================ +// Create Ad Group Tool +// ============================================================================ + +export const createCreateAdGroupTool = (env: Env) => + createPrivateTool({ + id: "create_adgroup", + description: + "Create a new ad group within a campaign. Requires advertiser ID, campaign ID, name, and optimization goal.", + inputSchema: z.object({ + advertiser_id: z.string().describe("Advertiser ID (required)"), + campaign_id: z.string().describe("Campaign ID (required)"), + adgroup_name: z.string().describe("Ad Group name (required)"), + optimization_goal: OptimizationGoalSchema.describe( + "Optimization goal (required). Options: CLICK, CONVERT, SHOW, REACH, VIDEO_VIEW, LEAD_GENERATION, ENGAGEMENT", + ), + placements: z + .array(PlacementSchema) + .optional() + .describe( + "Placements: PLACEMENT_TIKTOK, PLACEMENT_PANGLE, PLACEMENT_GLOBAL_APP_BUNDLE", + ), + bid_type: BidTypeSchema.optional().describe( + "Bid type: BID_TYPE_NO_BID or BID_TYPE_CUSTOM", + ), + bid_price: z.coerce + .number() + .positive() + .optional() + .describe("Bid price (required if bid_type is CUSTOM)"), + budget_mode: BudgetModeSchema.optional().describe("Budget mode"), + budget: z.coerce.number().positive().optional().describe("Budget amount"), + schedule_type: ScheduleTypeSchema.optional().describe( + "Schedule type: SCHEDULE_START_END or SCHEDULE_FROM_NOW", + ), + schedule_start_time: z + .string() + .optional() + .describe("Start time (format: YYYY-MM-DD HH:mm:ss)"), + schedule_end_time: z + .string() + .optional() + .describe("End time (format: YYYY-MM-DD HH:mm:ss)"), + location_ids: z + .array(z.string()) + .optional() + .describe("Target location IDs"), + gender: GenderSchema.optional().describe("Target gender"), + age_groups: z.array(z.string()).optional().describe("Target age groups"), + languages: z.array(z.string()).optional().describe("Target languages"), + operation_status: OperationStatusSchema.optional().describe( + "Initial status: ENABLE or DISABLE", + ), + }), + outputSchema: z.object({ + adgroup_id: z.string().describe("ID of the created ad group"), + success: z.boolean().describe("Whether creation was successful"), + message: z.string().describe("Result message"), + }), + execute: async ({ context }) => { + const client = new TikTokClient({ + accessToken: getAccessToken(env), + }); + + const result = await client.createAdGroup({ + advertiser_id: context.advertiser_id, + campaign_id: context.campaign_id, + adgroup_name: context.adgroup_name, + optimization_goal: context.optimization_goal, + placements: context.placements, + bid_type: context.bid_type, + bid_price: context.bid_price, + budget_mode: context.budget_mode, + budget: context.budget, + schedule_type: context.schedule_type, + schedule_start_time: context.schedule_start_time, + schedule_end_time: context.schedule_end_time, + location_ids: context.location_ids, + gender: context.gender, + age_groups: context.age_groups, + languages: context.languages, + operation_status: context.operation_status, + }); + + return { + adgroup_id: result.adgroup_id, + success: true, + message: `Ad Group "${context.adgroup_name}" created successfully`, + }; + }, + }); + +// ============================================================================ +// Update Ad Group Tool +// ============================================================================ + +export const createUpdateAdGroupTool = (env: Env) => + createPrivateTool({ + id: "update_adgroup", + description: + "Update an existing ad group. Only provided fields will be updated.", + inputSchema: z.object({ + advertiser_id: z.string().describe("Advertiser ID (required)"), + adgroup_id: z.string().describe("Ad Group ID to update (required)"), + adgroup_name: z.string().optional().describe("New ad group name"), + bid_price: z.coerce + .number() + .positive() + .optional() + .describe("New bid price"), + budget: z.coerce + .number() + .positive() + .optional() + .describe("New budget amount"), + schedule_end_time: z + .string() + .optional() + .describe("New end time (format: YYYY-MM-DD HH:mm:ss)"), + operation_status: OperationStatusSchema.optional().describe( + "New status: ENABLE, DISABLE, or DELETE", + ), + }), + outputSchema: z.object({ + adgroup_id: z.string().describe("ID of the updated ad group"), + success: z.boolean().describe("Whether update was successful"), + message: z.string().describe("Result message"), + }), + execute: async ({ context }) => { + const client = new TikTokClient({ + accessToken: getAccessToken(env), + }); + + const result = await client.updateAdGroup({ + advertiser_id: context.advertiser_id, + adgroup_id: context.adgroup_id, + adgroup_name: context.adgroup_name, + bid_price: context.bid_price, + budget: context.budget, + schedule_end_time: context.schedule_end_time, + operation_status: context.operation_status, + }); + + return { + adgroup_id: result.adgroup_id, + success: true, + message: `Ad Group ${context.adgroup_id} updated successfully`, + }; + }, + }); + +// ============================================================================ +// Export all ad group tools +// ============================================================================ + +export const adgroupTools = [ + createListAdGroupsTool, + createGetAdGroupTool, + createCreateAdGroupTool, + createUpdateAdGroupTool, +]; diff --git a/tiktok-ads/server/tools/ads.ts b/tiktok-ads/server/tools/ads.ts new file mode 100644 index 00000000..3107cbba --- /dev/null +++ b/tiktok-ads/server/tools/ads.ts @@ -0,0 +1,301 @@ +/** + * Ad Management Tools + * + * Tools for listing, creating, and updating ads + */ + +import { createPrivateTool } from "@decocms/runtime/tools"; +import { z } from "zod"; +import type { Env } from "../main.ts"; +import { TikTokClient, getAccessToken } from "../lib/tiktok-client.ts"; + +// ============================================================================ +// Schema Definitions +// ============================================================================ + +const OperationStatusSchema = z.enum(["ENABLE", "DISABLE", "DELETE"]); + +const AdFormatSchema = z.enum([ + "SINGLE_VIDEO", + "SINGLE_IMAGE", + "VIDEO_CAROUSEL", + "IMAGE_CAROUSEL", + "SPARK_ADS", +]); + +const AdSchema = z.object({ + ad_id: z.string().describe("Ad ID"), + ad_name: z.string().describe("Ad name"), + advertiser_id: z.string().describe("Advertiser ID"), + campaign_id: z.string().describe("Campaign ID"), + adgroup_id: z.string().describe("Ad Group ID"), + operation_status: OperationStatusSchema.describe("Operation status"), + secondary_status: z.string().describe("Secondary status"), + ad_format: AdFormatSchema.describe("Ad format"), + ad_text: z.string().optional().describe("Ad text/caption"), + call_to_action: z.string().optional().describe("Call to action text"), + landing_page_url: z.string().optional().describe("Landing page URL"), + display_name: z.string().optional().describe("Display name"), + video_id: z.string().optional().describe("Video ID"), + image_ids: z.array(z.string()).optional().describe("Image IDs"), + create_time: z.string().describe("Creation timestamp"), + modify_time: z.string().describe("Last modification timestamp"), +}); + +const PageInfoSchema = z.object({ + page: z.number().describe("Current page number"), + page_size: z.number().describe("Items per page"), + total_number: z.number().describe("Total number of items"), + total_page: z.number().describe("Total number of pages"), +}); + +// ============================================================================ +// List Ads Tool +// ============================================================================ + +export const createListAdsTool = (env: Env) => + createPrivateTool({ + id: "list_ads", + description: + "List all ads for an advertiser with optional filters for campaign, ad group, name, and status.", + inputSchema: z.object({ + advertiser_id: z.string().describe("Advertiser ID (required)"), + campaign_ids: z + .array(z.string()) + .optional() + .describe("Filter by campaign IDs"), + adgroup_ids: z + .array(z.string()) + .optional() + .describe("Filter by ad group IDs"), + ad_ids: z + .array(z.string()) + .optional() + .describe("Filter by specific ad IDs"), + ad_name: z + .string() + .optional() + .describe("Filter by ad name (partial match)"), + page: z.coerce + .number() + .int() + .min(1) + .optional() + .describe("Page number (default: 1)"), + page_size: z.coerce + .number() + .int() + .min(1) + .max(1000) + .optional() + .describe("Items per page (default: 50, max: 1000)"), + }), + outputSchema: z.object({ + ads: z.array(AdSchema).describe("List of ads"), + page_info: PageInfoSchema.describe("Pagination info"), + }), + execute: async ({ context }) => { + const client = new TikTokClient({ + accessToken: getAccessToken(env), + }); + + const result = await client.listAds({ + advertiser_id: context.advertiser_id, + campaign_ids: context.campaign_ids, + adgroup_ids: context.adgroup_ids, + ad_ids: context.ad_ids, + filtering: { + ad_name: context.ad_name, + }, + page: context.page, + page_size: context.page_size, + }); + + return { + ads: result.ads, + page_info: result.page_info, + }; + }, + }); + +// ============================================================================ +// Get Ad Tool +// ============================================================================ + +export const createGetAdTool = (env: Env) => + createPrivateTool({ + id: "get_ad", + description: "Get detailed information about a specific ad by its ID.", + inputSchema: z.object({ + advertiser_id: z.string().describe("Advertiser ID (required)"), + ad_id: z.string().describe("Ad ID to retrieve"), + }), + outputSchema: z.object({ + ad: AdSchema.nullable().describe("Ad details"), + }), + execute: async ({ context }) => { + const client = new TikTokClient({ + accessToken: getAccessToken(env), + }); + + const result = await client.listAds({ + advertiser_id: context.advertiser_id, + ad_ids: [context.ad_id], + }); + + return { + ad: result.ads[0] || null, + }; + }, + }); + +// ============================================================================ +// Create Ad Tool +// ============================================================================ + +export const createCreateAdTool = (env: Env) => + createPrivateTool({ + id: "create_ad", + description: + "Create a new ad within an ad group. Requires advertiser ID, ad group ID, name, and ad format.", + inputSchema: z.object({ + advertiser_id: z.string().describe("Advertiser ID (required)"), + adgroup_id: z.string().describe("Ad Group ID (required)"), + ad_name: z.string().describe("Ad name (required)"), + ad_format: AdFormatSchema.describe( + "Ad format (required). Options: SINGLE_VIDEO, SINGLE_IMAGE, VIDEO_CAROUSEL, IMAGE_CAROUSEL, SPARK_ADS", + ), + ad_text: z + .string() + .optional() + .describe("Ad text/caption (max 100 characters)"), + call_to_action: z + .string() + .optional() + .describe( + "Call to action (e.g., 'Learn More', 'Shop Now', 'Sign Up', 'Download')", + ), + landing_page_url: z + .string() + .url() + .optional() + .describe("Landing page URL"), + display_name: z + .string() + .optional() + .describe("Display name shown on the ad"), + video_id: z + .string() + .optional() + .describe("Video ID (required for video ads)"), + image_ids: z + .array(z.string()) + .optional() + .describe("Image IDs (required for image ads)"), + identity_id: z + .string() + .optional() + .describe("TikTok account identity ID (for Spark Ads)"), + identity_type: z + .string() + .optional() + .describe("Identity type: AUTH_CODE, TT_USER"), + operation_status: OperationStatusSchema.optional().describe( + "Initial status: ENABLE or DISABLE", + ), + }), + outputSchema: z.object({ + ad_id: z.string().describe("ID of the created ad"), + success: z.boolean().describe("Whether creation was successful"), + message: z.string().describe("Result message"), + }), + execute: async ({ context }) => { + const client = new TikTokClient({ + accessToken: getAccessToken(env), + }); + + const result = await client.createAd({ + advertiser_id: context.advertiser_id, + adgroup_id: context.adgroup_id, + ad_name: context.ad_name, + ad_format: context.ad_format, + ad_text: context.ad_text, + call_to_action: context.call_to_action, + landing_page_url: context.landing_page_url, + display_name: context.display_name, + video_id: context.video_id, + image_ids: context.image_ids, + identity_id: context.identity_id, + identity_type: context.identity_type, + operation_status: context.operation_status, + }); + + return { + ad_id: result.ad_id, + success: true, + message: `Ad "${context.ad_name}" created successfully`, + }; + }, + }); + +// ============================================================================ +// Update Ad Tool +// ============================================================================ + +export const createUpdateAdTool = (env: Env) => + createPrivateTool({ + id: "update_ad", + description: "Update an existing ad. Only provided fields will be updated.", + inputSchema: z.object({ + advertiser_id: z.string().describe("Advertiser ID (required)"), + ad_id: z.string().describe("Ad ID to update (required)"), + ad_name: z.string().optional().describe("New ad name"), + ad_text: z.string().optional().describe("New ad text/caption"), + call_to_action: z.string().optional().describe("New call to action"), + landing_page_url: z + .string() + .url() + .optional() + .describe("New landing page URL"), + operation_status: OperationStatusSchema.optional().describe( + "New status: ENABLE, DISABLE, or DELETE", + ), + }), + outputSchema: z.object({ + ad_id: z.string().describe("ID of the updated ad"), + success: z.boolean().describe("Whether update was successful"), + message: z.string().describe("Result message"), + }), + execute: async ({ context }) => { + const client = new TikTokClient({ + accessToken: getAccessToken(env), + }); + + const result = await client.updateAd({ + advertiser_id: context.advertiser_id, + ad_id: context.ad_id, + ad_name: context.ad_name, + ad_text: context.ad_text, + call_to_action: context.call_to_action, + landing_page_url: context.landing_page_url, + operation_status: context.operation_status, + }); + + return { + ad_id: result.ad_id, + success: true, + message: `Ad ${context.ad_id} updated successfully`, + }; + }, + }); + +// ============================================================================ +// Export all ad tools +// ============================================================================ + +export const adTools = [ + createListAdsTool, + createGetAdTool, + createCreateAdTool, + createUpdateAdTool, +]; diff --git a/tiktok-ads/server/tools/campaigns.ts b/tiktok-ads/server/tools/campaigns.ts new file mode 100644 index 00000000..89ce1550 --- /dev/null +++ b/tiktok-ads/server/tools/campaigns.ts @@ -0,0 +1,263 @@ +/** + * Campaign Management Tools + * + * Tools for listing, creating, and updating campaigns + */ + +import { createPrivateTool } from "@decocms/runtime/tools"; +import { z } from "zod"; +import type { Env } from "../main.ts"; +import { TikTokClient, getAccessToken } from "../lib/tiktok-client.ts"; + +// ============================================================================ +// Schema Definitions +// ============================================================================ + +const CampaignObjectiveSchema = z.enum([ + "TRAFFIC", + "APP_PROMOTION", + "WEB_CONVERSIONS", + "PRODUCT_SALES", + "REACH", + "VIDEO_VIEWS", + "LEAD_GENERATION", + "COMMUNITY_INTERACTION", +]); + +const BudgetModeSchema = z.enum([ + "BUDGET_MODE_INFINITE", + "BUDGET_MODE_DAY", + "BUDGET_MODE_TOTAL", +]); + +const OperationStatusSchema = z.enum(["ENABLE", "DISABLE", "DELETE"]); + +const CampaignSchema = z.object({ + campaign_id: z.string().describe("Campaign ID"), + campaign_name: z.string().describe("Campaign name"), + advertiser_id: z.string().describe("Advertiser ID"), + objective_type: CampaignObjectiveSchema.describe("Campaign objective"), + operation_status: OperationStatusSchema.describe("Operation status"), + secondary_status: z.string().describe("Secondary status"), + budget_mode: BudgetModeSchema.describe("Budget mode"), + budget: z.number().optional().describe("Budget amount"), + create_time: z.string().describe("Creation timestamp"), + modify_time: z.string().describe("Last modification timestamp"), +}); + +const PageInfoSchema = z.object({ + page: z.number().describe("Current page number"), + page_size: z.number().describe("Items per page"), + total_number: z.number().describe("Total number of items"), + total_page: z.number().describe("Total number of pages"), +}); + +// ============================================================================ +// List Campaigns Tool +// ============================================================================ + +export const createListCampaignsTool = (env: Env) => + createPrivateTool({ + id: "list_campaigns", + description: + "List all campaigns for an advertiser with optional filters for name, objective, and status.", + inputSchema: z.object({ + advertiser_id: z.string().describe("Advertiser ID (required)"), + campaign_ids: z + .array(z.string()) + .optional() + .describe("Filter by specific campaign IDs"), + campaign_name: z + .string() + .optional() + .describe("Filter by campaign name (partial match)"), + objective_type: CampaignObjectiveSchema.optional().describe( + "Filter by objective type", + ), + page: z.coerce + .number() + .int() + .min(1) + .optional() + .describe("Page number (default: 1)"), + page_size: z.coerce + .number() + .int() + .min(1) + .max(1000) + .optional() + .describe("Items per page (default: 50, max: 1000)"), + }), + outputSchema: z.object({ + campaigns: z.array(CampaignSchema).describe("List of campaigns"), + page_info: PageInfoSchema.describe("Pagination info"), + }), + execute: async ({ context }) => { + const client = new TikTokClient({ + accessToken: getAccessToken(env), + }); + + const result = await client.listCampaigns({ + advertiser_id: context.advertiser_id, + campaign_ids: context.campaign_ids, + filtering: { + campaign_name: context.campaign_name, + objective_type: context.objective_type, + }, + page: context.page, + page_size: context.page_size, + }); + + return { + campaigns: result.campaigns, + page_info: result.page_info, + }; + }, + }); + +// ============================================================================ +// Get Campaign Tool +// ============================================================================ + +export const createGetCampaignTool = (env: Env) => + createPrivateTool({ + id: "get_campaign", + description: + "Get detailed information about a specific campaign by its ID.", + inputSchema: z.object({ + advertiser_id: z.string().describe("Advertiser ID (required)"), + campaign_id: z.string().describe("Campaign ID to retrieve"), + }), + outputSchema: z.object({ + campaign: CampaignSchema.nullable().describe("Campaign details"), + }), + execute: async ({ context }) => { + const client = new TikTokClient({ + accessToken: getAccessToken(env), + }); + + const result = await client.listCampaigns({ + advertiser_id: context.advertiser_id, + campaign_ids: [context.campaign_id], + }); + + return { + campaign: result.campaigns[0] || null, + }; + }, + }); + +// ============================================================================ +// Create Campaign Tool +// ============================================================================ + +export const createCreateCampaignTool = (env: Env) => + createPrivateTool({ + id: "create_campaign", + description: + "Create a new advertising campaign. Requires advertiser ID, name, and objective type.", + inputSchema: z.object({ + advertiser_id: z.string().describe("Advertiser ID (required)"), + campaign_name: z.string().describe("Campaign name (required)"), + objective_type: CampaignObjectiveSchema.describe( + "Campaign objective (required). Options: TRAFFIC, APP_PROMOTION, WEB_CONVERSIONS, PRODUCT_SALES, REACH, VIDEO_VIEWS, LEAD_GENERATION, COMMUNITY_INTERACTION", + ), + budget_mode: BudgetModeSchema.optional().describe( + "Budget mode: BUDGET_MODE_INFINITE (no limit), BUDGET_MODE_DAY (daily), BUDGET_MODE_TOTAL (lifetime)", + ), + budget: z.coerce + .number() + .positive() + .optional() + .describe("Budget amount (required if budget_mode is DAY or TOTAL)"), + operation_status: OperationStatusSchema.optional().describe( + "Initial status: ENABLE or DISABLE (default: ENABLE)", + ), + }), + outputSchema: z.object({ + campaign_id: z.string().describe("ID of the created campaign"), + success: z.boolean().describe("Whether creation was successful"), + message: z.string().describe("Result message"), + }), + execute: async ({ context }) => { + const client = new TikTokClient({ + accessToken: getAccessToken(env), + }); + + const result = await client.createCampaign({ + advertiser_id: context.advertiser_id, + campaign_name: context.campaign_name, + objective_type: context.objective_type, + budget_mode: context.budget_mode, + budget: context.budget, + operation_status: context.operation_status, + }); + + return { + campaign_id: result.campaign_id, + success: true, + message: `Campaign "${context.campaign_name}" created successfully`, + }; + }, + }); + +// ============================================================================ +// Update Campaign Tool +// ============================================================================ + +export const createUpdateCampaignTool = (env: Env) => + createPrivateTool({ + id: "update_campaign", + description: + "Update an existing campaign. Only provided fields will be updated.", + inputSchema: z.object({ + advertiser_id: z.string().describe("Advertiser ID (required)"), + campaign_id: z.string().describe("Campaign ID to update (required)"), + campaign_name: z.string().optional().describe("New campaign name"), + budget_mode: BudgetModeSchema.optional().describe("New budget mode"), + budget: z.coerce + .number() + .positive() + .optional() + .describe("New budget amount"), + operation_status: OperationStatusSchema.optional().describe( + "New status: ENABLE, DISABLE, or DELETE", + ), + }), + outputSchema: z.object({ + campaign_id: z.string().describe("ID of the updated campaign"), + success: z.boolean().describe("Whether update was successful"), + message: z.string().describe("Result message"), + }), + execute: async ({ context }) => { + const client = new TikTokClient({ + accessToken: getAccessToken(env), + }); + + const result = await client.updateCampaign({ + advertiser_id: context.advertiser_id, + campaign_id: context.campaign_id, + campaign_name: context.campaign_name, + budget_mode: context.budget_mode, + budget: context.budget, + operation_status: context.operation_status, + }); + + return { + campaign_id: result.campaign_id, + success: true, + message: `Campaign ${context.campaign_id} updated successfully`, + }; + }, + }); + +// ============================================================================ +// Export all campaign tools +// ============================================================================ + +export const campaignTools = [ + createListCampaignsTool, + createGetCampaignTool, + createCreateCampaignTool, + createUpdateCampaignTool, +]; diff --git a/tiktok-ads/server/tools/index.ts b/tiktok-ads/server/tools/index.ts new file mode 100644 index 00000000..75c520bb --- /dev/null +++ b/tiktok-ads/server/tools/index.ts @@ -0,0 +1,29 @@ +/** + * Central export point for all TikTok Ads tools + * + * This file aggregates all tools from different modules into a single + * export, making it easy to import all tools in main.ts. + * + * Tools: + * - campaignTools: Campaign management (list, get, create, update) + * - adgroupTools: Ad Group management (list, get, create, update) + * - adTools: Ad management (list, get, create, update) + * - reportTools: Reports and analytics (get_report, campaign/adgroup/ad reports, advertiser info) + */ + +import { campaignTools } from "./campaigns.ts"; +import { adgroupTools } from "./adgroups.ts"; +import { adTools } from "./ads.ts"; +import { reportTools } from "./reports.ts"; + +// Export all tools from all modules +export const tools = [ + // Campaign management tools + ...campaignTools, + // Ad Group management tools + ...adgroupTools, + // Ad management tools + ...adTools, + // Report and analytics tools + ...reportTools, +]; diff --git a/tiktok-ads/server/tools/reports.ts b/tiktok-ads/server/tools/reports.ts new file mode 100644 index 00000000..a9cb9193 --- /dev/null +++ b/tiktok-ads/server/tools/reports.ts @@ -0,0 +1,484 @@ +/** + * Report Tools + * + * Tools for getting performance reports at different levels + */ + +import { createPrivateTool } from "@decocms/runtime/tools"; +import { z } from "zod"; +import type { Env } from "../main.ts"; +import { TikTokClient, getAccessToken } from "../lib/tiktok-client.ts"; + +// ============================================================================ +// Schema Definitions +// ============================================================================ + +const DataLevelSchema = z.enum([ + "AUCTION_ADVERTISER", + "AUCTION_CAMPAIGN", + "AUCTION_ADGROUP", + "AUCTION_AD", +]); + +const DimensionSchema = z.enum([ + "advertiser_id", + "campaign_id", + "adgroup_id", + "ad_id", + "stat_time_day", + "stat_time_hour", +]); + +const MetricSchema = z.enum([ + "spend", + "impressions", + "clicks", + "ctr", + "cpc", + "cpm", + "reach", + "frequency", + "conversion", + "cost_per_conversion", + "conversion_rate", + "video_play_actions", + "video_watched_2s", + "video_watched_6s", + "average_video_play", + "average_video_play_per_user", + "profile_visits", + "likes", + "comments", + "shares", + "follows", +]); + +const ReportRowSchema = z.object({ + dimensions: z + .record(z.string()) + .describe("Dimension values (campaign_id, stat_time_day, etc.)"), + metrics: z + .record(z.number()) + .describe("Metric values (spend, impressions, clicks, etc.)"), +}); + +const PageInfoSchema = z.object({ + page: z.number().describe("Current page number"), + page_size: z.number().describe("Items per page"), + total_number: z.number().describe("Total number of items"), + total_page: z.number().describe("Total number of pages"), +}); + +// ============================================================================ +// Get Report Tool (Generic) +// ============================================================================ + +export const createGetReportTool = (env: Env) => + createPrivateTool({ + id: "get_report", + description: + "Get performance report data for campaigns, ad groups, or ads. Supports custom date ranges, dimensions, and metrics.", + inputSchema: z.object({ + advertiser_id: z.string().describe("Advertiser ID (required)"), + data_level: DataLevelSchema.describe( + "Data level: AUCTION_ADVERTISER, AUCTION_CAMPAIGN, AUCTION_ADGROUP, AUCTION_AD", + ), + start_date: z + .string() + .describe("Start date (format: YYYY-MM-DD, required)"), + end_date: z.string().describe("End date (format: YYYY-MM-DD, required)"), + dimensions: z + .array(DimensionSchema) + .optional() + .describe( + "Dimensions to include: advertiser_id, campaign_id, adgroup_id, ad_id, stat_time_day, stat_time_hour", + ), + metrics: z + .array(MetricSchema) + .optional() + .describe( + "Metrics to include: spend, impressions, clicks, ctr, cpc, cpm, reach, conversion, etc. Default: all common metrics", + ), + campaign_ids: z + .array(z.string()) + .optional() + .describe("Filter by campaign IDs"), + adgroup_ids: z + .array(z.string()) + .optional() + .describe("Filter by ad group IDs"), + ad_ids: z.array(z.string()).optional().describe("Filter by ad IDs"), + page: z.coerce + .number() + .int() + .min(1) + .optional() + .describe("Page number (default: 1)"), + page_size: z.coerce + .number() + .int() + .min(1) + .max(1000) + .optional() + .describe("Items per page (default: 50, max: 1000)"), + }), + outputSchema: z.object({ + rows: z.array(ReportRowSchema).describe("Report data rows"), + page_info: PageInfoSchema.describe("Pagination info"), + }), + execute: async ({ context }) => { + const client = new TikTokClient({ + accessToken: getAccessToken(env), + }); + + // Default dimensions based on data level + let dimensions = context.dimensions; + if (!dimensions || dimensions.length === 0) { + switch (context.data_level) { + case "AUCTION_CAMPAIGN": + dimensions = ["campaign_id", "stat_time_day"]; + break; + case "AUCTION_ADGROUP": + dimensions = ["adgroup_id", "stat_time_day"]; + break; + case "AUCTION_AD": + dimensions = ["ad_id", "stat_time_day"]; + break; + default: + dimensions = ["advertiser_id", "stat_time_day"]; + } + } + + // Default metrics + const metrics = + context.metrics && context.metrics.length > 0 + ? context.metrics + : [ + "spend", + "impressions", + "clicks", + "ctr", + "cpc", + "cpm", + "conversion", + ]; + + const result = await client.getReport({ + advertiser_id: context.advertiser_id, + data_level: context.data_level, + dimensions, + metrics, + start_date: context.start_date, + end_date: context.end_date, + filters: { + campaign_ids: context.campaign_ids, + adgroup_ids: context.adgroup_ids, + ad_ids: context.ad_ids, + }, + page: context.page, + page_size: context.page_size, + }); + + return { + rows: result.rows, + page_info: result.page_info, + }; + }, + }); + +// ============================================================================ +// Get Campaign Report Tool (Simplified) +// ============================================================================ + +export const createGetCampaignReportTool = (env: Env) => + createPrivateTool({ + id: "get_campaign_report", + description: + "Get performance report for campaigns. Returns spend, impressions, clicks, conversions and other metrics by day.", + inputSchema: z.object({ + advertiser_id: z.string().describe("Advertiser ID (required)"), + start_date: z + .string() + .describe("Start date (format: YYYY-MM-DD, required)"), + end_date: z.string().describe("End date (format: YYYY-MM-DD, required)"), + campaign_ids: z + .array(z.string()) + .optional() + .describe("Filter by specific campaign IDs"), + page: z.coerce + .number() + .int() + .min(1) + .optional() + .describe("Page number (default: 1)"), + page_size: z.coerce + .number() + .int() + .min(1) + .max(1000) + .optional() + .describe("Items per page (default: 50)"), + }), + outputSchema: z.object({ + rows: z.array(ReportRowSchema).describe("Campaign report data"), + page_info: PageInfoSchema.describe("Pagination info"), + }), + execute: async ({ context }) => { + const client = new TikTokClient({ + accessToken: getAccessToken(env), + }); + + const result = await client.getReport({ + advertiser_id: context.advertiser_id, + data_level: "AUCTION_CAMPAIGN", + dimensions: ["campaign_id", "stat_time_day"], + metrics: [ + "spend", + "impressions", + "clicks", + "ctr", + "cpc", + "cpm", + "reach", + "conversion", + "cost_per_conversion", + ], + start_date: context.start_date, + end_date: context.end_date, + filters: { + campaign_ids: context.campaign_ids, + }, + page: context.page, + page_size: context.page_size, + }); + + return { + rows: result.rows, + page_info: result.page_info, + }; + }, + }); + +// ============================================================================ +// Get Ad Group Report Tool (Simplified) +// ============================================================================ + +export const createGetAdGroupReportTool = (env: Env) => + createPrivateTool({ + id: "get_adgroup_report", + description: + "Get performance report for ad groups. Returns spend, impressions, clicks, conversions and other metrics by day.", + inputSchema: z.object({ + advertiser_id: z.string().describe("Advertiser ID (required)"), + start_date: z + .string() + .describe("Start date (format: YYYY-MM-DD, required)"), + end_date: z.string().describe("End date (format: YYYY-MM-DD, required)"), + campaign_ids: z + .array(z.string()) + .optional() + .describe("Filter by campaign IDs"), + adgroup_ids: z + .array(z.string()) + .optional() + .describe("Filter by specific ad group IDs"), + page: z.coerce + .number() + .int() + .min(1) + .optional() + .describe("Page number (default: 1)"), + page_size: z.coerce + .number() + .int() + .min(1) + .max(1000) + .optional() + .describe("Items per page (default: 50)"), + }), + outputSchema: z.object({ + rows: z.array(ReportRowSchema).describe("Ad group report data"), + page_info: PageInfoSchema.describe("Pagination info"), + }), + execute: async ({ context }) => { + const client = new TikTokClient({ + accessToken: getAccessToken(env), + }); + + const result = await client.getReport({ + advertiser_id: context.advertiser_id, + data_level: "AUCTION_ADGROUP", + dimensions: ["adgroup_id", "stat_time_day"], + metrics: [ + "spend", + "impressions", + "clicks", + "ctr", + "cpc", + "cpm", + "reach", + "conversion", + "cost_per_conversion", + ], + start_date: context.start_date, + end_date: context.end_date, + filters: { + campaign_ids: context.campaign_ids, + adgroup_ids: context.adgroup_ids, + }, + page: context.page, + page_size: context.page_size, + }); + + return { + rows: result.rows, + page_info: result.page_info, + }; + }, + }); + +// ============================================================================ +// Get Ad Report Tool (Simplified) +// ============================================================================ + +export const createGetAdReportTool = (env: Env) => + createPrivateTool({ + id: "get_ad_report", + description: + "Get performance report for individual ads. Returns spend, impressions, clicks, conversions and other metrics by day.", + inputSchema: z.object({ + advertiser_id: z.string().describe("Advertiser ID (required)"), + start_date: z + .string() + .describe("Start date (format: YYYY-MM-DD, required)"), + end_date: z.string().describe("End date (format: YYYY-MM-DD, required)"), + campaign_ids: z + .array(z.string()) + .optional() + .describe("Filter by campaign IDs"), + adgroup_ids: z + .array(z.string()) + .optional() + .describe("Filter by ad group IDs"), + ad_ids: z + .array(z.string()) + .optional() + .describe("Filter by specific ad IDs"), + page: z.coerce + .number() + .int() + .min(1) + .optional() + .describe("Page number (default: 1)"), + page_size: z.coerce + .number() + .int() + .min(1) + .max(1000) + .optional() + .describe("Items per page (default: 50)"), + }), + outputSchema: z.object({ + rows: z.array(ReportRowSchema).describe("Ad report data"), + page_info: PageInfoSchema.describe("Pagination info"), + }), + execute: async ({ context }) => { + const client = new TikTokClient({ + accessToken: getAccessToken(env), + }); + + const result = await client.getReport({ + advertiser_id: context.advertiser_id, + data_level: "AUCTION_AD", + dimensions: ["ad_id", "stat_time_day"], + metrics: [ + "spend", + "impressions", + "clicks", + "ctr", + "cpc", + "cpm", + "reach", + "conversion", + "cost_per_conversion", + "video_play_actions", + "video_watched_2s", + "video_watched_6s", + "likes", + "comments", + "shares", + ], + start_date: context.start_date, + end_date: context.end_date, + filters: { + campaign_ids: context.campaign_ids, + adgroup_ids: context.adgroup_ids, + ad_ids: context.ad_ids, + }, + page: context.page, + page_size: context.page_size, + }); + + return { + rows: result.rows, + page_info: result.page_info, + }; + }, + }); + +// ============================================================================ +// Get Advertiser Info Tool +// ============================================================================ + +export const createGetAdvertiserInfoTool = (env: Env) => + createPrivateTool({ + id: "get_advertiser_info", + description: + "Get information about one or more advertisers, including name, status, balance, and timezone.", + inputSchema: z.object({ + advertiser_ids: z + .array(z.string()) + .describe("List of advertiser IDs to retrieve (required)"), + }), + outputSchema: z.object({ + advertisers: z + .array( + z.object({ + advertiser_id: z.string().describe("Advertiser ID"), + advertiser_name: z.string().describe("Advertiser name"), + status: z.string().describe("Account status"), + company: z.string().optional().describe("Company name"), + balance: z.number().optional().describe("Account balance"), + currency: z.string().optional().describe("Currency code"), + timezone: z.string().optional().describe("Timezone"), + create_time: z.string().describe("Account creation time"), + }), + ) + .describe("List of advertiser information"), + }), + execute: async ({ context }) => { + const client = new TikTokClient({ + accessToken: getAccessToken(env), + }); + + const advertisers = await client.getAdvertiserInfo({ + advertiser_ids: context.advertiser_ids, + }); + + return { + advertisers, + }; + }, + }); + +// ============================================================================ +// Export all report tools +// ============================================================================ + +export const reportTools = [ + createGetReportTool, + createGetCampaignReportTool, + createGetAdGroupReportTool, + createGetAdReportTool, + createGetAdvertiserInfoTool, +]; diff --git a/tiktok-ads/shared/deco.gen.ts b/tiktok-ads/shared/deco.gen.ts new file mode 100644 index 00000000..b49e9656 --- /dev/null +++ b/tiktok-ads/shared/deco.gen.ts @@ -0,0 +1,38 @@ +// Generated types for TikTok Ads MCP + +import { z } from "zod"; + +/** + * State schema for TikTok Ads configuration. + * Users fill these values when installing the MCP. + */ +export const StateSchema = z.object({ + accessToken: z + .string() + .describe( + "TikTok Access Token - Obtenha em TikTok Developer Portal > Tools > Access Token", + ), +}); + +/** + * Inferred state type from the schema + */ +export type State = z.infer; + +/** + * Deco request context injected by the Deco runtime + * Contains the state filled by user during installation + */ +export interface DecoRequestContext { + /** State filled by user during MCP installation */ + state: State; +} + +/** + * Environment type for TikTok Ads MCP + * Extends process env with Deco runtime context + */ +export interface Env { + /** Deco request context containing user-configured state */ + DECO_REQUEST_CONTEXT: DecoRequestContext; +} diff --git a/tiktok-ads/tsconfig.json b/tiktok-ads/tsconfig.json new file mode 100644 index 00000000..34882abb --- /dev/null +++ b/tiktok-ads/tsconfig.json @@ -0,0 +1,36 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "paths": { + "@decocms/mcps-shared/*": ["../shared/*"] + }, + "types": ["bun-types"] + }, + "include": [ + "server/**/*.ts", + "shared/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist" + ] +} +