Skip to main content
Glama
martechery

Google Ads MCP Server

by martechery

manage_auth

Manage Google Ads authentication: check status, switch configurations, refresh tokens, and set projects using gcloud commands or OAuth login.

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
allow_subprocessNoexecute gcloud steps (default true). Set false to only print commands.
config_nameNogcloud configuration name (for switch)

Implementation Reference

  • The core handler function implementing the manage_auth tool logic. Handles various authentication actions including status checks, OAuth login, gcloud configuration switches, project settings, and credential refreshes. Includes subprocess execution for gcloud commands and detailed logging.
    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; }
  • Registration of the manage_auth tool using addTool function, specifying name, description, input schema (ManageAuthZ), and handler.
    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 the manage_auth tool, converted to JSON schema for MCP tool definition.
    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