feat(google-ads): add google ads integration for campaign and ad performance queries (#3360)

* feat(google-ads): add google ads integration for campaign and ad performance queries

* fix(google-ads): add input validation for GAQL query parameters

* fix(google-ads): remove deprecated pageSize param, fix searchSettings nesting, add missing date ranges

* fix(google-ads): validate managerCustomerId before use in login-customer-id header

* chore(docs): regenerate docs after google ads integration

* fix(google-ads): use centralized scope utilities and add type re-export

- Replace hardcoded scopes in auth.ts with getCanonicalScopesForProvider('google-ads')
- Replace hardcoded requiredScopes in block with getScopesForService('google-ads')
- Add type re-export from index.ts barrel

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(google-ads): add userinfo scopes to oauth provider config

Align google-ads with all other Google services by including
userinfo.email and userinfo.profile scopes in the centralized
OAUTH_PROVIDERS config.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* lint

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Waleed
2026-03-12 21:08:58 -07:00
committed by GitHub
parent 72bb7e6945
commit a8bbab2d21
20 changed files with 1745 additions and 0 deletions

View File

@@ -3572,6 +3572,27 @@ export const ResendIcon = (props: SVGProps<SVGSVGElement>) => (
</svg>
)
export const GoogleAdsIcon = (props: SVGProps<SVGSVGElement>) => (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'>
<g transform='matrix(.257748 0 0 .257745 -.361416 2.515516)'>
<path
d='M85.9 28.6c2.4-6.3 5.7-12.1 10.6-16.8 19.6-19.1 52-14.3 65.3 9.7 10 18.2 20.6 36 30.9 54l51.6 89.8c14.3 25.1-1.2 56.8-29.6 61.1-17.4 2.6-33.7-5.4-42.7-21l-45.4-78.8c-.3-.6-.7-1.1-1.1-1.6-1.6-1.3-2.3-3.2-3.3-4.9L88.8 62.2c-3.9-6.8-5.7-14.2-5.5-22 .3-4 .8-8 2.6-11.6'
fill='#3c8bd9'
/>
<path
d='M85.9 28.6c-.9 3.6-1.7 7.2-1.9 11-.3 8.4 1.8 16.2 6 23.5l32.9 56.9c1 1.7 1.8 3.4 2.8 5l-18.1 31.1-25.3 43.6c-.4 0-.5-.2-.6-.5-.1-.8.2-1.5.4-2.3 4.1-15 .7-28.3-9.6-39.7-6.3-6.9-14.3-10.8-23.5-12.1-12-1.7-22.6 1.4-32.1 8.9-1.7 1.3-2.8 3.2-4.8 4.2-.4 0-.6-.2-.7-.5l14.3-24.9L85.2 29.7c.2-.4.5-.7.7-1.1'
fill='#fabc04'
/>
<path
d='M11.8 158l5.7-5.1c24.3-19.2 60.8-5.3 66.1 25.1 1.3 7.3.6 14.3-1.6 21.3-.1.6-.2 1.1-.4 1.7-.9 1.6-1.7 3.3-2.7 4.9-8.9 14.7-22 22-39.2 20.9C20 225.4 4.5 210.6 1.8 191c-1.3-9.5.6-18.4 5.5-26.6 1-1.8 2.2-3.4 3.3-5.2.5-.4.3-1.2 1.2-1.2'
fill='#34a852'
/>
<path d='M11.8 158c-.4.4-.4 1.1-1.1 1.2-.1-.7.3-1.1.7-1.6l.4.4' fill='#fabc04' />
<path d='M81.6 201c-.4-.7 0-1.2.4-1.7l.4.4-.8 1.3' fill='#e1c025' />
</g>
</svg>
)
export const GoogleBigQueryIcon = (props: SVGProps<SVGSVGElement>) => (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'>
<path

View File

@@ -51,6 +51,7 @@ import {
GitLabIcon,
GmailIcon,
GongIcon,
GoogleAdsIcon,
GoogleBigQueryIcon,
GoogleBooksIcon,
GoogleCalendarIcon,
@@ -216,6 +217,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
gitlab: GitLabIcon,
gmail_v2: GmailIcon,
gong: GongIcon,
google_ads: GoogleAdsIcon,
google_bigquery: GoogleBigQueryIcon,
google_books: GoogleBooksIcon,
google_calendar_v2: GoogleCalendarIcon,

View File

@@ -0,0 +1,192 @@
---
title: Google Ads
description: Query campaigns, ad groups, and performance metrics
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="google_ads"
color="#E0E0E0"
/>
{/* MANUAL-CONTENT-START:intro */}
[Google Ads](https://ads.google.com) is Google's online advertising platform that lets businesses create ads to reach customers across Google Search, YouTube, Gmail, and millions of partner websites. It supports campaign types including Search, Display, Video, Shopping, and Performance Max, with detailed targeting, bidding strategies, and performance analytics.
In Sim, the Google Ads integration enables your agents to query campaign data, monitor ad group performance, and pull detailed metrics using the Google Ads Query Language (GAQL). This supports use cases such as automated performance reporting, budget monitoring, campaign health checks, and data-driven optimization workflows. By connecting Sim with Google Ads, your agents can retrieve real-time advertising data and act on insights without manual dashboard navigation.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Connect to Google Ads to list accessible accounts, list campaigns, view ad group details, get performance metrics, and run custom GAQL queries.
## Tools
### `google_ads_list_customers`
List all Google Ads customer accounts accessible by the authenticated user
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `developerToken` | string | Yes | Google Ads API developer token |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `customerIds` | array | List of accessible customer IDs |
| `totalCount` | number | Total number of accessible customer accounts |
### `google_ads_search`
Run a custom Google Ads Query Language (GAQL) query
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `customerId` | string | Yes | Google Ads customer ID \(numeric, no dashes\) |
| `developerToken` | string | Yes | Google Ads API developer token |
| `managerCustomerId` | string | No | Manager account customer ID \(if accessing via manager account\) |
| `query` | string | Yes | GAQL query to execute |
| `pageToken` | string | No | Page token for pagination |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `results` | json | Array of result objects from the GAQL query |
| `totalResultsCount` | number | Total number of matching results |
| `nextPageToken` | string | Token for the next page of results |
### `google_ads_list_campaigns`
List campaigns in a Google Ads account with optional status filtering
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `customerId` | string | Yes | Google Ads customer ID \(numeric, no dashes\) |
| `developerToken` | string | Yes | Google Ads API developer token |
| `managerCustomerId` | string | No | Manager account customer ID \(if accessing via manager account\) |
| `status` | string | No | Filter by campaign status \(ENABLED, PAUSED, REMOVED\) |
| `limit` | number | No | Maximum number of campaigns to return |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `campaigns` | array | List of campaigns in the account |
| ↳ `id` | string | Campaign ID |
| ↳ `name` | string | Campaign name |
| ↳ `status` | string | Campaign status \(ENABLED, PAUSED, REMOVED\) |
| ↳ `channelType` | string | Advertising channel type \(SEARCH, DISPLAY, SHOPPING, VIDEO, PERFORMANCE_MAX\) |
| ↳ `startDate` | string | Campaign start date \(YYYY-MM-DD\) |
| ↳ `endDate` | string | Campaign end date \(YYYY-MM-DD\) |
| ↳ `budgetAmountMicros` | string | Daily budget in micros \(divide by 1,000,000 for currency value\) |
| `totalCount` | number | Total number of campaigns returned |
### `google_ads_campaign_performance`
Get performance metrics for Google Ads campaigns over a date range
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `customerId` | string | Yes | Google Ads customer ID \(numeric, no dashes\) |
| `developerToken` | string | Yes | Google Ads API developer token |
| `managerCustomerId` | string | No | Manager account customer ID \(if accessing via manager account\) |
| `campaignId` | string | No | Filter by specific campaign ID |
| `dateRange` | string | No | Predefined date range \(LAST_7_DAYS, LAST_30_DAYS, THIS_MONTH, LAST_MONTH, TODAY, YESTERDAY\) |
| `startDate` | string | No | Custom start date in YYYY-MM-DD format |
| `endDate` | string | No | Custom end date in YYYY-MM-DD format |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `campaigns` | array | Campaign performance data broken down by date |
| ↳ `id` | string | Campaign ID |
| ↳ `name` | string | Campaign name |
| ↳ `status` | string | Campaign status |
| ↳ `impressions` | string | Number of impressions |
| ↳ `clicks` | string | Number of clicks |
| ↳ `costMicros` | string | Cost in micros \(divide by 1,000,000 for currency value\) |
| ↳ `ctr` | number | Click-through rate \(0.0 to 1.0\) |
| ↳ `conversions` | number | Number of conversions |
| ↳ `date` | string | Date for this row \(YYYY-MM-DD\) |
| `totalCount` | number | Total number of result rows |
### `google_ads_list_ad_groups`
List ad groups in a Google Ads campaign
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `customerId` | string | Yes | Google Ads customer ID \(numeric, no dashes\) |
| `developerToken` | string | Yes | Google Ads API developer token |
| `managerCustomerId` | string | No | Manager account customer ID \(if accessing via manager account\) |
| `campaignId` | string | Yes | Campaign ID to list ad groups for |
| `status` | string | No | Filter by ad group status \(ENABLED, PAUSED, REMOVED\) |
| `limit` | number | No | Maximum number of ad groups to return |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `adGroups` | array | List of ad groups in the campaign |
| ↳ `id` | string | Ad group ID |
| ↳ `name` | string | Ad group name |
| ↳ `status` | string | Ad group status \(ENABLED, PAUSED, REMOVED\) |
| ↳ `type` | string | Ad group type \(SEARCH_STANDARD, DISPLAY_STANDARD, SHOPPING_PRODUCT_ADS\) |
| ↳ `campaignId` | string | Parent campaign ID |
| ↳ `campaignName` | string | Parent campaign name |
| `totalCount` | number | Total number of ad groups returned |
### `google_ads_ad_performance`
Get performance metrics for individual ads over a date range
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `customerId` | string | Yes | Google Ads customer ID \(numeric, no dashes\) |
| `developerToken` | string | Yes | Google Ads API developer token |
| `managerCustomerId` | string | No | Manager account customer ID \(if accessing via manager account\) |
| `campaignId` | string | No | Filter by campaign ID |
| `adGroupId` | string | No | Filter by ad group ID |
| `dateRange` | string | No | Predefined date range \(LAST_7_DAYS, LAST_30_DAYS, THIS_MONTH, LAST_MONTH, TODAY, YESTERDAY\) |
| `startDate` | string | No | Custom start date in YYYY-MM-DD format |
| `endDate` | string | No | Custom end date in YYYY-MM-DD format |
| `limit` | number | No | Maximum number of results to return |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ads` | array | Ad performance data broken down by date |
| ↳ `adId` | string | Ad ID |
| ↳ `adGroupId` | string | Parent ad group ID |
| ↳ `adGroupName` | string | Parent ad group name |
| ↳ `campaignId` | string | Parent campaign ID |
| ↳ `campaignName` | string | Parent campaign name |
| ↳ `adType` | string | Ad type \(RESPONSIVE_SEARCH_AD, EXPANDED_TEXT_AD, etc.\) |
| ↳ `impressions` | string | Number of impressions |
| ↳ `clicks` | string | Number of clicks |
| ↳ `costMicros` | string | Cost in micros \(divide by 1,000,000 for currency value\) |
| ↳ `ctr` | number | Click-through rate \(0.0 to 1.0\) |
| ↳ `conversions` | number | Number of conversions |
| ↳ `date` | string | Date for this row \(YYYY-MM-DD\) |
| `totalCount` | number | Total number of result rows |

View File

@@ -46,6 +46,7 @@
"gitlab",
"gmail",
"gong",
"google_ads",
"google_bigquery",
"google_books",
"google_calendar",

View File

@@ -0,0 +1,294 @@
import { GoogleAdsIcon } from '@/components/icons'
import { getScopesForService } from '@/lib/oauth/utils'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
export const GoogleAdsBlock: BlockConfig = {
type: 'google_ads',
name: 'Google Ads',
description: 'Query campaigns, ad groups, and performance metrics',
longDescription:
'Connect to Google Ads to list accessible accounts, list campaigns, view ad group details, get performance metrics, and run custom GAQL queries.',
docsLink: 'https://docs.sim.ai/tools/google_ads',
category: 'tools',
bgColor: '#E0E0E0',
icon: GoogleAdsIcon,
authMode: AuthMode.OAuth,
subBlocks: [
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
{ label: 'List Customers', id: 'list_customers' },
{ label: 'List Campaigns', id: 'list_campaigns' },
{ label: 'Campaign Performance', id: 'campaign_performance' },
{ label: 'List Ad Groups', id: 'list_ad_groups' },
{ label: 'Ad Performance', id: 'ad_performance' },
{ label: 'Custom Query (GAQL)', id: 'search' },
],
value: () => 'list_campaigns',
},
{
id: 'credential',
title: 'Google Ads Account',
type: 'oauth-input',
canonicalParamId: 'oauthCredential',
mode: 'basic',
required: true,
serviceId: 'google-ads',
requiredScopes: getScopesForService('google-ads'),
placeholder: 'Select Google Ads account',
},
{
id: 'manualCredential',
title: 'Google Ads Account',
type: 'short-input',
canonicalParamId: 'oauthCredential',
mode: 'advanced',
placeholder: 'Enter credential ID',
required: true,
},
{
id: 'developerToken',
title: 'Developer Token',
type: 'short-input',
placeholder: 'Enter your Google Ads API developer token',
required: true,
password: true,
},
{
id: 'customerId',
title: 'Customer ID',
type: 'short-input',
placeholder: 'Google Ads customer ID (no dashes)',
condition: {
field: 'operation',
value: 'list_customers',
not: true,
},
required: {
field: 'operation',
value: 'list_customers',
not: true,
},
},
{
id: 'managerCustomerId',
title: 'Manager Customer ID',
type: 'short-input',
placeholder: 'Manager account ID (optional)',
mode: 'advanced',
condition: {
field: 'operation',
value: 'list_customers',
not: true,
},
},
{
id: 'query',
title: 'GAQL Query',
type: 'long-input',
placeholder:
"SELECT campaign.id, campaign.name, metrics.impressions FROM campaign WHERE campaign.status = 'ENABLED'",
condition: { field: 'operation', value: 'search' },
required: { field: 'operation', value: 'search' },
wandConfig: {
enabled: true,
prompt: `Generate a Google Ads Query Language (GAQL) query based on the user's description.
The query should:
- Use valid GAQL syntax
- Include relevant metrics when asking about performance
- Include segments.date with a date range when using metrics
- Be efficient and well-formatted
Common resources: campaign, ad_group, ad_group_ad, keyword_view, search_term_view
Common metrics: metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.ctr, metrics.conversions
Date ranges: LAST_7_DAYS, LAST_30_DAYS, THIS_MONTH, YESTERDAY
Examples:
- "active campaigns" -> SELECT campaign.id, campaign.name, campaign.status FROM campaign WHERE campaign.status = 'ENABLED'
- "campaign spend last week" -> SELECT campaign.name, metrics.cost_micros, segments.date FROM campaign WHERE segments.date DURING LAST_7_DAYS AND campaign.status != 'REMOVED'
Return ONLY the GAQL query - no explanations, no quotes, no extra text.`,
placeholder: 'Describe the query you want to run...',
},
},
{
id: 'campaignId',
title: 'Campaign ID',
type: 'short-input',
placeholder: 'Campaign ID to filter by',
condition: {
field: 'operation',
value: ['campaign_performance', 'list_ad_groups', 'ad_performance'],
},
required: { field: 'operation', value: 'list_ad_groups' },
},
{
id: 'adGroupId',
title: 'Ad Group ID',
type: 'short-input',
placeholder: 'Ad group ID to filter by',
mode: 'advanced',
condition: { field: 'operation', value: 'ad_performance' },
},
{
id: 'status',
title: 'Status Filter',
type: 'dropdown',
options: [
{ label: 'All (except removed)', id: '' },
{ label: 'Enabled', id: 'ENABLED' },
{ label: 'Paused', id: 'PAUSED' },
],
mode: 'advanced',
condition: { field: 'operation', value: ['list_campaigns', 'list_ad_groups'] },
},
{
id: 'dateRange',
title: 'Date Range',
type: 'dropdown',
options: [
{ label: 'Last 30 Days', id: 'LAST_30_DAYS' },
{ label: 'Last 7 Days', id: 'LAST_7_DAYS' },
{ label: 'Today', id: 'TODAY' },
{ label: 'Yesterday', id: 'YESTERDAY' },
{ label: 'This Month', id: 'THIS_MONTH' },
{ label: 'Last Month', id: 'LAST_MONTH' },
{ label: 'Custom', id: 'CUSTOM' },
],
condition: { field: 'operation', value: ['campaign_performance', 'ad_performance'] },
value: () => 'LAST_30_DAYS',
},
{
id: 'startDate',
title: 'Start Date',
type: 'short-input',
placeholder: 'YYYY-MM-DD',
condition: { field: 'dateRange', value: 'CUSTOM' },
required: { field: 'dateRange', value: 'CUSTOM' },
},
{
id: 'endDate',
title: 'End Date',
type: 'short-input',
placeholder: 'YYYY-MM-DD',
condition: { field: 'dateRange', value: 'CUSTOM' },
required: { field: 'dateRange', value: 'CUSTOM' },
},
{
id: 'pageToken',
title: 'Page Token',
type: 'short-input',
placeholder: 'Pagination token',
mode: 'advanced',
condition: { field: 'operation', value: 'search' },
},
{
id: 'limit',
title: 'Limit',
type: 'short-input',
placeholder: 'Maximum results to return',
mode: 'advanced',
condition: {
field: 'operation',
value: ['list_campaigns', 'list_ad_groups', 'ad_performance'],
},
},
],
tools: {
access: [
'google_ads_list_customers',
'google_ads_search',
'google_ads_list_campaigns',
'google_ads_campaign_performance',
'google_ads_list_ad_groups',
'google_ads_ad_performance',
],
config: {
tool: (params) => `google_ads_${params.operation}`,
params: (params) => {
const { oauthCredential, dateRange, limit, ...rest } = params
const result: Record<string, unknown> = {
...rest,
oauthCredential,
}
if (dateRange && dateRange !== 'CUSTOM') {
result.dateRange = dateRange
}
if (limit !== undefined && limit !== '') {
result.limit = Number(limit)
}
return result
},
},
},
inputs: {
operation: { type: 'string', description: 'Operation to perform' },
oauthCredential: { type: 'string', description: 'Google Ads OAuth credential' },
developerToken: { type: 'string', description: 'Google Ads API developer token' },
customerId: { type: 'string', description: 'Google Ads customer ID (numeric, no dashes)' },
managerCustomerId: { type: 'string', description: 'Manager account customer ID' },
query: { type: 'string', description: 'GAQL query to execute' },
campaignId: { type: 'string', description: 'Campaign ID to filter by' },
adGroupId: { type: 'string', description: 'Ad group ID to filter by' },
status: { type: 'string', description: 'Status filter (ENABLED, PAUSED)' },
dateRange: { type: 'string', description: 'Date range for performance queries' },
startDate: { type: 'string', description: 'Custom start date (YYYY-MM-DD)' },
endDate: { type: 'string', description: 'Custom end date (YYYY-MM-DD)' },
pageToken: { type: 'string', description: 'Pagination token' },
limit: { type: 'number', description: 'Maximum results to return' },
},
outputs: {
customerIds: {
type: 'json',
description: 'List of accessible customer IDs (list_customers)',
},
results: {
type: 'json',
description: 'Query results (search)',
},
campaigns: {
type: 'json',
description: 'Campaign data (list_campaigns, campaign_performance)',
},
adGroups: {
type: 'json',
description: 'Ad group data (list_ad_groups)',
},
ads: {
type: 'json',
description: 'Ad performance data (ad_performance)',
},
totalCount: {
type: 'number',
description: 'Total number of results',
},
totalResultsCount: {
type: 'number',
description: 'Total results count (search)',
},
nextPageToken: {
type: 'string',
description: 'Token for next page of results',
},
},
}

View File

@@ -52,6 +52,7 @@ import { GitLabBlock } from '@/blocks/blocks/gitlab'
import { GmailBlock, GmailV2Block } from '@/blocks/blocks/gmail'
import { GongBlock } from '@/blocks/blocks/gong'
import { GoogleSearchBlock } from '@/blocks/blocks/google'
import { GoogleAdsBlock } from '@/blocks/blocks/google_ads'
import { GoogleBigQueryBlock } from '@/blocks/blocks/google_bigquery'
import { GoogleBooksBlock } from '@/blocks/blocks/google_books'
import { GoogleCalendarBlock, GoogleCalendarV2Block } from '@/blocks/blocks/google_calendar'
@@ -257,6 +258,7 @@ export const registry: Record<string, BlockConfig> = {
gmail_v2: GmailV2Block,
google_calendar: GoogleCalendarBlock,
google_calendar_v2: GoogleCalendarV2Block,
google_ads: GoogleAdsBlock,
google_books: GoogleBooksBlock,
google_contacts: GoogleContactsBlock,
google_docs: GoogleDocsBlock,

View File

@@ -3572,6 +3572,27 @@ export const ResendIcon = (props: SVGProps<SVGSVGElement>) => (
</svg>
)
export const GoogleAdsIcon = (props: SVGProps<SVGSVGElement>) => (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'>
<g transform='matrix(.257748 0 0 .257745 -.361416 2.515516)'>
<path
d='M85.9 28.6c2.4-6.3 5.7-12.1 10.6-16.8 19.6-19.1 52-14.3 65.3 9.7 10 18.2 20.6 36 30.9 54l51.6 89.8c14.3 25.1-1.2 56.8-29.6 61.1-17.4 2.6-33.7-5.4-42.7-21l-45.4-78.8c-.3-.6-.7-1.1-1.1-1.6-1.6-1.3-2.3-3.2-3.3-4.9L88.8 62.2c-3.9-6.8-5.7-14.2-5.5-22 .3-4 .8-8 2.6-11.6'
fill='#3c8bd9'
/>
<path
d='M85.9 28.6c-.9 3.6-1.7 7.2-1.9 11-.3 8.4 1.8 16.2 6 23.5l32.9 56.9c1 1.7 1.8 3.4 2.8 5l-18.1 31.1-25.3 43.6c-.4 0-.5-.2-.6-.5-.1-.8.2-1.5.4-2.3 4.1-15 .7-28.3-9.6-39.7-6.3-6.9-14.3-10.8-23.5-12.1-12-1.7-22.6 1.4-32.1 8.9-1.7 1.3-2.8 3.2-4.8 4.2-.4 0-.6-.2-.7-.5l14.3-24.9L85.2 29.7c.2-.4.5-.7.7-1.1'
fill='#fabc04'
/>
<path
d='M11.8 158l5.7-5.1c24.3-19.2 60.8-5.3 66.1 25.1 1.3 7.3.6 14.3-1.6 21.3-.1.6-.2 1.1-.4 1.7-.9 1.6-1.7 3.3-2.7 4.9-8.9 14.7-22 22-39.2 20.9C20 225.4 4.5 210.6 1.8 191c-1.3-9.5.6-18.4 5.5-26.6 1-1.8 2.2-3.4 3.3-5.2.5-.4.3-1.2 1.2-1.2'
fill='#34a852'
/>
<path d='M11.8 158c-.4.4-.4 1.1-1.1 1.2-.1-.7.3-1.1.7-1.6l.4.4' fill='#fabc04' />
<path d='M81.6 201c-.4-.7 0-1.2.4-1.7l.4.4-.8 1.3' fill='#e1c025' />
</g>
</svg>
)
export const GoogleBigQueryIcon = (props: SVGProps<SVGSVGElement>) => (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'>
<path

View File

@@ -486,6 +486,7 @@ export const auth = betterAuth({
'google-docs',
'google-sheets',
'google-forms',
'google-ads',
'google-bigquery',
'google-vault',
'google-groups',
@@ -1008,6 +1009,41 @@ export const auth = betterAuth({
}
},
},
{
providerId: 'google-ads',
clientId: env.GOOGLE_CLIENT_ID as string,
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
accessType: 'offline',
scopes: getCanonicalScopesForProvider('google-ads'),
prompt: 'consent',
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-ads`,
getUserInfo: async (tokens) => {
try {
const response = await fetch('https://openidconnect.googleapis.com/v1/userinfo', {
headers: { Authorization: `Bearer ${tokens.accessToken}` },
})
if (!response.ok) {
logger.error('Failed to fetch Google user info', { status: response.status })
throw new Error(`Failed to fetch Google user info: ${response.statusText}`)
}
const profile = await response.json()
const now = new Date()
return {
id: `${profile.sub}-${crypto.randomUUID()}`,
name: profile.name || 'Google User',
email: profile.email,
image: profile.picture || undefined,
emailVerified: profile.email_verified || false,
createdAt: now,
updatedAt: now,
}
} catch (error) {
logger.error('Error in Google getUserInfo', { error })
throw error
}
},
},
{
providerId: 'google-bigquery',
clientId: env.GOOGLE_CLIENT_ID as string,

View File

@@ -7,6 +7,7 @@ import {
ConfluenceIcon,
DropboxIcon,
GmailIcon,
GoogleAdsIcon,
GoogleBigQueryIcon,
GoogleCalendarIcon,
GoogleContactsIcon,
@@ -146,6 +147,18 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
'https://www.googleapis.com/auth/contacts',
],
},
'google-ads': {
name: 'Google Ads',
description: 'Query campaigns, ad groups, and performance metrics in Google Ads.',
providerId: 'google-ads',
icon: GoogleAdsIcon,
baseProviderIcon: GoogleIcon,
scopes: [
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/adwords',
],
},
'google-bigquery': {
name: 'Google BigQuery',
description: 'Query, list, and insert data in Google BigQuery.',

View File

@@ -8,6 +8,7 @@ export type OAuthProvider =
| 'google-sheets'
| 'google-calendar'
| 'google-contacts'
| 'google-ads'
| 'google-bigquery'
| 'google-tasks'
| 'google-vault'
@@ -55,6 +56,7 @@ export type OAuthService =
| 'google-sheets'
| 'google-calendar'
| 'google-contacts'
| 'google-ads'
| 'google-bigquery'
| 'google-tasks'
| 'google-vault'

View File

@@ -24,6 +24,7 @@ export const SCOPE_DESCRIPTIONS: Record<string, string> = {
'https://www.googleapis.com/auth/userinfo.profile': 'View basic profile info',
'https://www.googleapis.com/auth/forms.body': 'View and manage Google Forms',
'https://www.googleapis.com/auth/forms.responses.readonly': 'View responses to Google Forms',
'https://www.googleapis.com/auth/adwords': 'Manage Google Ads campaigns and reporting',
'https://www.googleapis.com/auth/bigquery': 'View and manage data in Google BigQuery',
'https://www.googleapis.com/auth/ediscovery': 'Access Google Vault for eDiscovery',
'https://www.googleapis.com/auth/devstorage.read_only': 'Read files from Google Cloud Storage',

View File

@@ -0,0 +1,211 @@
import type {
GoogleAdsAdPerformanceParams,
GoogleAdsAdPerformanceResponse,
} from '@/tools/google_ads/types'
import { validateDate, validateDateRange, validateNumericId } from '@/tools/google_ads/types'
import type { ToolConfig } from '@/tools/types'
export const googleAdsAdPerformanceTool: ToolConfig<
GoogleAdsAdPerformanceParams,
GoogleAdsAdPerformanceResponse
> = {
id: 'google_ads_ad_performance',
name: 'Google Ads Ad Performance',
description: 'Get performance metrics for individual ads over a date range',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-ads',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for the Google Ads API',
},
customerId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Ads customer ID (numeric, no dashes)',
},
developerToken: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Google Ads API developer token',
},
managerCustomerId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Manager account customer ID (if accessing via manager account)',
},
campaignId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by campaign ID',
},
adGroupId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by ad group ID',
},
dateRange: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Predefined date range (LAST_7_DAYS, LAST_30_DAYS, THIS_MONTH, LAST_MONTH, TODAY, YESTERDAY)',
},
startDate: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Custom start date in YYYY-MM-DD format',
},
endDate: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Custom end date in YYYY-MM-DD format',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results to return',
},
},
request: {
url: (params) => {
const customerId = validateNumericId(params.customerId, 'customerId')
return `https://googleads.googleapis.com/v19/customers/${customerId}/googleAds:search`
},
method: 'POST',
headers: (params) => {
const headers: Record<string, string> = {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
'developer-token': params.developerToken,
}
if (params.managerCustomerId) {
headers['login-customer-id'] = validateNumericId(
params.managerCustomerId,
'managerCustomerId'
)
}
return headers
},
body: (params) => {
let query =
'SELECT ad_group_ad.ad.id, ad_group.id, ad_group.name, campaign.id, campaign.name, ad_group_ad.ad.type, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.ctr, metrics.conversions, segments.date FROM ad_group_ad'
const conditions: string[] = ["ad_group_ad.status != 'REMOVED'"]
if (params.campaignId) {
conditions.push(`campaign.id = ${validateNumericId(params.campaignId, 'campaignId')}`)
}
if (params.adGroupId) {
conditions.push(`ad_group.id = ${validateNumericId(params.adGroupId, 'adGroupId')}`)
}
if (params.startDate && params.endDate) {
const start = validateDate(params.startDate, 'startDate')
const end = validateDate(params.endDate, 'endDate')
conditions.push(`segments.date BETWEEN '${start}' AND '${end}'`)
} else {
const dateRange = validateDateRange(params.dateRange || 'LAST_30_DAYS')
conditions.push(`segments.date DURING ${dateRange}`)
}
query += ` WHERE ${conditions.join(' AND ')}`
query += ' ORDER BY metrics.impressions DESC'
if (params.limit) {
query += ` LIMIT ${params.limit}`
}
return { query }
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
const errorMessage =
data?.error?.message ?? data?.error?.details?.[0]?.errors?.[0]?.message ?? 'Unknown error'
return {
success: false,
output: { ads: [], totalCount: 0 },
error: errorMessage,
}
}
const results = data.results ?? []
const ads = results.map((r: Record<string, any>) => ({
adId: r.adGroupAd?.ad?.id ?? '',
adGroupId: r.adGroup?.id ?? '',
adGroupName: r.adGroup?.name ?? null,
campaignId: r.campaign?.id ?? '',
campaignName: r.campaign?.name ?? null,
adType: r.adGroupAd?.ad?.type ?? null,
impressions: r.metrics?.impressions ?? '0',
clicks: r.metrics?.clicks ?? '0',
costMicros: r.metrics?.costMicros ?? '0',
ctr: r.metrics?.ctr ?? null,
conversions: r.metrics?.conversions ?? null,
date: r.segments?.date ?? null,
}))
return {
success: true,
output: {
ads,
totalCount: ads.length,
},
}
},
outputs: {
ads: {
type: 'array',
description: 'Ad performance data broken down by date',
items: {
type: 'object',
properties: {
adId: { type: 'string', description: 'Ad ID' },
adGroupId: { type: 'string', description: 'Parent ad group ID' },
adGroupName: { type: 'string', description: 'Parent ad group name' },
campaignId: { type: 'string', description: 'Parent campaign ID' },
campaignName: { type: 'string', description: 'Parent campaign name' },
adType: {
type: 'string',
description: 'Ad type (RESPONSIVE_SEARCH_AD, EXPANDED_TEXT_AD, etc.)',
},
impressions: { type: 'string', description: 'Number of impressions' },
clicks: { type: 'string', description: 'Number of clicks' },
costMicros: {
type: 'string',
description: 'Cost in micros (divide by 1,000,000 for currency value)',
},
ctr: { type: 'number', description: 'Click-through rate (0.0 to 1.0)' },
conversions: { type: 'number', description: 'Number of conversions' },
date: { type: 'string', description: 'Date for this row (YYYY-MM-DD)' },
},
},
},
totalCount: {
type: 'number',
description: 'Total number of result rows',
},
},
}

