execute_gaql_query
Run GAQL queries to retrieve Google Ads data for campaigns, metrics, and account management with support for pagination and multiple output formats.
Instructions
Execute GAQL. Optional: login_customer_id (aka MCC/manager account id) overrides env.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| customer_id | No | 10-digit customer ID (no dashes). Optional. | |
| login_customer_id | No | Manager account (MCC) ID to use as login-customer for this request (10 digits, no dashes). Overrides env GOOGLE_ADS_MANAGER_ACCOUNT_ID. | |
| query | Yes | GAQL query string. Examples: SELECT campaign.id, campaign.name, metrics.clicks FROM campaign WHERE segments.date DURING LAST_30_DAYS LIMIT 10 SELECT ad_group_ad.ad.id, metrics.impressions FROM ad_group_ad WHERE campaign.status = 'ENABLED' LIMIT 50 | |
| page_size | No | optional page size (1-10000) | |
| page_token | No | optional page token | |
| auto_paginate | No | fetch multiple pages automatically | |
| max_pages | No | limit when auto_paginate=true (1-20) | |
| output_format | No | render format | table |
Implementation Reference
- src/server-tools.ts:401-507 (handler)Primary MCP tool handler for 'execute_gaql_query'. Manages sessions, rate limiting, customer selection, pagination (auto or manual), error mapping, logging, and output formatting (table/json/csv). Delegates the actual GAQL API call to the executeGaql helper.const input = (_input || {}) as any; const startTs = Date.now(); let sessionKey: string | undefined; try { sessionKey = requireSessionKeyIfEnabled(input); } catch (e: any) { const msg = e?.message || String(e); logEvent('execute_gaql_query', startTs, { sessionKey, customerId: input?.customer_id, requestId: input?.request_id, error: { code: 'ERR_INPUT', message: String(msg) } }); return { content: [{ type: 'text', text: `Error: ${msg}` }] }; } if (sessionKey) { const rc = checkRateLimit(sessionKey); if (!rc.allowed) { logEvent('execute_gaql_query', startTs, { sessionKey, customerId: input?.customer_id, requestId: input?.request_id, error: { code: 'ERR_RATE_LIMITED', message: `Retry after ${rc.retryAfter}s` } }); return { content: [{ type: 'text', text: JSON.stringify({ error: { code: 'ERR_RATE_LIMITED', message: `Rate limit exceeded. Retry after ${rc.retryAfter} seconds`, retry_after: rc.retryAfter } }) }] }; } } if (!input.customer_id) { const envAccount = process.env.GOOGLE_ADS_ACCOUNT_ID; if (envAccount) { input.customer_id = envAccount; } else { const res = await listAccessibleCustomers(sessionKey); if (!res.ok) { const hint = mapAdsErrorMsg(res.status, res.errorText || ''); const lines = [ 'No customer_id provided. Please choose an account and re-run with customer_id.', `Error listing accounts (status ${res.status}): ${res.errorText || ''}`, ]; if (hint) lines.push(`Hint: ${hint}`); logEvent('execute_gaql_query', startTs, { sessionKey, requestId: input?.request_id, error: { code: `HTTP_${res.status}`, message: String(res.errorText || '') } }); return { content: [{ type: 'text', text: lines.join('\n') }] }; } const names = res.data?.resourceNames || []; if (!names.length) return { content: [{ type: 'text', text: 'No accessible accounts found.' }] }; const rows = names.map((rn: string) => ({ account_id: (rn.split('/').pop() || rn) })); const table = tabulate(rows, ['account_id']); const lines = [ 'No customer_id provided. Select one of the accounts below, then call again with customer_id.', table, ]; logEvent('execute_gaql_query', startTs, { sessionKey, requestId: input?.request_id }); return { content: [{ type: 'text', text: lines.join('\n') }] }; } } // Enforce allowlist if present if (sessionKey && input.customer_id && !isCustomerAllowedForSession(sessionKey, input.customer_id)) { return { content: [{ type: 'text', text: `Error: Customer ID ${input.customer_id} not in allowlist for this session` }] }; } const auto = !!input.auto_paginate; const maxPages = Math.max(1, Math.min(20, Number(input.max_pages ?? 5))); const pageSize = (typeof input.page_size === 'number') ? Math.max(1, Math.min(10_000, Number(input.page_size))) : undefined; let pageToken = input.page_token as string | undefined; let all: any[] = []; let lastToken: string | undefined; let pageCount = 0; // Normalize MCC/login-customer aliases for robustness const loginCustomerId = (input as any).login_customer_id ?? (input as any).loginCustomerId ?? (input as any).managerAccountId ?? (input as any).mcc; do { const res = await executeGaql({ customerId: input.customer_id, query: input.query, pageSize, pageToken, loginCustomerId, sessionKey }); if (!res.ok) { const hint = mapAdsErrorMsg(res.status, res.errorText || ''); const lines = [`Error executing query (status ${res.status}): ${res.errorText || ''}`]; if (hint) lines.push(`Hint: ${hint}`); logEvent('execute_gaql_query', startTs, { sessionKey, customerId: input.customer_id, requestId: input?.request_id, error: { code: `HTTP_${res.status}`, message: String(res.errorText || '') } }); return { content: [{ type: "text", text: lines.join('\n') }] }; } const data = res.data; const results = (data?.results && Array.isArray(data.results)) ? data.results : []; all = all.concat(results); lastToken = data?.nextPageToken; pageToken = auto ? lastToken : undefined; pageCount++; } while (auto && pageToken && pageCount < maxPages); if (!all.length) { return { content: [{ type: "text", text: "No results found for the query." }] }; } const first = all[0]; const fields: string[] = []; for (const key of Object.keys(first)) { const val = (first as any)[key]; if (val && typeof val === "object" && !Array.isArray(val)) { for (const sub of Object.keys(val)) fields.push(`${key}.${sub}`); } else { fields.push(key); } } const fmt = (input.output_format || 'table').toLowerCase(); if (fmt === 'json') return { content: [{ type: 'text', text: JSON.stringify(all, null, 2) }] }; if (fmt === 'csv') { const { toCsv } = await import('./utils/formatCsv.js'); const csv = toCsv(all, fields); return { content: [{ type: 'text', text: csv }] }; } const table = tabulate(all, fields); const lines: string[] = ["Query Results:", table]; if (!auto && lastToken) lines.push(`Next Page Token: ${lastToken}`); if (auto) lines.push(`Pages fetched: ${pageCount}`); const out = { content: [{ type: "text", text: lines.join("\n") }] }; logEvent('execute_gaql_query', startTs, { sessionKey, customerId: input.customer_id, requestId: input?.request_id }); return out; } );
- src/schemas.ts:24-42 (schema)Zod schema (ExecuteGaqlZ) defining input parameters and validation for the execute_gaql_query tool, including descriptions for LLM guidance.export const ExecuteGaqlZ = z.object({ customer_id: z.string().optional().describe('10-digit customer ID (no dashes). Optional.'), // Per-call login customer (MCC/manager) override login_customer_id: z.union([z.string(), z.number()]).optional().describe('Manager account (MCC) ID to use as login-customer for this request (10 digits, no dashes). Overrides env GOOGLE_ADS_MANAGER_ACCOUNT_ID.'), query: z.string().describe( [ 'GAQL query string. Examples:', "SELECT campaign.id, campaign.name, metrics.clicks FROM campaign WHERE segments.date DURING LAST_30_DAYS LIMIT 10", "SELECT ad_group_ad.ad.id, metrics.impressions FROM ad_group_ad WHERE campaign.status = 'ENABLED' LIMIT 50", ].join('\n') ), page_size: z.number().min(1).optional().describe('optional page size (1-10000)'), page_token: z.string().optional().describe('optional page token'), auto_paginate: z.boolean().default(false).describe('fetch multiple pages automatically'), max_pages: z.number().min(1).max(20).default(5).describe('limit when auto_paginate=true (1-20)'), output_format: z.enum(['table','json','csv']).default('table').describe('render format'), }); export const ExecuteGaqlSchema: JsonSchema = zodToJsonSchema(ExecuteGaqlZ, 'ExecuteGaql') as unknown as JsonSchema;
- src/tools/gaql.ts:25-56 (helper)Core helper function executeGaql that performs the HTTP POST to Google Ads API /googleAds:search endpoint, handling auth headers, pagination params, and response parsing.export async function executeGaql({ customerId, query, pageSize, pageToken, loginCustomerId, sessionKey }: ExecuteGaqlParams): Promise<GaqlResponse> { const { token, quotaProjectId, developerToken: devFromToken } = await getAccessToken(sessionKey); const developerToken = devFromToken || process.env.GOOGLE_ADS_DEVELOPER_TOKEN || ''; const loginCustomerHeader = (loginCustomerId != null ? String(loginCustomerId) : process.env.GOOGLE_ADS_MANAGER_ACCOUNT_ID); const headers = buildAdsHeaders({ accessToken: token, developerToken, loginCustomerId: loginCustomerHeader, quotaProjectId, }); const url = `https://googleads.googleapis.com/${API_VERSION}/customers/${formatCustomerId(customerId)}/googleAds:search`; const body: any = { query }; if (typeof pageSize === 'number' && pageSize > 0) body.pageSize = pageSize; if (pageToken) body.pageToken = pageToken; const init: RequestInit = { method: 'POST', headers, body: JSON.stringify(body), }; const res = await fetch(url, init); const contentType = res.headers?.get?.('content-type') || ''; if (res.ok) { const data = contentType.includes('application/json') ? await res.json() : await res.text(); return { ok: true, status: res.status, data }; } const errorText = await res.text(); return { ok: false, status: res.status, errorText }; }
- src/server-tools.ts:396-508 (registration)Tool registration via addTool in registerTools function, specifying name, description, input schema, and handler.server, "execute_gaql_query", "Execute GAQL. Optional: login_customer_id (aka MCC/manager account id) overrides env.", ExecuteGaqlZ, async (_input: any) => { const input = (_input || {}) as any; const startTs = Date.now(); let sessionKey: string | undefined; try { sessionKey = requireSessionKeyIfEnabled(input); } catch (e: any) { const msg = e?.message || String(e); logEvent('execute_gaql_query', startTs, { sessionKey, customerId: input?.customer_id, requestId: input?.request_id, error: { code: 'ERR_INPUT', message: String(msg) } }); return { content: [{ type: 'text', text: `Error: ${msg}` }] }; } if (sessionKey) { const rc = checkRateLimit(sessionKey); if (!rc.allowed) { logEvent('execute_gaql_query', startTs, { sessionKey, customerId: input?.customer_id, requestId: input?.request_id, error: { code: 'ERR_RATE_LIMITED', message: `Retry after ${rc.retryAfter}s` } }); return { content: [{ type: 'text', text: JSON.stringify({ error: { code: 'ERR_RATE_LIMITED', message: `Rate limit exceeded. Retry after ${rc.retryAfter} seconds`, retry_after: rc.retryAfter } }) }] }; } } if (!input.customer_id) { const envAccount = process.env.GOOGLE_ADS_ACCOUNT_ID; if (envAccount) { input.customer_id = envAccount; } else { const res = await listAccessibleCustomers(sessionKey); if (!res.ok) { const hint = mapAdsErrorMsg(res.status, res.errorText || ''); const lines = [ 'No customer_id provided. Please choose an account and re-run with customer_id.', `Error listing accounts (status ${res.status}): ${res.errorText || ''}`, ]; if (hint) lines.push(`Hint: ${hint}`); logEvent('execute_gaql_query', startTs, { sessionKey, requestId: input?.request_id, error: { code: `HTTP_${res.status}`, message: String(res.errorText || '') } }); return { content: [{ type: 'text', text: lines.join('\n') }] }; } const names = res.data?.resourceNames || []; if (!names.length) return { content: [{ type: 'text', text: 'No accessible accounts found.' }] }; const rows = names.map((rn: string) => ({ account_id: (rn.split('/').pop() || rn) })); const table = tabulate(rows, ['account_id']); const lines = [ 'No customer_id provided. Select one of the accounts below, then call again with customer_id.', table, ]; logEvent('execute_gaql_query', startTs, { sessionKey, requestId: input?.request_id }); return { content: [{ type: 'text', text: lines.join('\n') }] }; } } // Enforce allowlist if present if (sessionKey && input.customer_id && !isCustomerAllowedForSession(sessionKey, input.customer_id)) { return { content: [{ type: 'text', text: `Error: Customer ID ${input.customer_id} not in allowlist for this session` }] }; } const auto = !!input.auto_paginate; const maxPages = Math.max(1, Math.min(20, Number(input.max_pages ?? 5))); const pageSize = (typeof input.page_size === 'number') ? Math.max(1, Math.min(10_000, Number(input.page_size))) : undefined; let pageToken = input.page_token as string | undefined; let all: any[] = []; let lastToken: string | undefined; let pageCount = 0; // Normalize MCC/login-customer aliases for robustness const loginCustomerId = (input as any).login_customer_id ?? (input as any).loginCustomerId ?? (input as any).managerAccountId ?? (input as any).mcc; do { const res = await executeGaql({ customerId: input.customer_id, query: input.query, pageSize, pageToken, loginCustomerId, sessionKey }); if (!res.ok) { const hint = mapAdsErrorMsg(res.status, res.errorText || ''); const lines = [`Error executing query (status ${res.status}): ${res.errorText || ''}`]; if (hint) lines.push(`Hint: ${hint}`); logEvent('execute_gaql_query', startTs, { sessionKey, customerId: input.customer_id, requestId: input?.request_id, error: { code: `HTTP_${res.status}`, message: String(res.errorText || '') } }); return { content: [{ type: "text", text: lines.join('\n') }] }; } const data = res.data; const results = (data?.results && Array.isArray(data.results)) ? data.results : []; all = all.concat(results); lastToken = data?.nextPageToken; pageToken = auto ? lastToken : undefined; pageCount++; } while (auto && pageToken && pageCount < maxPages); if (!all.length) { return { content: [{ type: "text", text: "No results found for the query." }] }; } const first = all[0]; const fields: string[] = []; for (const key of Object.keys(first)) { const val = (first as any)[key]; if (val && typeof val === "object" && !Array.isArray(val)) { for (const sub of Object.keys(val)) fields.push(`${key}.${sub}`); } else { fields.push(key); } } const fmt = (input.output_format || 'table').toLowerCase(); if (fmt === 'json') return { content: [{ type: 'text', text: JSON.stringify(all, null, 2) }] }; if (fmt === 'csv') { const { toCsv } = await import('./utils/formatCsv.js'); const csv = toCsv(all, fields); return { content: [{ type: 'text', text: csv }] }; } const table = tabulate(all, fields); const lines: string[] = ["Query Results:", table]; if (!auto && lastToken) lines.push(`Next Page Token: ${lastToken}`); if (auto) lines.push(`Pages fetched: ${pageCount}`); const out = { content: [{ type: "text", text: lines.join("\n") }] }; logEvent('execute_gaql_query', startTs, { sessionKey, customerId: input.customer_id, requestId: input?.request_id }); return out; } );
- src/server.ts:13-13 (registration)Invocation of registerTools to register all tools including execute_gaql_query on the MCP server instance.registerTools(server as any);