Skip to main content
Glama
andrealufino

aapl-ads-mcp

by andrealufino
README.md
# aapl-ads-mcp

[![Node version](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](https://nodejs.org)
[![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
[![andrealufino/aapl-ads-mcp MCP server](https://glama.ai/mcp/servers/andrealufino/aapl-ads-mcp/badges/score.svg)](https://glama.ai/mcp/servers/andrealufino/aapl-ads-mcp)

An MCP server that connects Claude (and any MCP-compatible client) to
[Apple Search Ads API v5](https://developer.apple.com/documentation/apple_search_ads).

## What is this

MCP (Model Context Protocol) is an open standard that lets AI assistants call
external tools. This server implements the MCP stdio transport and exposes 9
read-only tools that query your Apple Search Ads account — campaigns, ad groups,
keywords, and performance reports.

You install it once, point Claude Desktop at it, and then ask questions in plain
English: "Which keywords drove the most installs last month?" or "Show me
campaigns with zero impressions this week."

## Why

The official ASA dashboards are good for humans but not for ad-hoc analysis or
automated reporting. Existing MCP alternatives are either SaaS (you hand over
your keys) or unmaintained. This is a self-hosted, open-source option you
control.

## Features

- **list_orgs** — verify authentication, list accessible organizations
- **list_campaigns** — enumerate campaigns, optionally filter by status
- **list_ad_groups** — ad groups for a given campaign
- **list_keywords** — targeting keywords with bid amounts and match type
- **get_campaign_report** — impressions, taps, installs, spend, CPI, TTR by campaign
- **get_ad_group_report** — same metrics broken down by ad group
- **get_keyword_report** — per-keyword performance with weekly/daily/monthly granularity
- **get_search_terms_report** — the real search queries that triggered your ads (most useful for discovery)

All tools default to the last 30 days. Reports support `HOURLY`, `DAILY`,
`WEEKLY`, and `MONTHLY` granularity.

## Limitations
- **Read-only by design.** No write operations (create, update, pause) in this release.
- **Requires Apple Search Ads Campaign Management API access.** You need to create an API user in your ASA account and generate an ES256 key pair.
- **Aggregate install metrics work without app-side integration.** `tapInstalls`, `viewInstalls`, and related fields in ASA reports are populated by Apple Search Ads directly and do not require any SDK in your app. AdServices / [AdAttributionKit](https://developer.apple.com/documentation/adattributionkit)is only needed if you want to attribute installs to specific campaigns from inside your app (e.g. for onboarding personalization).
- **Single organization.** The org ID is fixed in the config. Multi-org switching is not implemented.

## Setup

### 1. Generate an ES256 key pair

Use the modern `genpkey` command — it produces PKCS#8 format directly, which is what this server requires. The older `ecparam -genkey` produces SEC1 format and will cause a startup error.

```bash
# Generate private key (PKCS#8)
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out private-key.pem

# Derive public key
openssl pkey -in private-key.pem -pubout -out public-key.pem
```

Verify the private key starts with `-----BEGIN PRIVATE KEY-----` (not `-----BEGIN EC PRIVATE KEY-----`). If it starts with the EC variant, convert it:

```bash
openssl pkcs8 -topk8 -nocrypt -in ec-key.pem -out private-key.pem
```

Store `private-key.pem` outside the repository root if possible (e.g. `~/.ssh/asa-private-key.pem`).

### 2. Create an API user in Apple Search Ads

1. Go to **ASA → Account Settings → User Management**
2. Click **Create User**, choose role **API Account Read Only** for read-only usage (recommended for this server). **API Campaign Manager** is also fine and adds write permissions if you plan to extend the server with write tools later.
3. Go to the **API** tab, click **Create Client**
4. Upload `public-key.pem`
5. Copy `client_id`, `team_id`, and `key_id` from the confirmation screen
6. Find your `org_id` in **Account Settings → Overview**

### 3. Clone and build

```bash
git clone https://github.com/andrealufino/aapl-ads-mcp.git
cd aapl-ads-mcp
npm install
npm run build
```

### 4. Configure Claude Desktop
Edit `~/Library/Application Support/Claude/claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "aapl-ads": {
      "command": "node",
```
  "args": ["/absolute/path/to/aapl-ads-mcp/dist/index.js"],
  "env": {
    "ASA_CLIENT_ID": "SEARCHADS.your-client-id-here",
```

```
    "ASA_TEAM_ID": "SEARCHADS.your-team-id-here",
    "ASA_KEY_ID": "your-key-id-here",
    "ASA_ORG_ID": "12345678",
    "ASA_PRIVATE_KEY_PATH": "/absolute/path/to/private-key.pem"
  }
}
```
```

} }

```

**Note:** `ASA_PRIVATE_KEY_PATH` must be an absolute path. Tilde (`~`) is not
expanded by Node.js — use the full path.

For container or cloud deployments where mounting a file is impractical, set
`ASA_PRIVATE_KEY` to the inline PEM contents instead (newlines preserved). If
both are set, `ASA_PRIVATE_KEY` wins.

Restart Claude Desktop. Ask "run health check" to verify the server is
connected.

## Usage examples

These are natural-language prompts that work with Claude Desktop once the server
is running:
```

List my Apple Ads campaigns

```
```
```
Show me the last 30 days of campaign performance
```

```
Which keywords drove installs in my Brand campaign last week?
```

```
What search terms triggered my ads in the past month? Focus on ones
with impressions but no installs.
```

```
Compare weekly spend across all campaigns for Q1 2025
```

```
Show ad groups in campaign 1234567890 with their bid amounts
```

## Development

```bash
npm run build      # compile TypeScript
npm test           # run test suite (Vitest)
npm run typecheck  # type-check without emitting
npm run lint       # Biome lint
npm run format     # Biome format (write)
```

### MCP Inspector

To debug tool calls interactively without Claude Desktop:

```bash
npx @modelcontextprotocol/inspector node dist/index.js
```

Set the env vars in the Inspector UI before connecting.

### Pre-commit hooks

Install lefthook hooks locally after cloning:

```bash
npx lefthook install
```

This sets up:
- `gitleaks protect --staged` — blocks commits that contain secrets
- Biome lint check on staged `.ts` files
- TypeScript type check

## Contributing

See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for technical details: auth
flow, HTTP client design, tool pattern, report schema quirks, and ASA v5
lessons learned during development.

Bug reports and pull requests welcome.

## Security

- Never commit `.env` or `*.pem` files — both are in `.gitignore`
- Keep `private-key.pem` outside the repository root
- The access token is held in memory only, never written to disk
- If you suspect a key has been exposed, rotate it in **ASA → Account Settings → API**

## License

MIT — see [LICENSE](LICENSE).

TDQS

A4.5/5.0

Scored across 9 tools

Disambiguation5/5

Each tool targets a distinct entity and action: list_* for enumerating hierarchy levels, get_*_report for metrics at each level, get_search_terms_report for search queries, and health for server status. No two tools could be confused for the same task.

Naming Consistency4/5

Most tools follow the list_ or get_*_report pattern clearly separating metadata enumeration from report retrieval. The single deviation is 'health', which is a noun rather than a verb-prefixed action (e.g., check_health), making the pattern slightly inconsistent.

Tool Count5/5

Nine tools is well-scoped for a read-only Apple Search Ads data access server: four list tools for entity hierarchy, four report tools for metrics breakdowns, and a health check. Each tool serves a distinct need without redundancy.

Completeness4/5

The read-only surface covers the full object hierarchy (org → campaign → ad group → keyword) with list operations and corresponding report endpoints, including search terms. However, the complete absence of write operations (no create/update/delete for campaigns or keywords) could be a gap if management tasks are expected, though it appears intentionally read-only.

Maintenance

ActivityInactive
ResponsivenessNo issues