View File

@@ -0,0 +1,182 @@
import type {
GoogleAdsCampaignPerformanceParams,
GoogleAdsCampaignPerformanceResponse,
} from '@/tools/google_ads/types'
import { validateDate, validateDateRange, validateNumericId } from '@/tools/google_ads/types'
import type { ToolConfig } from '@/tools/types'
export const googleAdsCampaignPerformanceTool: ToolConfig<
GoogleAdsCampaignPerformanceParams,
GoogleAdsCampaignPerformanceResponse
> = {
id: 'google_ads_campaign_performance',
name: 'Google Ads Campaign Performance',
description: 'Get performance metrics for Google Ads campaigns over a date range',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-ads',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for the Google Ads API',
},
customerId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Ads customer ID (numeric, no dashes)',
},
developerToken: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Google Ads API developer token',
},
managerCustomerId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Manager account customer ID (if accessing via manager account)',
},
campaignId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by specific campaign ID',
},
dateRange: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Predefined date range (LAST_7_DAYS, LAST_30_DAYS, THIS_MONTH, LAST_MONTH, TODAY, YESTERDAY)',
},
startDate: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Custom start date in YYYY-MM-DD format',
},
endDate: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Custom end date in YYYY-MM-DD format',
},
},
request: {
url: (params) => {
const customerId = validateNumericId(params.customerId, 'customerId')
return `https://googleads.googleapis.com/v19/customers/${customerId}/googleAds:search`
},
method: 'POST',
headers: (params) => {
const headers: Record<string, string> = {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
'developer-token': params.developerToken,
}
if (params.managerCustomerId) {
headers['login-customer-id'] = validateNumericId(
params.managerCustomerId,
'managerCustomerId'
)
}
return headers
},
body: (params) => {
let query =
'SELECT campaign.id, campaign.name, campaign.status, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.ctr, metrics.conversions, segments.date FROM campaign'
const conditions: string[] = ["campaign.status != 'REMOVED'"]
if (params.campaignId) {
conditions.push(`campaign.id = ${validateNumericId(params.campaignId, 'campaignId')}`)
}
if (params.startDate && params.endDate) {
const start = validateDate(params.startDate, 'startDate')
const end = validateDate(params.endDate, 'endDate')
conditions.push(`segments.date BETWEEN '${start}' AND '${end}'`)
} else {
const dateRange = validateDateRange(params.dateRange || 'LAST_30_DAYS')
conditions.push(`segments.date DURING ${dateRange}`)
}
query += ` WHERE ${conditions.join(' AND ')}`
query += ' ORDER BY metrics.impressions DESC'
return { query }
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
const errorMessage =
data?.error?.message ?? data?.error?.details?.[0]?.errors?.[0]?.message ?? 'Unknown error'
return {
success: false,
output: { campaigns: [], totalCount: 0 },
error: errorMessage,
}
}
const results = data.results ?? []
const campaigns = results.map((r: Record<string, any>) => ({
id: r.campaign?.id ?? '',
name: r.campaign?.name ?? '',
status: r.campaign?.status ?? '',
impressions: r.metrics?.impressions ?? '0',
clicks: r.metrics?.clicks ?? '0',
costMicros: r.metrics?.costMicros ?? '0',
ctr: r.metrics?.ctr ?? null,
conversions: r.metrics?.conversions ?? null,
date: r.segments?.date ?? null,
}))
return {
success: true,
output: {
campaigns,
totalCount: campaigns.length,
},
}
},
outputs: {
campaigns: {
type: 'array',
description: 'Campaign performance data broken down by date',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Campaign ID' },
name: { type: 'string', description: 'Campaign name' },
status: { type: 'string', description: 'Campaign status' },
impressions: { type: 'string', description: 'Number of impressions' },
clicks: { type: 'string', description: 'Number of clicks' },
costMicros: {
type: 'string',
description: 'Cost in micros (divide by 1,000,000 for currency value)',
},
ctr: { type: 'number', description: 'Click-through rate (0.0 to 1.0)' },
conversions: { type: 'number', description: 'Number of conversions' },
date: { type: 'string', description: 'Date for this row (YYYY-MM-DD)' },
},
},
},
totalCount: {
type: 'number',
description: 'Total number of result rows',
},
},
}

