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
| Name | Required | Description | Default |
|---|---|---|---|
| action | No | status | switch | refresh | status |
| allow_subprocess | No | execute gcloud steps (default true). Set false to only print commands. | |
| config_name | No | gcloud configuration name (for switch) |
Implementation Reference
- src/server-tools.ts:60-391 (handler)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; }
- src/server-tools.ts:56-392 (registration)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; } );
- src/schemas.ts:7-12 (schema)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;