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;

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