View File

@@ -0,0 +1,17 @@
import { googleAdsAdPerformanceTool } from '@/tools/google_ads/ad_performance'
import { googleAdsCampaignPerformanceTool } from '@/tools/google_ads/campaign_performance'
import { googleAdsListAdGroupsTool } from '@/tools/google_ads/list_ad_groups'
import { googleAdsListCampaignsTool } from '@/tools/google_ads/list_campaigns'
import { googleAdsListCustomersTool } from '@/tools/google_ads/list_customers'
import { googleAdsSearchTool } from '@/tools/google_ads/search'
export {
googleAdsAdPerformanceTool,
googleAdsCampaignPerformanceTool,
googleAdsListAdGroupsTool,
googleAdsListCampaignsTool,
googleAdsListCustomersTool,
googleAdsSearchTool,
}
export * from './types'

View File

@@ -0,0 +1,167 @@
import type {
GoogleAdsListAdGroupsParams,
GoogleAdsListAdGroupsResponse,
} from '@/tools/google_ads/types'
import { validateNumericId, validateStatus } from '@/tools/google_ads/types'
import type { ToolConfig } from '@/tools/types'
export const googleAdsListAdGroupsTool: ToolConfig<
GoogleAdsListAdGroupsParams,
GoogleAdsListAdGroupsResponse
> = {
id: 'google_ads_list_ad_groups',
name: 'List Google Ads Ad Groups',
description: 'List ad groups in a Google Ads campaign',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-ads',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for the Google Ads API',
},
customerId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Ads customer ID (numeric, no dashes)',
},
developerToken: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Google Ads API developer token',
},
managerCustomerId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Manager account customer ID (if accessing via manager account)',
},
campaignId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Campaign ID to list ad groups for',
},
status: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by ad group status (ENABLED, PAUSED, REMOVED)',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of ad groups to return',
},
},
request: {
url: (params) => {
const customerId = validateNumericId(params.customerId, 'customerId')
return `https://googleads.googleapis.com/v19/customers/${customerId}/googleAds:search`
},
method: 'POST',
headers: (params) => {
const headers: Record<string, string> = {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
'developer-token': params.developerToken,
}
if (params.managerCustomerId) {
headers['login-customer-id'] = validateNumericId(
params.managerCustomerId,
'managerCustomerId'
)
}
return headers
},
body: (params) => {
let query =
'SELECT ad_group.id, ad_group.name, ad_group.status, ad_group.type, campaign.id, campaign.name FROM ad_group'
const campaignId = validateNumericId(params.campaignId, 'campaignId')
const conditions: string[] = [`campaign.id = ${campaignId}`]
if (params.status) {
conditions.push(`ad_group.status = '${validateStatus(params.status)}'`)
} else {
conditions.push("ad_group.status != 'REMOVED'")
}
query += ` WHERE ${conditions.join(' AND ')}`
query += ' ORDER BY ad_group.name'
if (params.limit) {
query += ` LIMIT ${params.limit}`
}
return { query }
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
const errorMessage =
data?.error?.message ?? data?.error?.details?.[0]?.errors?.[0]?.message ?? 'Unknown error'
return {
success: false,
output: { adGroups: [], totalCount: 0 },
error: errorMessage,
}
}
const results = data.results ?? []
const adGroups = results.map((r: Record<string, any>) => ({
id: r.adGroup?.id ?? '',
name: r.adGroup?.name ?? '',
status: r.adGroup?.status ?? '',
type: r.adGroup?.type ?? null,
campaignId: r.campaign?.id ?? '',
campaignName: r.campaign?.name ?? null,
}))
return {
success: true,
output: {
adGroups,
totalCount: adGroups.length,
},
}
},
outputs: {
adGroups: {
type: 'array',
description: 'List of ad groups in the campaign',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Ad group ID' },
name: { type: 'string', description: 'Ad group name' },
status: { type: 'string', description: 'Ad group status (ENABLED, PAUSED, REMOVED)' },
type: {
type: 'string',
description: 'Ad group type (SEARCH_STANDARD, DISPLAY_STANDARD, SHOPPING_PRODUCT_ADS)',
},
campaignId: { type: 'string', description: 'Parent campaign ID' },
campaignName: { type: 'string', description: 'Parent campaign name' },
},
},
},
totalCount: {
type: 'number',
description: 'Total number of ad groups returned',
},
},
}

