Skip to main content
Glama
martechery

Google Ads MCP Server

by martechery

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
NameRequiredDescriptionDefault
customer_idNo10-digit customer ID (no dashes). Optional.
login_customer_idNoManager account (MCC) ID to use as login-customer for this request (10 digits, no dashes). Overrides env GOOGLE_ADS_MANAGER_ACCOUNT_ID.
queryYesGAQL 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_sizeNooptional page size (1-10000)
page_tokenNooptional page token
auto_paginateNofetch multiple pages automatically
max_pagesNolimit when auto_paginate=true (1-20)
output_formatNorender formattable

Implementation Reference

  • 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;
      }
    );
  • 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;
  • 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 };
    }
  • 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);

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/martechery/mcp-google-ads-ts'

If you have feedback or need assistance with the MCP directory API, please join our Discord server