Skip to main content
Glama
martechery

Google Ads MCP Server

by martechery

refresh_access_token

Refresh access tokens for Google Ads API sessions in multi-tenant mode to maintain authentication and continue API operations.

Instructions

Refresh the access token for a session (multi-tenant mode). Requires GOOGLE_OAUTH_CLIENT_ID/SECRET.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
session_keyYesUUID v4 session key

Implementation Reference

  • Core handler function that refreshes the access token for the given session key by calling the OAuth2 refresh and updating the session credentials.
    export async function refreshAccessTokenForSession(sessionKey: string): Promise<GoogleCredential> {
      const ctx = connections.get(sessionKey);
      if (!ctx) throw new Error('No session found');
      const cred = ctx.credentials;
      if (!cred.refresh_token) throw new Error('No refresh token available');
      if (ctx.refreshPromise) return ctx.refreshPromise;
    
      ctx.refreshPromise = refreshTokenWithOAuth2Client(cred)
        .then((updated) => {
          ctx.credentials = updated;
          ctx.refreshPromise = undefined;
          refreshCount++;
          try { emitMcpEvent({ timestamp: nowIso(), tool: 'token_refresh', session_key: sessionKey, response_time_ms: 0 }); } catch (e) { void e; }
          return updated;
        })
        .catch((err) => {
          ctx.refreshPromise = undefined;
          // Purge on invalid_grant per security model
          if (String(err?.message || '').includes('invalid_grant')) {
            connections.delete(sessionKey);
            try { emitMcpEvent({ timestamp: nowIso(), tool: 'session_ended', session_key: sessionKey, response_time_ms: 0, reason: 'invalid_grant' }); } catch (e) { void e; }
          }
          refreshFailureCount++;
          try { emitMcpEvent({ timestamp: nowIso(), tool: 'token_refresh', session_key: sessionKey, response_time_ms: 0, error: { code: 'ERR_REFRESH_FAILED', message: String(err?.message || err) } }); } catch (e) { void e; }
          throw err;
        });
    
      return ctx.refreshPromise;
    }
  • Registration of the 'refresh_access_token' tool, including input validation, environment checks, and wrapper handler that delegates to refreshAccessTokenForSession.
    addTool(
      server,
      'refresh_access_token',
      'Refresh the access token for a session (multi-tenant mode). Requires GOOGLE_OAUTH_CLIENT_ID/SECRET.',
      RefreshAccessTokenZ,
      async (input: any) => {
        if (process.env.ENABLE_RUNTIME_CREDENTIALS !== 'true') {
          return { content: [{ type: 'text', text: 'Multi-tenant mode not enabled' }] };
        }
        const clientId = (process.env.GOOGLE_OAUTH_CLIENT_ID || '').trim();
        const clientSecret = (process.env.GOOGLE_OAUTH_CLIENT_SECRET || '').trim();
        if (!clientId || !clientSecret) {
          return { content: [{ type: 'text', text: 'OAuth client credentials not set. Set GOOGLE_OAUTH_CLIENT_ID and GOOGLE_OAUTH_CLIENT_SECRET to enable token refresh.' }] };
        }
        try {
          validateSessionKey(String(input?.session_key || ''));
        } catch (e: any) {
          return { content: [{ type: 'text', text: `Error: ${e?.message || String(e)}` }] };
        }
        try {
          const updated = await refreshAccessTokenForSession(String(input.session_key));
          const masked = updated.access_token && updated.access_token.length > 8 ? `${updated.access_token.slice(0,4)}****${updated.access_token.slice(-4)}` : '****';
          const expiresIn = Math.max(0, Math.floor(((updated.expires_at || (Date.now()+3600000)) - Date.now())/1000));
          return { content: [{ type: 'text', text: JSON.stringify({ status: 'refreshed', expires_in: expiresIn, masked_token: masked }) }] };
        } catch (e: any) {
          const msg = String(e?.message || e);
          if (msg.includes('invalid_grant')) {
            return { content: [{ type: 'text', text: JSON.stringify({ error: { code: 'ERR_INVALID_GRANT', message: 'Refresh token invalid or revoked. Re-authentication required.' } }) }] };
          }
          return { content: [{ type: 'text', text: JSON.stringify({ error: { code: 'ERR_REFRESH_FAILED', message: msg } }) }] };
        }
      }
    );
  • Zod schema for the refresh_access_token tool input: requires session_key.
    export const RefreshAccessTokenZ = z.object({
      session_key: z.string().describe('UUID v4 session key'),
    });
  • Helper function that performs the actual OAuth2 access token refresh using google-auth-library.
    async function refreshTokenWithOAuth2Client(cred: GoogleCredential): Promise<GoogleCredential> {
      const clientId = (process.env.GOOGLE_OAUTH_CLIENT_ID || '').trim();
      const clientSecret = (process.env.GOOGLE_OAUTH_CLIENT_SECRET || '').trim();
      if (!clientId || !clientSecret) throw new Error('OAuth client credentials not set');
    
      const { OAuth2Client } = await import('google-auth-library');
      const oauth2Client = new OAuth2Client({ clientId, clientSecret });
      oauth2Client.setCredentials({ refresh_token: cred.refresh_token });
      try {
        const { credentials } = await (oauth2Client as any).refreshAccessToken();
        if (!credentials?.access_token) throw new Error('No access token in refresh response');
        return {
          ...cred,
          access_token: credentials.access_token,
          expires_at: credentials.expiry_date || (Date.now() + 3600 * 1000),
        };
      } catch (e: any) {
        const msg = String(e?.message || e);
        if (msg.includes('invalid_grant')) {
          throw new Error('invalid_grant: Refresh token is invalid or revoked');
        }
        throw e;
      }
    }

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