View File

@@ -0,0 +1,168 @@
import type {
GoogleAdsListCampaignsParams,
GoogleAdsListCampaignsResponse,
} from '@/tools/google_ads/types'
import { validateNumericId, validateStatus } from '@/tools/google_ads/types'
import type { ToolConfig } from '@/tools/types'
export const googleAdsListCampaignsTool: ToolConfig<
GoogleAdsListCampaignsParams,
GoogleAdsListCampaignsResponse
> = {
id: 'google_ads_list_campaigns',
name: 'List Google Ads Campaigns',
description: 'List campaigns in a Google Ads account with optional status filtering',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-ads',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for the Google Ads API',
},
customerId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Ads customer ID (numeric, no dashes)',
},
developerToken: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Google Ads API developer token',
},
managerCustomerId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Manager account customer ID (if accessing via manager account)',
},
status: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by campaign status (ENABLED, PAUSED, REMOVED)',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of campaigns to return',
},
},
request: {
url: (params) => {
const customerId = validateNumericId(params.customerId, 'customerId')
return `https://googleads.googleapis.com/v19/customers/${customerId}/googleAds:search`
},
method: 'POST',
headers: (params) => {
const headers: Record<string, string> = {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
'developer-token': params.developerToken,
}
if (params.managerCustomerId) {
headers['login-customer-id'] = validateNumericId(
params.managerCustomerId,
'managerCustomerId'
)
}
return headers
},
body: (params) => {
let query =
'SELECT campaign.id, campaign.name, campaign.status, campaign.advertising_channel_type, campaign.start_date, campaign.end_date, campaign_budget.amount_micros FROM campaign'
const conditions: string[] = []
if (params.status) {
conditions.push(`campaign.status = '${validateStatus(params.status)}'`)
} else {
conditions.push("campaign.status != 'REMOVED'")
}
if (conditions.length > 0) {
query += ` WHERE ${conditions.join(' AND ')}`
}
query += ' ORDER BY campaign.name'
if (params.limit) {
query += ` LIMIT ${params.limit}`
}
return { query }
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
const errorMessage =
data?.error?.message ?? data?.error?.details?.[0]?.errors?.[0]?.message ?? 'Unknown error'
return {
success: false,
output: { campaigns: [], totalCount: 0 },
error: errorMessage,
}
}
const results = data.results ?? []
const campaigns = results.map((r: Record<string, any>) => ({
id: r.campaign?.id ?? '',
name: r.campaign?.name ?? '',
status: r.campaign?.status ?? '',
channelType: r.campaign?.advertisingChannelType ?? null,
startDate: r.campaign?.startDate ?? null,
endDate: r.campaign?.endDate ?? null,
budgetAmountMicros: r.campaignBudget?.amountMicros ?? null,
}))
return {
success: true,
output: {
campaigns,
totalCount: campaigns.length,
},
}
},
outputs: {
campaigns: {
type: 'array',
description: 'List of campaigns in the account',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Campaign ID' },
name: { type: 'string', description: 'Campaign name' },
status: { type: 'string', description: 'Campaign status (ENABLED, PAUSED, REMOVED)' },
channelType: {
type: 'string',
description:
'Advertising channel type (SEARCH, DISPLAY, SHOPPING, VIDEO, PERFORMANCE_MAX)',
},
startDate: { type: 'string', description: 'Campaign start date (YYYY-MM-DD)' },
endDate: { type: 'string', description: 'Campaign end date (YYYY-MM-DD)' },
budgetAmountMicros: {
type: 'string',
description: 'Daily budget in micros (divide by 1,000,000 for currency value)',
},
},
},
},
totalCount: {
type: 'number',
description: 'Total number of campaigns returned',
},
},
}

