Skip to main content
Glama
martechery

Google Ads MCP Server

by martechery

manage_auth

Check authentication status, switch between Google Ads accounts, or refresh credentials using gcloud commands and OAuth login options.

Instructions

Manage Google Ads auth: status; switch/refresh via gcloud; set_project/set_quota_project; optional oauth_login using env client id/secret to create ADC file.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
actionNostatus | switch | refreshstatus
config_nameNogcloud configuration name (for switch)
allow_subprocessNoexecute gcloud steps (default true). Set false to only print commands.

Implementation Reference

  • The core handler function implementing all logic for the 'manage_auth' tool, including auth status checks, gcloud config switches, ADC refresh, OAuth login, and subprocess execution.
    async (input: any) => {
      const startTs = Date.now();
      if (process.env.ENABLE_RUNTIME_CREDENTIALS === 'true') {
        const out = { content: [{ type: 'text', text: 'Authentication cannot be modified in multi-tenant mode (manage_auth disabled).' }] };
        logEvent('manage_auth', startTs, { requestId: input?.request_id, error: { code: 'DISABLED_MULTI_TENANT', message: 'manage_auth disabled in multi-tenant mode' } });
        return out;
      }
      const action = (input?.action || 'status').toLowerCase();
      // Default: execute subprocess actions unless explicitly disabled
      const allowSub = input?.allow_subprocess !== false;
    
      const useLocalExec = process.env.VITEST_REAL === '1';
    
      // Minimal local exec wrapper to avoid test-mocking pitfalls
      function localExec(cmd: string, args: string[], opts?: { timeoutMs?: number }): Promise<{ code: number; stdout: string; stderr: string }>{
        return new Promise((resolve) => {
          try {
            import('node:child_process').then(({ spawn }) => {
              const child = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'] });
              let out = '';
              let err = '';
              child.stdout?.on('data', (d) => { try { out += String(d); } catch { void 0; } });
              child.stderr?.on('data', (d) => { try { err += String(d); } catch { void 0; } });
              let timeout: NodeJS.Timeout | undefined;
              const finalize = (code: number) => { if (timeout) clearTimeout(timeout); resolve({ code, stdout: out, stderr: err }); };
              child.on('error', () => finalize(1));
              child.on('close', (code) => finalize(code ?? 1));
              if (opts?.timeoutMs && opts.timeoutMs > 0) {
                timeout = setTimeout(() => { try { child.kill('SIGKILL'); } catch { void 0; } }, opts.timeoutMs);
              }
            }).catch(() => resolve({ code: 1, stdout: '', stderr: '' }));
          } catch {
            resolve({ code: 1, stdout: '', stderr: '' });
          }
        });
      }
    
      async function execCmd(cmd: string, args: string[], opts?: { timeoutMs?: number }) {
        if (useLocalExec) return localExec(cmd, args, opts);
        try {
          const mod = await import('./utils/exec.js');
          const run = (mod as any).execCmd as (c: string, a: string[], o?: { timeoutMs?: number }) => Promise<{ code: number; stdout: string; stderr: string }>;
          return run(cmd, args, opts);
        } catch {
          return localExec(cmd, args, opts);
        }
      }
    
      async function isGcloudAvailable(): Promise<boolean> {
        const res = await execCmd('gcloud', ['--version'], { timeoutMs: 5_000 });
        return res.code === 0;
      }
    
      if (action === 'status') {
        const accountId = process.env.GOOGLE_ADS_ACCOUNT_ID || "(not set)";
        const managerAccountId = process.env.GOOGLE_ADS_MANAGER_ACCOUNT_ID || "(not set)";
        const developerTokenRaw = process.env.GOOGLE_ADS_DEVELOPER_TOKEN;
        const gacEnv = process.env.GOOGLE_APPLICATION_CREDENTIALS || "(not set)";
    
        const lines: string[] = [
          'Google Ads Auth Status',
          '=======================',
          'Environment:',
          `  GOOGLE_APPLICATION_CREDENTIALS: ${gacEnv}`,
          `  GOOGLE_ADS_ACCOUNT_ID: ${accountId}`,
          `  GOOGLE_ADS_MANAGER_ACCOUNT_ID: ${managerAccountId}`,
          `  GOOGLE_ADS_DEVELOPER_TOKEN: ${developerTokenRaw ? "(set)" : "(not set)"}`,
          'Notes:',
          "- ADC via gcloud is preferred for stability and auto-refresh.",
          '- You can also use an existing authorized_user JSON via GOOGLE_APPLICATION_CREDENTIALS.',
          '',
          'Probes:',
        ];
        try {
          const { token, quotaProjectId, type } = await (await import('./auth.js')).getAccessToken();
          lines.push(`  Auth type: ${type}`);
          lines.push(`  Token present: ${token ? 'yes' : 'no'}`);
          lines.push(`  Quota project: ${quotaProjectId || '(none)'}`);
          // Token info (scopes/audience)
          try {
            const infoRes = await fetch(`https://oauth2.googleapis.com/tokeninfo?access_token=${encodeURIComponent(token)}`);
            if (infoRes.ok) {
              const info: any = await infoRes.json();
              if (info.scope) lines.push(`  Token scopes: ${info.scope}`);
              if (info.aud) lines.push(`  Token audience: ${info.aud}`);
              if (info.azp) lines.push(`  Token azp: ${info.azp}`);
            } else {
              lines.push('  Token info: (unavailable)');
            }
          } catch {
            lines.push('  Token info: (error fetching)');
          }
          // Probe scope by hitting listAccessibleCustomers
          const resp = await listAccessibleCustomers();
          if (resp.ok) {
            lines.push('  Ads scope check: OK');
            const count = resp.data?.resourceNames?.length || 0;
            lines.push(`  Accessible accounts: ${count}`);
          } else if (resp.status === 403 && (resp.errorText || '').includes('ACCESS_TOKEN_SCOPE_INSUFFICIENT')) {
            lines.push('  Ads scope check: missing scope (ACCESS_TOKEN_SCOPE_INSUFFICIENT)');
          } else {
            lines.push(`  Ads scope check: HTTP ${resp.status}`);
          }
        } catch (e: any) {
          lines.push(`  Error determining auth status: ${e?.message || String(e)}`);
        }
    
        // ADC file discovery hints (including local .auth/adc.json)
        try {
          const fs = await import('node:fs');
          const path = await import('node:path');
          const os = await import('node:os');
          const hints: string[] = [];
          const envPath = process.env.GOOGLE_APPLICATION_CREDENTIALS;
          const exists = (p?: string | null) => !!p && fs.existsSync(p);
          const localPath = path.resolve(process.cwd(), '.auth', 'adc.json');
          let wellKnown: string | null = null;
          if (process.platform === 'win32') {
            const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
            wellKnown = path.join(appData, 'gcloud', 'application_default_credentials.json');
          } else {
            wellKnown = path.join(os.homedir(), '.config', 'gcloud', 'application_default_credentials.json');
          }
          if (exists(envPath)) hints.push(`  ADC file (env): ${envPath}`);
          else if (exists(localPath)) hints.push(`  ADC file (project .auth): ${localPath}`);
          else if (exists(wellKnown)) hints.push(`  ADC file (well-known): ${wellKnown}`);
          else hints.push('  ADC file: not found in env or well-known path');
          lines.push('', 'ADC file discovery:', ...hints);
          if (!exists(envPath) && !exists(wellKnown)) {
            lines.push('  Hint: Install gcloud to create ADC via browser login, or provide an authorized_user JSON and set GOOGLE_APPLICATION_CREDENTIALS to its path.');
          }
        } catch {
          // ignore discovery errors
        }
        const out = { content: [{ type: 'text', text: lines.join('\n') }] };
        logEvent('manage_auth', startTs, { requestId: input?.request_id });
        return out;
      }
    
      if (action === 'oauth_login') {
        const clientId = (process.env.GOOGLE_OAUTH_CLIENT_ID || '').trim();
        const clientSecret = (process.env.GOOGLE_OAUTH_CLIENT_SECRET || '').trim();
        if (!clientId || !clientSecret) {
          const lines = [
            'OAuth credentials not available',
            'Missing GOOGLE_OAUTH_CLIENT_ID and/or GOOGLE_OAUTH_CLIENT_SECRET.',
            'Set both env vars to a Desktop app OAuth client and retry.',
            'Preferred path remains: gcloud auth application-default login with Ads scope.',
          ];
          const out = { content: [{ type: 'text', text: lines.join('\n') }] };
          logEvent('manage_auth', startTs, { requestId: input?.request_id, error: { code: 'OAUTH_CLIENT_MISSING', message: 'Missing client id/secret' } });
          return out;
        }
        try {
          const mod = await import('./tools/oauth.js');
          const { runDeviceOAuthForAds } = mod as any;
          const out = await runDeviceOAuthForAds({ clientId, clientSecret });
          const lines = [
            'OAuth device flow completed.',
            `Saved ADC authorized_user JSON: ${out.path}`,
            '',
            'Next steps for current shell (optional):',
            `  export GOOGLE_APPLICATION_CREDENTIALS="${out.path}"`,
          ];
          // Verify scope by listing accounts
          try {
            const resp = await listAccessibleCustomers();
            if (resp.ok) {
              lines.push('Ads scope check after oauth_login: OK');
              const count = resp.data?.resourceNames?.length || 0;
              lines.push(`Accessible accounts: ${count}`);
            } else {
              lines.push(`Ads scope check after oauth_login: HTTP ${resp.status}`);
            }
          } catch { /* ignore */ }
          const resp = { content: [{ type: 'text', text: lines.join('\n') }] };
          logEvent('manage_auth', startTs, { requestId: input?.request_id });
          return resp;
        } catch (e: any) {
          const lines = [
            'OAuth device flow failed.',
            `Error: ${e?.message || String(e)}`,
          ];
          const resp = { content: [{ type: 'text', text: lines.join('\n') }] };
          logEvent('manage_auth', startTs, { requestId: input?.request_id, error: { code: 'OAUTH_DEVICE_FAILED', message: String(e?.message || e) } });
          return resp;
        }
      }
    
      if (action === 'switch') {
        const name = input?.config_name?.trim();
        if (!name) {
          return { content: [{ type: 'text', text: "Missing 'config_name'. Example: { action: 'switch', config_name: 'my-config' }" }] };
        }
        const cmd = `gcloud config configurations activate ${name}`;
        if (!allowSub) {
          const text = [
            'Planned action: switch gcloud configuration',
            `Command: ${cmd}`,
            'Tip: Re-run with allow_subprocess=true (default) to execute from MCP, or set allow_subprocess=false to only print steps.',
          ].join('\n');
          const out = { content: [{ type: 'text', text }] };
          logEvent('manage_auth', startTs, { requestId: input?.request_id });
          return out;
        }
        if (!(await isGcloudAvailable())) {
          const text = [
            'gcloud not found on PATH. Cannot execute switch automatically.',
            `Please run manually: ${cmd}`,
            'Install: https://cloud.google.com/sdk/docs/install',
          ].join('\n');
          const out = { content: [{ type: 'text', text }] };
          logEvent('manage_auth', startTs, { requestId: input?.request_id, error: { code: 'GCLOUD_NOT_FOUND', message: 'gcloud not on PATH' } });
          return out;
        }
        const { code, stdout, stderr } = await execCmd('gcloud', ['config', 'configurations', 'activate', name]);
        const lines = [
          `gcloud switch (${name}) exit: ${code}`,
          stdout ? `stdout:\n${stdout}` : '',
          stderr ? `stderr:\n${stderr}` : '',
          'Next: refresh ADC credentials to ensure Ads scope:',
          '  gcloud auth application-default login --scopes=https://www.googleapis.com/auth/cloud-platform,https://www.googleapis.com/auth/adwords',
        ].filter(Boolean);
        const out = { content: [{ type: 'text', text: lines.join('\n') }] };
        logEvent('manage_auth', startTs, { requestId: input?.request_id });
        return out;
      }
    
      if (action === 'set_project') {
        const projectId = (input?.project_id || input?.project || '').trim();
        const allowSub = input?.allow_subprocess !== false;
        if (!projectId) return { content: [{ type: 'text', text: "Missing 'project_id'. Example: { action: 'set_project', project_id: 'my-project' }" }] };
        const cmd = `gcloud config set project ${projectId}`;
        if (!allowSub) {
          const text = [
            'Planned action: set default gcloud project',
            `Command: ${cmd}`,
          ].join('\n');
          const out = { content: [{ type: 'text', text }] };
          logEvent('manage_auth', startTs, { requestId: input?.request_id });
          return out;
        }
        const { code, stdout, stderr } = await execCmd('gcloud', ['config', 'set', 'project', projectId]);
        const text = [
          `gcloud set project exit: ${code}`,
          stdout ? `stdout:\n${stdout}` : '',
          stderr ? `stderr:\n${stderr}` : '',
        ].filter(Boolean).join('\n');
        const out = { content: [{ type: 'text', text }] };
        logEvent('manage_auth', startTs, { requestId: input?.request_id });
        return out;
      }
    
      if (action === 'set_quota_project') {
        const projectId = (input?.project_id || input?.project || '').trim();
        const allowSub = input?.allow_subprocess !== false;
        if (!projectId) return { content: [{ type: 'text', text: "Missing 'project_id'. Example: { action: 'set_quota_project', project_id: 'my-project' }" }] };
        const cmd = `gcloud auth application-default set-quota-project ${projectId}`;
        if (!allowSub) {
          const text = [
            'Planned action: set ADC quota project',
            `Command: ${cmd}`,
          ].join('\n');
        const out = { content: [{ type: 'text', text }] };
        logEvent('manage_auth', startTs, { requestId: input?.request_id });
        return out;
        }
        const { code, stdout, stderr } = await execCmd('gcloud', ['auth', 'application-default', 'set-quota-project', projectId]);
        const text = [
          `gcloud set-quota-project exit: ${code}`,
          stdout ? `stdout:\n${stdout}` : '',
          stderr ? `stderr:\n${stderr}` : '',
        ].filter(Boolean).join('\n');
        const out = { content: [{ type: 'text', text }] };
        logEvent('manage_auth', startTs, { requestId: input?.request_id });
        return out;
      }
    
      if (action === 'refresh') {
        const loginCmd = 'gcloud auth application-default login --scopes=https://www.googleapis.com/auth/cloud-platform,https://www.googleapis.com/auth/adwords';
        if (!allowSub) {
          const text = [
            'Planned action: refresh ADC credentials for Ads scope',
            `Command: ${loginCmd}`,
            'Tip: Re-run with allow_subprocess=true (default) to execute from MCP, or set allow_subprocess=false to only print steps.',
          ].join('\n');
          const out = { content: [{ type: 'text', text }] };
          logEvent('manage_auth', startTs, { requestId: input?.request_id });
          return out;
        }
        if (!(await isGcloudAvailable())) {
          const text = [
            'gcloud not found on PATH. Cannot execute refresh automatically.',
            `Please run manually: ${loginCmd}`,
            'Install: https://cloud.google.com/sdk/docs/install',
          ].join('\n');
          const out = { content: [{ type: 'text', text }] };
          logEvent('manage_auth', startTs, { requestId: input?.request_id, error: { code: 'GCLOUD_NOT_FOUND', message: 'gcloud not on PATH' } });
          return out;
        }
        const step1 = await execCmd('gcloud', ['auth', 'application-default', 'login', '--scopes=https://www.googleapis.com/auth/cloud-platform,https://www.googleapis.com/auth/adwords']);
        // Verify by printing a token (will also surface scope issues)
        const step2 = await execCmd('gcloud', ['auth', 'application-default', 'print-access-token']);
        let check: any;
        try {
          const mod = await import('./tools/accounts.js');
          const fn = (mod as any).listAccessibleCustomers || listAccessibleCustomers;
          check = await fn();
        } catch { check = undefined; }
        const lines = [
          `refresh login exit: ${step1.code}`,
          step1.stdout ? `login stdout:\n${step1.stdout}` : '',
          step1.stderr ? `login stderr:\n${step1.stderr}` : '',
          `print-token exit: ${step2.code}`,
          step2.stdout ? `token (truncated): ${step2.stdout.slice(0, 12)}...` : '',
          step2.stderr ? `print-token stderr:\n${step2.stderr}` : '',
          (check && check.ok)
            ? 'Ads scope check after refresh: OK'
            : (step2.code === 0 ? 'Ads scope check after refresh: OK (token printed)' : `Ads scope check after refresh: ${check ? `HTTP ${check.status}` : 'unknown'}`),
        ].filter(Boolean);
        if (step1.code !== 0) {
          lines.push('Install: https://cloud.google.com/sdk/docs/install');
        }
        const out = { content: [{ type: 'text', text: lines.join('\n') }] };
        logEvent('manage_auth', startTs, { requestId: input?.request_id });
        return out;
      }
    
      const out = { content: [{ type: 'text', text: `Unknown action '${action}'. Use status | switch | refresh.` }] };
      logEvent('manage_auth', startTs, { requestId: input?.request_id, error: { code: 'ERR_UNKNOWN_ACTION', message: String(action) } });
      return out;
  • Registers the 'manage_auth' tool on the MCP server using addTool, specifying name, description, input schema (ManageAuthZ), and handler function.
    addTool(
      server,
      "manage_auth",
      "Manage Google Ads auth: status; switch/refresh via gcloud; set_project/set_quota_project; optional oauth_login using env client id/secret to create ADC file.",
      ManageAuthZ,
      async (input: any) => {
        const startTs = Date.now();
        if (process.env.ENABLE_RUNTIME_CREDENTIALS === 'true') {
          const out = { content: [{ type: 'text', text: 'Authentication cannot be modified in multi-tenant mode (manage_auth disabled).' }] };
          logEvent('manage_auth', startTs, { requestId: input?.request_id, error: { code: 'DISABLED_MULTI_TENANT', message: 'manage_auth disabled in multi-tenant mode' } });
          return out;
        }
        const action = (input?.action || 'status').toLowerCase();
        // Default: execute subprocess actions unless explicitly disabled
        const allowSub = input?.allow_subprocess !== false;
    
        const useLocalExec = process.env.VITEST_REAL === '1';
    
        // Minimal local exec wrapper to avoid test-mocking pitfalls
        function localExec(cmd: string, args: string[], opts?: { timeoutMs?: number }): Promise<{ code: number; stdout: string; stderr: string }>{
          return new Promise((resolve) => {
            try {
              import('node:child_process').then(({ spawn }) => {
                const child = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'] });
                let out = '';
                let err = '';
                child.stdout?.on('data', (d) => { try { out += String(d); } catch { void 0; } });
                child.stderr?.on('data', (d) => { try { err += String(d); } catch { void 0; } });
                let timeout: NodeJS.Timeout | undefined;
                const finalize = (code: number) => { if (timeout) clearTimeout(timeout); resolve({ code, stdout: out, stderr: err }); };
                child.on('error', () => finalize(1));
                child.on('close', (code) => finalize(code ?? 1));
                if (opts?.timeoutMs && opts.timeoutMs > 0) {
                  timeout = setTimeout(() => { try { child.kill('SIGKILL'); } catch { void 0; } }, opts.timeoutMs);
                }
              }).catch(() => resolve({ code: 1, stdout: '', stderr: '' }));
            } catch {
              resolve({ code: 1, stdout: '', stderr: '' });
            }
          });
        }
    
        async function execCmd(cmd: string, args: string[], opts?: { timeoutMs?: number }) {
          if (useLocalExec) return localExec(cmd, args, opts);
          try {
            const mod = await import('./utils/exec.js');
            const run = (mod as any).execCmd as (c: string, a: string[], o?: { timeoutMs?: number }) => Promise<{ code: number; stdout: string; stderr: string }>;
            return run(cmd, args, opts);
          } catch {
            return localExec(cmd, args, opts);
          }
        }
    
        async function isGcloudAvailable(): Promise<boolean> {
          const res = await execCmd('gcloud', ['--version'], { timeoutMs: 5_000 });
          return res.code === 0;
        }
    
        if (action === 'status') {
          const accountId = process.env.GOOGLE_ADS_ACCOUNT_ID || "(not set)";
          const managerAccountId = process.env.GOOGLE_ADS_MANAGER_ACCOUNT_ID || "(not set)";
          const developerTokenRaw = process.env.GOOGLE_ADS_DEVELOPER_TOKEN;
          const gacEnv = process.env.GOOGLE_APPLICATION_CREDENTIALS || "(not set)";
    
          const lines: string[] = [
            'Google Ads Auth Status',
            '=======================',
            'Environment:',
            `  GOOGLE_APPLICATION_CREDENTIALS: ${gacEnv}`,
            `  GOOGLE_ADS_ACCOUNT_ID: ${accountId}`,
            `  GOOGLE_ADS_MANAGER_ACCOUNT_ID: ${managerAccountId}`,
            `  GOOGLE_ADS_DEVELOPER_TOKEN: ${developerTokenRaw ? "(set)" : "(not set)"}`,
            'Notes:',
            "- ADC via gcloud is preferred for stability and auto-refresh.",
            '- You can also use an existing authorized_user JSON via GOOGLE_APPLICATION_CREDENTIALS.',
            '',
            'Probes:',
          ];
          try {
            const { token, quotaProjectId, type } = await (await import('./auth.js')).getAccessToken();
            lines.push(`  Auth type: ${type}`);
            lines.push(`  Token present: ${token ? 'yes' : 'no'}`);
            lines.push(`  Quota project: ${quotaProjectId || '(none)'}`);
            // Token info (scopes/audience)
            try {
              const infoRes = await fetch(`https://oauth2.googleapis.com/tokeninfo?access_token=${encodeURIComponent(token)}`);
              if (infoRes.ok) {
                const info: any = await infoRes.json();
                if (info.scope) lines.push(`  Token scopes: ${info.scope}`);
                if (info.aud) lines.push(`  Token audience: ${info.aud}`);
                if (info.azp) lines.push(`  Token azp: ${info.azp}`);
              } else {
                lines.push('  Token info: (unavailable)');
              }
            } catch {
              lines.push('  Token info: (error fetching)');
            }
            // Probe scope by hitting listAccessibleCustomers
            const resp = await listAccessibleCustomers();
            if (resp.ok) {
              lines.push('  Ads scope check: OK');
              const count = resp.data?.resourceNames?.length || 0;
              lines.push(`  Accessible accounts: ${count}`);
            } else if (resp.status === 403 && (resp.errorText || '').includes('ACCESS_TOKEN_SCOPE_INSUFFICIENT')) {
              lines.push('  Ads scope check: missing scope (ACCESS_TOKEN_SCOPE_INSUFFICIENT)');
            } else {
              lines.push(`  Ads scope check: HTTP ${resp.status}`);
            }
          } catch (e: any) {
            lines.push(`  Error determining auth status: ${e?.message || String(e)}`);
          }
    
          // ADC file discovery hints (including local .auth/adc.json)
          try {
            const fs = await import('node:fs');
            const path = await import('node:path');
            const os = await import('node:os');
            const hints: string[] = [];
            const envPath = process.env.GOOGLE_APPLICATION_CREDENTIALS;
            const exists = (p?: string | null) => !!p && fs.existsSync(p);
            const localPath = path.resolve(process.cwd(), '.auth', 'adc.json');
            let wellKnown: string | null = null;
            if (process.platform === 'win32') {
              const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
              wellKnown = path.join(appData, 'gcloud', 'application_default_credentials.json');
            } else {
              wellKnown = path.join(os.homedir(), '.config', 'gcloud', 'application_default_credentials.json');
            }
            if (exists(envPath)) hints.push(`  ADC file (env): ${envPath}`);
            else if (exists(localPath)) hints.push(`  ADC file (project .auth): ${localPath}`);
            else if (exists(wellKnown)) hints.push(`  ADC file (well-known): ${wellKnown}`);
            else hints.push('  ADC file: not found in env or well-known path');
            lines.push('', 'ADC file discovery:', ...hints);
            if (!exists(envPath) && !exists(wellKnown)) {
              lines.push('  Hint: Install gcloud to create ADC via browser login, or provide an authorized_user JSON and set GOOGLE_APPLICATION_CREDENTIALS to its path.');
            }
          } catch {
            // ignore discovery errors
          }
          const out = { content: [{ type: 'text', text: lines.join('\n') }] };
          logEvent('manage_auth', startTs, { requestId: input?.request_id });
          return out;
        }
    
        if (action === 'oauth_login') {
          const clientId = (process.env.GOOGLE_OAUTH_CLIENT_ID || '').trim();
          const clientSecret = (process.env.GOOGLE_OAUTH_CLIENT_SECRET || '').trim();
          if (!clientId || !clientSecret) {
            const lines = [
              'OAuth credentials not available',
              'Missing GOOGLE_OAUTH_CLIENT_ID and/or GOOGLE_OAUTH_CLIENT_SECRET.',
              'Set both env vars to a Desktop app OAuth client and retry.',
              'Preferred path remains: gcloud auth application-default login with Ads scope.',
            ];
            const out = { content: [{ type: 'text', text: lines.join('\n') }] };
            logEvent('manage_auth', startTs, { requestId: input?.request_id, error: { code: 'OAUTH_CLIENT_MISSING', message: 'Missing client id/secret' } });
            return out;
          }
          try {
            const mod = await import('./tools/oauth.js');
            const { runDeviceOAuthForAds } = mod as any;
            const out = await runDeviceOAuthForAds({ clientId, clientSecret });
            const lines = [
              'OAuth device flow completed.',
              `Saved ADC authorized_user JSON: ${out.path}`,
              '',
              'Next steps for current shell (optional):',
              `  export GOOGLE_APPLICATION_CREDENTIALS="${out.path}"`,
            ];
            // Verify scope by listing accounts
            try {
              const resp = await listAccessibleCustomers();
              if (resp.ok) {
                lines.push('Ads scope check after oauth_login: OK');
                const count = resp.data?.resourceNames?.length || 0;
                lines.push(`Accessible accounts: ${count}`);
              } else {
                lines.push(`Ads scope check after oauth_login: HTTP ${resp.status}`);
              }
            } catch { /* ignore */ }
            const resp = { content: [{ type: 'text', text: lines.join('\n') }] };
            logEvent('manage_auth', startTs, { requestId: input?.request_id });
            return resp;
          } catch (e: any) {
            const lines = [
              'OAuth device flow failed.',
              `Error: ${e?.message || String(e)}`,
            ];
            const resp = { content: [{ type: 'text', text: lines.join('\n') }] };
            logEvent('manage_auth', startTs, { requestId: input?.request_id, error: { code: 'OAUTH_DEVICE_FAILED', message: String(e?.message || e) } });
            return resp;
          }
        }
    
        if (action === 'switch') {
          const name = input?.config_name?.trim();
          if (!name) {
            return { content: [{ type: 'text', text: "Missing 'config_name'. Example: { action: 'switch', config_name: 'my-config' }" }] };
          }
          const cmd = `gcloud config configurations activate ${name}`;
          if (!allowSub) {
            const text = [
              'Planned action: switch gcloud configuration',
              `Command: ${cmd}`,
              'Tip: Re-run with allow_subprocess=true (default) to execute from MCP, or set allow_subprocess=false to only print steps.',
            ].join('\n');
            const out = { content: [{ type: 'text', text }] };
            logEvent('manage_auth', startTs, { requestId: input?.request_id });
            return out;
          }
          if (!(await isGcloudAvailable())) {
            const text = [
              'gcloud not found on PATH. Cannot execute switch automatically.',
              `Please run manually: ${cmd}`,
              'Install: https://cloud.google.com/sdk/docs/install',
            ].join('\n');
            const out = { content: [{ type: 'text', text }] };
            logEvent('manage_auth', startTs, { requestId: input?.request_id, error: { code: 'GCLOUD_NOT_FOUND', message: 'gcloud not on PATH' } });
            return out;
          }
          const { code, stdout, stderr } = await execCmd('gcloud', ['config', 'configurations', 'activate', name]);
          const lines = [
            `gcloud switch (${name}) exit: ${code}`,
            stdout ? `stdout:\n${stdout}` : '',
            stderr ? `stderr:\n${stderr}` : '',
            'Next: refresh ADC credentials to ensure Ads scope:',
            '  gcloud auth application-default login --scopes=https://www.googleapis.com/auth/cloud-platform,https://www.googleapis.com/auth/adwords',
          ].filter(Boolean);
          const out = { content: [{ type: 'text', text: lines.join('\n') }] };
          logEvent('manage_auth', startTs, { requestId: input?.request_id });
          return out;
        }
    
        if (action === 'set_project') {
          const projectId = (input?.project_id || input?.project || '').trim();
          const allowSub = input?.allow_subprocess !== false;
          if (!projectId) return { content: [{ type: 'text', text: "Missing 'project_id'. Example: { action: 'set_project', project_id: 'my-project' }" }] };
          const cmd = `gcloud config set project ${projectId}`;
          if (!allowSub) {
            const text = [
              'Planned action: set default gcloud project',
              `Command: ${cmd}`,
            ].join('\n');
            const out = { content: [{ type: 'text', text }] };
            logEvent('manage_auth', startTs, { requestId: input?.request_id });
            return out;
          }
          const { code, stdout, stderr } = await execCmd('gcloud', ['config', 'set', 'project', projectId]);
          const text = [
            `gcloud set project exit: ${code}`,
            stdout ? `stdout:\n${stdout}` : '',
            stderr ? `stderr:\n${stderr}` : '',
          ].filter(Boolean).join('\n');
          const out = { content: [{ type: 'text', text }] };
          logEvent('manage_auth', startTs, { requestId: input?.request_id });
          return out;
        }
    
        if (action === 'set_quota_project') {
          const projectId = (input?.project_id || input?.project || '').trim();
          const allowSub = input?.allow_subprocess !== false;
          if (!projectId) return { content: [{ type: 'text', text: "Missing 'project_id'. Example: { action: 'set_quota_project', project_id: 'my-project' }" }] };
          const cmd = `gcloud auth application-default set-quota-project ${projectId}`;
          if (!allowSub) {
            const text = [
              'Planned action: set ADC quota project',
              `Command: ${cmd}`,
            ].join('\n');
          const out = { content: [{ type: 'text', text }] };
          logEvent('manage_auth', startTs, { requestId: input?.request_id });
          return out;
          }
          const { code, stdout, stderr } = await execCmd('gcloud', ['auth', 'application-default', 'set-quota-project', projectId]);
          const text = [
            `gcloud set-quota-project exit: ${code}`,
            stdout ? `stdout:\n${stdout}` : '',
            stderr ? `stderr:\n${stderr}` : '',
          ].filter(Boolean).join('\n');
          const out = { content: [{ type: 'text', text }] };
          logEvent('manage_auth', startTs, { requestId: input?.request_id });
          return out;
        }
    
        if (action === 'refresh') {
          const loginCmd = 'gcloud auth application-default login --scopes=https://www.googleapis.com/auth/cloud-platform,https://www.googleapis.com/auth/adwords';
          if (!allowSub) {
            const text = [
              'Planned action: refresh ADC credentials for Ads scope',
              `Command: ${loginCmd}`,
              'Tip: Re-run with allow_subprocess=true (default) to execute from MCP, or set allow_subprocess=false to only print steps.',
            ].join('\n');
            const out = { content: [{ type: 'text', text }] };
            logEvent('manage_auth', startTs, { requestId: input?.request_id });
            return out;
          }
          if (!(await isGcloudAvailable())) {
            const text = [
              'gcloud not found on PATH. Cannot execute refresh automatically.',
              `Please run manually: ${loginCmd}`,
              'Install: https://cloud.google.com/sdk/docs/install',
            ].join('\n');
            const out = { content: [{ type: 'text', text }] };
            logEvent('manage_auth', startTs, { requestId: input?.request_id, error: { code: 'GCLOUD_NOT_FOUND', message: 'gcloud not on PATH' } });
            return out;
          }
          const step1 = await execCmd('gcloud', ['auth', 'application-default', 'login', '--scopes=https://www.googleapis.com/auth/cloud-platform,https://www.googleapis.com/auth/adwords']);
          // Verify by printing a token (will also surface scope issues)
          const step2 = await execCmd('gcloud', ['auth', 'application-default', 'print-access-token']);
          let check: any;
          try {
            const mod = await import('./tools/accounts.js');
            const fn = (mod as any).listAccessibleCustomers || listAccessibleCustomers;
            check = await fn();
          } catch { check = undefined; }
          const lines = [
            `refresh login exit: ${step1.code}`,
            step1.stdout ? `login stdout:\n${step1.stdout}` : '',
            step1.stderr ? `login stderr:\n${step1.stderr}` : '',
            `print-token exit: ${step2.code}`,
            step2.stdout ? `token (truncated): ${step2.stdout.slice(0, 12)}...` : '',
            step2.stderr ? `print-token stderr:\n${step2.stderr}` : '',
            (check && check.ok)
              ? 'Ads scope check after refresh: OK'
              : (step2.code === 0 ? 'Ads scope check after refresh: OK (token printed)' : `Ads scope check after refresh: ${check ? `HTTP ${check.status}` : 'unknown'}`),
          ].filter(Boolean);
          if (step1.code !== 0) {
            lines.push('Install: https://cloud.google.com/sdk/docs/install');
          }
          const out = { content: [{ type: 'text', text: lines.join('\n') }] };
          logEvent('manage_auth', startTs, { requestId: input?.request_id });
          return out;
        }
    
        const out = { content: [{ type: 'text', text: `Unknown action '${action}'. Use status | switch | refresh.` }] };
        logEvent('manage_auth', startTs, { requestId: input?.request_id, error: { code: 'ERR_UNKNOWN_ACTION', message: String(action) } });
        return out;
      }
    );
  • Zod schema (ManageAuthZ) defining input parameters for manage_auth tool, with descriptions for LLM guidance, and converted JsonSchema for MCP.
    export const ManageAuthZ = z.object({
      action: z.enum(['status', 'switch', 'refresh']).default('status').describe('status | switch | refresh'),
      config_name: z.string().optional().describe('gcloud configuration name (for switch)'),
      allow_subprocess: z.boolean().optional().describe('execute gcloud steps (default true). Set false to only print commands.'),
    });
    export const ManageAuthSchema: JsonSchema = zodToJsonSchema(ManageAuthZ, 'ManageAuth') as unknown as JsonSchema;
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. It describes actions (status, switch, refresh) and mentions executing gcloud steps or printing commands, which adds useful context. However, it doesn't cover critical aspects like permissions needed, rate limits, error handling, or what 'switch' and 'refresh' entail operationally, leaving gaps for a mutation tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is appropriately sized and front-loaded, starting with the core purpose. It uses semicolons to list actions efficiently, though the sentence structure is slightly dense. Every phrase adds value without redundancy, making it concise but not perfectly polished.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given no annotations and no output schema, the description is incomplete for a tool with 3 parameters and mutation capabilities. It covers the basic purpose and actions but lacks details on behavioral traits, return values, or error conditions. For a tool managing auth—a critical operation—this leaves significant gaps in understanding how to use it effectively.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents all parameters. The description adds marginal value by mentioning 'gcloud configuration name (for switch)' and 'execute gcloud steps', which aligns with but doesn't significantly expand beyond the schema. With high schema coverage, the baseline is 3, and the description doesn't provide extra semantic depth.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose as managing Google Ads authentication with specific actions (status, switch/refresh via gcloud, set_project/set_quota_project, optional oauth_login). It distinguishes itself from sibling tools like 'get_credential_status' and 'refresh_access_token' by offering broader auth management capabilities. However, it doesn't explicitly contrast with all siblings, keeping it at a 4 rather than a perfect 5.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies usage for Google Ads auth management but doesn't explicitly state when to use this tool versus alternatives like 'get_credential_status' for checking status or 'refresh_access_token' for token renewal. It mentions optional oauth_login for creating ADC files, which provides some context, but lacks clear when/when-not guidance or named alternatives.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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