Skip to main content
Glama

Google Ads MCP Server

by seovimalraj
README.md10.3 kB
# Google Ads MCP Server A Next.js App Router deployment that exposes a [Model Context Protocol (MCP)](https://platform.openai.com/docs/mcp/overview) server backed by the Google Ads Keyword Planner. The server provides three tools—`get_keyword_ideas`, `get_historical_metrics`, and `get_forecast`—with secure OAuth, encrypted credential storage, in-flight validation, and lightweight rate limiting. It is optimised for Vercel (Node.js runtime) but runs locally with the same feature set. ## Features - **Next.js 15 App Router** using strict TypeScript, ESLint, and Prettier. - **MCP tools** for keyword discovery, historical metrics, and forecasts. - **Google OAuth 2.0 flow** with AES-256-GCM encrypted refresh token storage. - **Redis persistence** via Upstash Redis (or any REST-compatible endpoint) with an automatic in-memory fallback for local development. - **Zod validation** for every tool input plus environment assertions. - **Token bucket rate limiting** (10 requests/minute per user/IP). - **Structured logging & error mapping** for predictable MCP responses. ## Requirements - Node.js 18.18 or newer (Next.js 15 canary requires ≥18.18). - `pnpm` or `npm` for dependency management. - A Google Ads Manager account with API access. - Google Cloud OAuth client (type: Web application) authorised for the deployment URL. - Upstash Redis database (or any Redis instance that exposes the Upstash REST API). ## Environment variables Create a `.env` file (or configure Vercel project secrets) that includes all variables from `.env.example`. <!-- prettier-ignore --> | Variable | Required | Description | |----------|----------|-------------| | `GADS_CLIENT_ID` | ✅ | OAuth client ID created in Google Cloud Console. | | `GADS_CLIENT_SECRET` | ✅ | OAuth client secret that matches the client ID. | | `GADS_DEV_TOKEN` | ✅ | Google Ads developer token approved for production. | | `GADS_LOGIN_CUSTOMER_ID` | ⚪️ | Optional manager account ID used for scoped access. | | `ENCRYPTION_KEY` | ✅ | Base64 encoded 32-byte key for AES-256-GCM. Generate with `openssl rand -base64 32`. | | `REDIS_REST_URL` | ⚪️ | Redis REST endpoint (e.g. Upstash). Equivalent to Vercel's `UPSTASH_REDIS_REST_URL`. | | `REDIS_REST_TOKEN` | ⚪️ | Redis REST token with read/write permissions. Equivalent to `UPSTASH_REDIS_REST_TOKEN`. | | `UPSTASH_REDIS_REST_URL` | ⚪️ | Alternative env key automatically provided by Vercel / Upstash. | | `UPSTASH_REDIS_REST_TOKEN` | ⚪️ | Alternative env key automatically provided by Vercel / Upstash. | | `NEXT_PUBLIC_APP_NAME` | ✅ | Display name shown on the OAuth confirmation page. | | `NEXTAUTH_URL` | ⚪️ | Optional explicit base URL. When omitted the server uses the incoming request origin. | > ℹ️ If any required variable is missing the server throws an explicit error at startup, ensuring you see actionable messages in local development and on Vercel logs. ## Project structure ``` app/ api/ auth/ start/route.ts # Initiates Google OAuth consent callback/route.ts # Handles OAuth callback and refresh token storage mcp/route.ts # MCP endpoint (GET health, POST invoke) lib/ ads.ts # Google Ads client and helpers crypto.ts # AES-256-GCM encrypt/decrypt utilities kv.ts # Redis (Upstash) / in-memory abstraction mcp.ts # Tool registration, validation, rate limiting ratelimit.ts # Simple token-bucket implementation schemas.ts # Zod schemas for env + tool payloads types/index.d.ts # Shared TypeScript types (ToolResponse, AuthToken) ``` ## Getting started locally ```bash pnpm install pnpm dev ``` The development server defaults to `http://localhost:3000`. Ensure your Google OAuth client has that origin and callback (`http://localhost:3000/api/auth/callback`) added to the authorised redirect URIs. ### Running without Redis If `REDIS_REST_URL`/`REDIS_REST_TOKEN` (or the Upstash equivalents) are missing the server logs a warning and stores tokens in-memory. This is ideal for local testing but **not** for production because refresh tokens will be lost on redeploys. ## OAuth flow 1. Direct the browser (or an HTTP client) to `/api/auth/start?userId=<USER>&customerId=<CUSTOMER_ID>`. 2. Google prompts for consent with the `https://www.googleapis.com/auth/adwords` scope. 3. On success Google redirects to `/api/auth/callback`. The server exchanges the code for tokens, encrypts the refresh token with AES-256-GCM, and persists it in Redis. 4. The callback endpoint renders a simple confirmation page. Tokens are now available to all MCP tools via the stored `userId`. The `state` parameter is encrypted to prevent tampering and includes the user/customer IDs plus a timestamp. ## MCP tools All tools share the same POST endpoint: `POST /api/mcp` with JSON payload `{"tool": "<name>", "input": { ... } }`. Responses follow the `ToolResponse` shape (`{ ok: true, data }` or `{ ok: false, error }`). ### `ping` ```json { "tool": "ping", "input": { "userId": "demo" } } ``` Result: ```json { "ok": true, "data": { "status": "ok", "kv": "redis" }, "meta": { "ip": "127.0.0.1" } } ``` ### `get_keyword_ideas` ```json { "tool": "get_keyword_ideas", "input": { "userId": "demo", "customerId": "1234567890", "keywords": ["toroidal transformer"], "locationIds": ["2356"], "languageId": "1000", "network": "GOOGLE_SEARCH_AND_PARTNERS", "pageSize": 30 } } ``` ### `get_historical_metrics` ```json { "tool": "get_historical_metrics", "input": { "userId": "demo", "customerId": "1234567890", "keywords": ["toroidal transformer"], "locationIds": ["2356"], "languageId": "1000" } } ``` ### `get_forecast` ```json { "tool": "get_forecast", "input": { "userId": "demo", "customerId": "1234567890", "keywords": ["toroidal transformer"], "locationIds": ["2356"], "languageId": "1000", "cpcBidMicros": 1500000, "dailyBudgetMicros": 2500000 } } ``` All tool inputs are validated by Zod. Errors return HTTP 400 and a structured payload: ```json { "ok": false, "error": { "code": "INVALID_ARGUMENT", "message": "Input validation failed.", "details": { "fieldErrors": { "keywords": ["Provide either keywords[] or urlSeed."] } } } } ``` ## Rate limiting & errors - **Rate limiting**: 10 requests per minute per user (falls back to IP if userId missing). Exceeding the budget returns `429` with `code: RATE_LIMIT_EXCEEDED`. - **Authentication errors**: Missing or corrupt refresh tokens map to `AUTHENTICATION_ERROR` (HTTP 401). - **Permission issues**: Google Ads permission denials map to `PERMISSION_DENIED` (HTTP 403). - **Quota problems**: API quota issues map to `QUOTA_EXCEEDED` (HTTP 429). - **Unexpected failures**: Return `500` with `code: UNKNOWN` but never leak secrets. Structured logs (via `console.log` / `console.error`) include tool name, user ID, duration, and error codes to help diagnose incidents quickly. ## Deployment on Vercel 1. Push the repository to GitHub and create a Vercel project targeting this repo. 2. Set the production environment variables listed above. Remember to generate a strong `ENCRYPTION_KEY` and configure Redis credentials. 3. Deploy. Vercel automatically builds the Next.js app in the Node.js runtime. 4. After deployment, run the OAuth flow (`https://<app>.vercel.app/api/auth/start?...`) and confirm the callback stores the token (check logs or `/api/mcp` ping). 5. Smoke test each tool with `curl` or the MCP client using the production URL. ## ChatGPT Developer Mode integration Add a remote MCP server in ChatGPT using the following configuration: - **Server URL**: `https://<your-app>.vercel.app/api/mcp` - **Tools**: Automatically discovered (`ping`, `get_keyword_ideas`, `get_historical_metrics`, `get_forecast`). - **Auth flow**: Run `/api/auth/start` in a browser **before** invoking any tool so the refresh token is available. Example invocation in ChatGPT (after auth): ```json { "tool": "get_keyword_ideas", "input": { "userId": "demo", "customerId": "1234567890", "keywords": ["toroidal transformer"], "locationIds": ["2356"], "languageId": "1000" } } ``` Expect a JSON array of keyword ideas with text, competition, and bid ranges. ## Troubleshooting <!-- prettier-ignore --> | Symptom | Resolution | |---------|------------| | `Invalid environment configuration` error on boot | Verify `.env` matches `.env.example` and the encryption key is 32 bytes base64. | | `/api/auth/start` returns 400 | Both `userId` and `customerId` must be present in the query string. | | Google callback lacks `refresh_token` | Ensure you append `prompt=consent&access_type=offline` (the start endpoint does this automatically). Remove the app from Google account permissions and retry. | | MCP tools return `AUTHENTICATION_ERROR` | The stored refresh token may be corrupted or encrypted with an old key. Regenerate `ENCRYPTION_KEY`, re-run OAuth, and redeploy. | | `RATE_LIMIT_EXCEEDED` | Wait until the window resets (~60 seconds) or reduce tool frequency. | | `PERMISSION_DENIED` from Google | Confirm the user has access to the specified `customerId` and the developer token is approved. | ## Acceptance checklist - [x] App Router structure with TypeScript, ESLint, Prettier, and strict mode. - [x] OAuth start/callback routes using AES-256-GCM encrypted refresh tokens. - [x] Redis abstraction with Upstash + in-memory modes. - [x] Google Ads helpers for keyword ideas, historical metrics, and forecasts. - [x] MCP tools with validation, rate limiting, and error handling. - [x] README covering setup, usage, deployment, and troubleshooting. ## Build Fix Notes ### ESLint/Prettier - Installed: eslint, prettier, eslint-plugin-prettier, eslint-config-prettier, @typescript-eslint/parser, @typescript-eslint/eslint-plugin. - .eslintrc.cjs uses `plugin:prettier/recommended`. ### Google Ads Client - Use `customer_id` (snake_case) in CustomerOptions. - Keep other options snake_case: `login_customer_id`, `refresh_token`. ### Telemetry - Optional: set `NEXT_TELEMETRY_DISABLED=1` to silence logs. ### Commands - `pnpm lint`, `pnpm format`, `pnpm typecheck`, `pnpm build`.

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/seovimalraj/google-ads-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server