View File

@@ -0,0 +1,84 @@
import type {
GoogleAdsListCustomersParams,
GoogleAdsListCustomersResponse,
} from '@/tools/google_ads/types'
import type { ToolConfig } from '@/tools/types'
export const googleAdsListCustomersTool: ToolConfig<
GoogleAdsListCustomersParams,
GoogleAdsListCustomersResponse
> = {
id: 'google_ads_list_customers',
name: 'List Google Ads Customers',
description: 'List all Google Ads customer accounts accessible by the authenticated user',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-ads',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for the Google Ads API',
},
developerToken: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Google Ads API developer token',
},
},
request: {
url: 'https://googleads.googleapis.com/v19/customers:listAccessibleCustomers',
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'developer-token': params.developerToken,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
const errorMessage =
data?.error?.message ?? data?.error?.details?.[0]?.errors?.[0]?.message ?? 'Unknown error'
return {
success: false,
output: { customerIds: [], totalCount: 0 },
error: errorMessage,
}
}
const resourceNames: string[] = data.resourceNames ?? []
const customerIds = resourceNames.map((rn: string) => rn.replace('customers/', ''))
return {
success: true,
output: {
customerIds,
totalCount: customerIds.length,
},
}
},
outputs: {
customerIds: {
type: 'array',
description: 'List of accessible customer IDs',
items: {
type: 'string',
description: 'Google Ads customer ID (numeric, no dashes)',
},
},
totalCount: {
type: 'number',
description: 'Total number of accessible customer accounts',
},
},
}

