Skip to main content
Glama
martechery

Google Ads MCP Server

by martechery

get_performance

Retrieve Google Ads performance metrics for accounts, campaigns, ad groups, or ads with customizable filters, date ranges, and output formats.

Instructions

Get performance (level: account|campaign|ad_group|ad). 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.
levelYesAggregation level
daysNoDays back to query (1-365, default 30)
limitNoGAQL LIMIT (1-1000, default 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
filtersNooptional performance filters

Implementation Reference

  • Main handler for get_performance tool: manages sessions, builds and executes GAQL query for performance data at specified level (account/campaign/etc), handles pagination and formatting.
    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('get_performance', 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('get_performance', 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('get_performance', 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('get_performance', 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 days = Math.max(1, Math.min(365, Number(input.days ?? 30)));
      const limit = Math.max(1, Math.min(1000, Number(input.limit ?? 50)));
      const query = buildPerformanceQuery(input.level, days, limit, input.filters || {});
      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, pageSize, pageToken, loginCustomerId, sessionKey });
        if (!res.ok) {
          const hint = mapAdsErrorMsg(res.status, res.errorText || '');
          const lines = [`Error executing performance query (status ${res.status}): ${res.errorText || ''}`];
          if (hint) lines.push(`Hint: ${hint}`);
          logEvent('get_performance', 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 selected period." }] };
      }
      const rows = (all as any[]).map((r: any) => {
        const out = { ...r };
        const metrics = { ...(r?.metrics || {}) } as any;
        const micros = (metrics.cost_micros ?? metrics.costMicros);
        if (typeof micros === 'number') metrics.cost_units = microsToUnits(micros);
        (out as any).metrics = metrics;
        return out;
      });
      const first = rows[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(rows, null, 2) }] };
      if (fmt === 'csv') {
        const { toCsv } = await import('./utils/formatCsv.js');
        const csv = toCsv(rows, fields);
        return { content: [{ type: 'text', text: csv }] };
      }
      const table = tabulate(rows, fields);
      const lines: string[] = [
        `Performance (${input.level}) for last ${input.days ?? 30} days:`,
        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('get_performance', startTs, { sessionKey, customerId: input.customer_id, requestId: input?.request_id });
      return out;
    }
  • Registration of the get_performance tool using addTool, linking name, description, schema, and handler.
    addTool(
      server,
      "get_performance",
      "Get performance (level: account|campaign|ad_group|ad). Optional: login_customer_id (aka MCC/manager account id) overrides env.",
      GetPerformanceZ,
      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('get_performance', 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('get_performance', 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('get_performance', 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('get_performance', 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 days = Math.max(1, Math.min(365, Number(input.days ?? 30)));
        const limit = Math.max(1, Math.min(1000, Number(input.limit ?? 50)));
        const query = buildPerformanceQuery(input.level, days, limit, input.filters || {});
        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, pageSize, pageToken, loginCustomerId, sessionKey });
          if (!res.ok) {
            const hint = mapAdsErrorMsg(res.status, res.errorText || '');
            const lines = [`Error executing performance query (status ${res.status}): ${res.errorText || ''}`];
            if (hint) lines.push(`Hint: ${hint}`);
            logEvent('get_performance', 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 selected period." }] };
        }
        const rows = (all as any[]).map((r: any) => {
          const out = { ...r };
          const metrics = { ...(r?.metrics || {}) } as any;
          const micros = (metrics.cost_micros ?? metrics.costMicros);
          if (typeof micros === 'number') metrics.cost_units = microsToUnits(micros);
          (out as any).metrics = metrics;
          return out;
        });
        const first = rows[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(rows, null, 2) }] };
        if (fmt === 'csv') {
          const { toCsv } = await import('./utils/formatCsv.js');
          const csv = toCsv(rows, fields);
          return { content: [{ type: 'text', text: csv }] };
        }
        const table = tabulate(rows, fields);
        const lines: string[] = [
          `Performance (${input.level}) for last ${input.days ?? 30} days:`,
          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('get_performance', startTs, { sessionKey, customerId: input.customer_id, requestId: input?.request_id });
        return out;
      }
    );
  • Zod schema definition for get_performance input validation (GetPerformanceZ) and JSON schema export.
    export const GetPerformanceZ = 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.'),
    
      level: z.enum(['account','campaign','ad_group','ad']).describe('Aggregation level'),
      days: z.number().default(30).describe('Days back to query (1-365, default 30)'),
      limit: z.number().default(50).describe('GAQL LIMIT (1-1000, default 50)'),
      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'),
      filters: z.object({
        status: z.string().optional().describe('e.g., ENABLED, PAUSED'),
        name_contains: z.string().optional().describe('substring in entity name (case sensitive)'),
        campaign_name_contains: z.string().optional().describe('substring in campaign name (case sensitive)'),
        min_clicks: z.number().optional().describe('minimum clicks (>=0)'),
        min_impressions: z.number().optional().describe('minimum impressions (>=0)'),
      }).optional().describe('optional performance filters'),
    });
    export const GetPerformanceSchema: JsonSchema = zodToJsonSchema(GetPerformanceZ, 'GetPerformance') as unknown as JsonSchema;
  • Helper function buildPerformanceQuery generates the GAQL query string tailored to the performance level, date range, limits, and filters.
    export function buildPerformanceQuery(
      level: PerformanceLevel,
      days = 30,
      limit = 50,
      filters: PerformanceFilters = {}
    ): string {
      // Runtime bounds
      const safeDays = Math.max(1, Math.min(365, Math.floor(days)));
      const safeLimit = Math.max(1, Math.min(1000, Math.floor(limit)));
      const baseMetrics = `
        metrics.impressions,
        metrics.clicks,
        metrics.cost_micros,
        metrics.conversions,
        metrics.average_cpc,
        customer.currency_code
      `;
    
      let fields = '';
      let from = '';
      let statusField = '';
      let nameField = '';
      const campaignNameField = 'campaign.name';
      switch (level) {
        case 'account':
          fields = `
            customer.id,
            customer.descriptive_name,
            customer.currency_code,
            ${baseMetrics}
          `;
          from = 'customer';
          statusField = 'customer.status';
          nameField = 'customer.descriptive_name';
          break;
        case 'campaign':
          fields = `
            campaign.id,
            campaign.name,
            campaign.status,
            ${baseMetrics}
          `;
          from = 'campaign';
          statusField = 'campaign.status';
          nameField = 'campaign.name';
          break;
        case 'ad_group':
          fields = `
            campaign.name,
            ad_group.id,
            ad_group.name,
            ad_group.status,
            ${baseMetrics}
          `;
          from = 'ad_group';
          statusField = 'ad_group.status';
          nameField = 'ad_group.name';
          break;
        case 'ad':
          fields = `
            campaign.name,
            ad_group.name,
            ad_group_ad.ad.id,
            ad_group_ad.status,
            ${baseMetrics}
          `;
          from = 'ad_group_ad';
          statusField = 'ad_group_ad.status';
          nameField = 'ad_group.name';
          break;
        default:
          throw new Error('Invalid level. Use campaign | ad_group | ad');
      }
    
      const whereClauses: string[] = [`
        SELECT
          ${fields}
        FROM ${from}
        WHERE segments.date DURING LAST_${safeDays}_DAYS`];
    
      // Apply filters
      const esc = (v: string) => v.replace(/'/g, "''");
      if (filters.status) whereClauses.push(`AND ${statusField} = '${esc(filters.status)}'`);
      if (filters.name_contains) whereClauses.push(`AND ${nameField} LIKE '%${esc(filters.name_contains)}%'`);
      if (filters.campaign_name_contains && level !== 'account')
        whereClauses.push(`AND ${campaignNameField} LIKE '%${esc(filters.campaign_name_contains)}%'`);
      if (typeof filters.min_clicks === 'number') whereClauses.push(`AND metrics.clicks >= ${Math.max(0, Math.floor(filters.min_clicks))}`);
      if (typeof filters.min_impressions === 'number') whereClauses.push(`AND metrics.impressions >= ${Math.max(0, Math.floor(filters.min_impressions))}`);
    
      const query = `
        ${whereClauses.join('\n    ')}
        ORDER BY metrics.cost_micros DESC
        LIMIT ${safeLimit}
      `;
      return query;
    }

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