View File

@@ -0,0 +1,130 @@
import type { GoogleAdsSearchParams, GoogleAdsSearchResponse } from '@/tools/google_ads/types'
import { validateNumericId } from '@/tools/google_ads/types'
import type { ToolConfig } from '@/tools/types'
export const googleAdsSearchTool: ToolConfig<GoogleAdsSearchParams, GoogleAdsSearchResponse> = {
id: 'google_ads_search',
name: 'Google Ads Search (GAQL)',
description: 'Run a custom Google Ads Query Language (GAQL) query',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-ads',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for the Google Ads API',
},
customerId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Ads customer ID (numeric, no dashes)',
},
developerToken: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Google Ads API developer token',
},
managerCustomerId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Manager account customer ID (if accessing via manager account)',
},
query: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'GAQL query to execute',
},
pageToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Page token for pagination',
},
},
request: {
url: (params) => {
const customerId = validateNumericId(params.customerId, 'customerId')
return `https://googleads.googleapis.com/v19/customers/${customerId}/googleAds:search`
},
method: 'POST',
headers: (params) => {
const headers: Record<string, string> = {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
'developer-token': params.developerToken,
}
if (params.managerCustomerId) {
headers['login-customer-id'] = validateNumericId(
params.managerCustomerId,
'managerCustomerId'
)
}
return headers
},
body: (params) => {
const body: Record<string, unknown> = {
query: params.query,
searchSettings: {
returnTotalResultsCount: true,
},
}
if (params.pageToken) {
body.pageToken = params.pageToken
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
const errorMessage =
data?.error?.message ?? data?.error?.details?.[0]?.errors?.[0]?.message ?? 'Unknown error'
return {
success: false,
output: {
results: [],
totalResultsCount: null,
nextPageToken: null,
},
error: errorMessage,
}
}
return {
success: true,
output: {
results: data.results ?? [],
totalResultsCount: data.totalResultsCount ? Number(data.totalResultsCount) : null,
nextPageToken: data.nextPageToken ?? null,
},
}
},
outputs: {
results: {
type: 'json',
description: 'Array of result objects from the GAQL query',
},
totalResultsCount: {
type: 'number',
description: 'Total number of matching results',
},
nextPageToken: {
type: 'string',
description: 'Token for the next page of results',
},
},
}

View File

@@ -0,0 +1,187 @@
import type { ToolResponse } from '@/tools/types'
const NUMERIC_ID_REGEX = /^\d+$/
const DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/
const VALID_STATUSES = new Set(['ENABLED', 'PAUSED', 'REMOVED'])
const VALID_DATE_RANGES = new Set([
'TODAY',
'YESTERDAY',
'LAST_7_DAYS',
'LAST_14_DAYS',
'LAST_30_DAYS',
'LAST_BUSINESS_WEEK',
'THIS_MONTH',
'LAST_MONTH',
'THIS_WEEK_SUN_TODAY',
'THIS_WEEK_MON_TODAY',
'LAST_WEEK_SUN_SAT',
'LAST_WEEK_MON_SUN',
])
/** Validates that a value is a numeric ID (digits only). */
export function validateNumericId(value: string, fieldName: string): string {
const cleaned = value.replace(/-/g, '')
if (!NUMERIC_ID_REGEX.test(cleaned)) {
throw new Error(`${fieldName} must be numeric (digits only), got: ${value}`)
}
return cleaned
}
/** Validates that a status value is a known Google Ads status. */
export function validateStatus(value: string): string {
if (!VALID_STATUSES.has(value)) {
throw new Error(`Invalid status: ${value}. Must be one of: ${[...VALID_STATUSES].join(', ')}`)
}
return value
}
/** Validates a date string is in YYYY-MM-DD format. */
export function validateDate(value: string, fieldName: string): string {
if (!DATE_REGEX.test(value)) {
throw new Error(`${fieldName} must be in YYYY-MM-DD format, got: ${value}`)
}
return value
}
/** Validates a date range is a known Google Ads predefined range. */
export function validateDateRange(value: string): string {
if (!VALID_DATE_RANGES.has(value)) {
throw new Error(
`Invalid date range: ${value}. Must be one of: ${[...VALID_DATE_RANGES].join(', ')}`
)
}
return value
}
export interface GoogleAdsBaseParams {
accessToken: string
customerId: string
developerToken: string
managerCustomerId?: string
}
export interface GoogleAdsListCustomersParams {
accessToken: string
developerToken: string
}
export interface GoogleAdsSearchParams extends GoogleAdsBaseParams {
query: string
pageToken?: string
}
export interface GoogleAdsListCampaignsParams extends GoogleAdsBaseParams {
status?: string
limit?: number
}
export interface GoogleAdsCampaignPerformanceParams extends GoogleAdsBaseParams {
campaignId?: string
dateRange?: string
startDate?: string
endDate?: string
}
export interface GoogleAdsListAdGroupsParams extends GoogleAdsBaseParams {
campaignId: string
status?: string
limit?: number
}
export interface GoogleAdsAdPerformanceParams extends GoogleAdsBaseParams {
campaignId?: string
adGroupId?: string
dateRange?: string
startDate?: string
endDate?: string
limit?: number
}
export interface GoogleAdsListCustomersResponse extends ToolResponse {
output: {
customerIds: string[]
totalCount: number
}
}
export interface GoogleAdsSearchResponse extends ToolResponse {
output: {
results: Record<string, unknown>[]
totalResultsCount: number | null
nextPageToken: string | null
}
}
export interface GoogleAdsCampaign {
id: string
name: string
status: string
channelType: string | null
startDate: string | null
endDate: string | null
budgetAmountMicros: string | null
}
export interface GoogleAdsListCampaignsResponse extends ToolResponse {
output: {
campaigns: GoogleAdsCampaign[]
totalCount: number
}
}
export interface GoogleAdsCampaignPerformance {
id: string
name: string
status: string
impressions: string
clicks: string
costMicros: string
ctr: number | null
conversions: number | null
date: string | null
}
export interface GoogleAdsCampaignPerformanceResponse extends ToolResponse {
output: {
campaigns: GoogleAdsCampaignPerformance[]
totalCount: number
}
}
export interface GoogleAdsAdGroup {
id: string
name: string
status: string
type: string | null
campaignId: string
campaignName: string | null
}
export interface GoogleAdsListAdGroupsResponse extends ToolResponse {
output: {
adGroups: GoogleAdsAdGroup[]
totalCount: number
}
}
export interface GoogleAdsAdPerformance {
adId: string
adGroupId: string
adGroupName: string | null
campaignId: string
campaignName: string | null
adType: string | null
impressions: string
clicks: string
costMicros: string
ctr: number | null
conversions: number | null
date: string | null
}
export interface GoogleAdsAdPerformanceResponse extends ToolResponse {
output: {
ads: GoogleAdsAdPerformance[]
totalCount: number
}
}

View File

@@ -728,6 +728,14 @@ import {
gongLookupPhoneTool,
} from '@/tools/gong'
import { googleSearchTool } from '@/tools/google'
import {
googleAdsAdPerformanceTool,
googleAdsCampaignPerformanceTool,
googleAdsListAdGroupsTool,
googleAdsListCampaignsTool,
googleAdsListCustomersTool,
googleAdsSearchTool,
} from '@/tools/google_ads'
import {
googleBigQueryGetTableTool,
googleBigQueryInsertRowsTool,
@@ -4038,6 +4046,12 @@ export const tools: Record<string, ToolConfig> = {
wordpress_list_users: wordpressListUsersTool,
wordpress_get_user: wordpressGetUserTool,
wordpress_search_content: wordpressSearchContentTool,
google_ads_list_customers: googleAdsListCustomersTool,
google_ads_search: googleAdsSearchTool,
google_ads_list_campaigns: googleAdsListCampaignsTool,
google_ads_campaign_performance: googleAdsCampaignPerformanceTool,
google_ads_list_ad_groups: googleAdsListAdGroupsTool,
google_ads_ad_performance: googleAdsAdPerformanceTool,
google_bigquery_query: googleBigQueryQueryTool,
google_bigquery_list_datasets: googleBigQueryListDatasetsTool,
google_bigquery_list_tables: googleBigQueryListTablesTool,