Metadata-Version: 2.4
Name: linkedin-ads-mcp-py
Version: 0.1.2
Summary: LinkedIn Ads MCP server with MCC client-switching support
Keywords: mcp,ai,tools,linkedin,ads
Author: LOCOMOTIVE Agency
Author-email: LOCOMOTIVE Agency <erick@locomotive.agency>
License-Expression: MIT
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Requires-Dist: fastmcp>=0.1.0
Requires-Dist: httpx>=0.27.0
Requires-Dist: loguru>=0.7.3
Requires-Dist: pydantic>=2.11.7
Requires-Dist: pydantic-settings>=2.10.1
Requires-Python: >=3.12
Description-Content-Type: text/markdown

# linkedin-ads-mcp

MCP server for the LinkedIn Marketing API, with built-in MCC-style client switching across every
ad account your LinkedIn login can access.

## Installation

Once published to PyPI:

```bash
uv tool install linkedin-ads-mcp-py
```

This installs the `linkedin-ads-mcp` and `linkedin-ads-auth` commands. Or run it directly without
installing:

```bash
uvx --from linkedin-ads-mcp-py linkedin-ads-mcp
```

Point your MCP client (e.g. `.mcp.json` or Claude Desktop's config) at the `linkedin-ads-mcp`
command.

## Setup

1. Create an app at [LinkedIn Developers](https://www.linkedin.com/developers/apps) with access to
   the **Advertising API** product (requires LinkedIn's approval — see their docs, no published
   SLA).
2. Add `http://localhost:3000/callback` (or your own `LINKEDIN_REDIRECT_URI`) to the app's
   authorized redirect URLs.
3. Set the following environment variables (e.g. in a `.env` file in your working directory, or
   directly in your MCP client's server config):

   ```bash
   LINKEDIN_CLIENT_ID=your_client_id
   LINKEDIN_CLIENT_SECRET=your_client_secret
   # Optional overrides:
   # LINKEDIN_REDIRECT_URI=http://localhost:3000/callback
   # LINKEDIN_TOKEN_PATH=/path/to/tokens.json
   ```

4. Authenticate (opens a browser for the LinkedIn OAuth consent screen):

   ```bash
   uvx --from linkedin-ads-mcp-py linkedin-ads-auth
   ```

   Tokens are saved to `~/.linkedin-ads-mcp/tokens.json` by default and refreshed automatically
   when a refresh token is available.

5. Run the server:

   ```bash
   uvx --from linkedin-ads-mcp-py linkedin-ads-mcp
   ```

### Headless / remote deployments

Hosts that can't open a browser or receive the local OAuth callback (e.g. MCP Anywhere) can't run
`linkedin-ads-auth` themselves. Instead, run it once locally as above, then read the `refresh_token`
value out of the saved token file (`~/.linkedin-ads-mcp/tokens.json` by default) and set it as
`LINKEDIN_REFRESH_TOKEN` alongside `LINKEDIN_CLIENT_ID`/`LINKEDIN_CLIENT_SECRET` in that host's
environment variables. On first tool call, the server exchanges it for a live access token
automatically, no browser needed at runtime.

If the host requires a `uvx`-prefixed start command, use
`uvx --from linkedin-ads-mcp-py linkedin-ads-mcp` — plain `uvx linkedin-ads-mcp` fetches an
unrelated package.

If auth fails despite a valid token, double-check it pasted in full.

## MCC / multi-account support

Unlike the original TypeScript `linkedin-ads-mcp`, this server exposes every ad account your
LinkedIn login can access and lets you switch between them by name or ID:

- `list_clients` — list all accessible ad accounts (call this first)
- `set_active_client` — select a default account (by name or numeric ID) for subsequent calls
- `get_active_client` — show which account is currently active

Every account-specific tool also accepts an optional `account` argument to target a client
directly without changing the active selection.

## Tools

| Tool | Notes |
|---|---|
| `list_clients` | MCC view — call first |
| `set_active_client` | select account by name or ID |
| `get_active_client` | show current selection |
| `get_account_details` | account metadata |
| `list_campaign_groups` | |
| `list_campaigns` | |
| `get_campaign_performance` | report |
| `get_campaign_groups_performance` | report |
| `get_creative_performance` | report |
| `get_audience_demographics` | demographic breakdown, top segments by impressions |
| `list_report_dimensions` | catalogue of every dimension reports can use |
| `get_dimension_report` | metrics by any dimension, or up to three crossed |
| `get_audience_reach` | reach / frequency |
| `compare_performance` | compare two time windows or entity sets |
| `get_daily_trends` | daily time series |
| `list_creatives` | |
| `get_conversion_performance` | report |
| `list_conversions` | |
| `get_lead_gen_performance` | report |
| `list_lead_forms` | |
| `list_saved_audiences` | |

## Dimension reports

`get_dimension_report` reaches every dimension LinkedIn's `adAnalytics` endpoint offers. Call
`list_report_dimensions` for the catalogue.

- **Demographic** (one per report): `MEMBER_COMPANY`, `MEMBER_COMPANY_SIZE`, `MEMBER_COUNTRY_V2`,
  `MEMBER_COUNTY`, `MEMBER_INDUSTRY`, `MEMBER_JOB_FUNCTION`, `MEMBER_JOB_TITLE`,
  `MEMBER_REGION_V2`, `MEMBER_SENIORITY`. The legacy `MEMBER_COUNTRY` and `MEMBER_REGION` names map
  to their V2 equivalents.
- **Performance** (up to three crossed): `ACCOUNT`, `CAMPAIGN_GROUP`, `CAMPAIGN`, `CREATIVE`,
  `SHARE`, `COMPANY`, `CONVERSION`, `CONVERSATION_NODE`, `CONVERSATION_NODE_OPTION_INDEX`,
  `SERVING_LOCATION`, `CARD_INDEX`, `PLACEMENT_NAME`, `IMPRESSION_DEVICE_TYPE`, `EVENT_STAGE`,
  and `OBJECTIVE_TYPE` (statistics finder only).

Rules the server enforces before calling LinkedIn, so a bad request fails fast with a clear message:

- Demographic dimensions cannot be combined with any other dimension.
- Demographic dimensions do not support `approximateMemberReach`, `audiencePenetration`, carousel
  card metrics, or `conversionValueInLocalCurrency`.
- `IMPRESSION_DEVICE_TYPE` does not support reach or conversion metrics.
- `audiencePenetration` needs `ACCOUNT`, `CAMPAIGN_GROUP`, or `CAMPAIGN` among the dimensions.
- `CONVERSATION_NODE_OPTION_INDEX` must be requested together with `CONVERSATION_NODE`.
- At most 20 metrics per request.

LinkedIn drops demographic values with fewer than 3 events, returns only the top 100 values per
creative per day, and can lag 12 to 24 hours on demographic data. Responses repeat these caveats in
a `notes` field. Rows carry URNs (for example `urn:li:seniority:5`) in `pivotValues`; `MEMBER_COMPANY_SIZE`
returns values like `SIZE_51_TO_200`. Some breakdowns overlap: in a live check `MEMBER_INDUSTRY` rows summed
to 2.6x the account total, so compare rows to each other rather than adding them up.

`MEMBER_DESIGNATED_MARKET_AREA` (Nielsen DMA) needs API version `202609` or later, so it is not
exposed while the client targets `202601`.

## Architecture

- `auth/oauth.py` — OAuth 2.0 authorization-code flow, browser + local callback server
- `auth/token_store.py` — persistent token storage with automatic refresh
- `api/client.py` — `LinkedInClient`, async wrapper around the LinkedIn Marketing REST API
- `api/dimensions.py` — supported dimensions, finder routing, and metric compatibility rules
- `api/types.py` — Pydantic models for API entities and responses
- `context.py` — `ServerContext` singleton holding the active client and name→ID cache
- `tools/accounts.py` — MCC tools (`list_clients`, `set_active_client`, `get_active_client`)
- `server.py` — MCP stdio server, tool registration

## Development

```bash
uv sync
uv run pre-commit install

uv run ruff format
uv run ruff check
uv run mypy src/
uv run pytest
```

`pytest` runs offline against a fake HTTP layer. The live suite pulls every dimension from the
real LinkedIn API, read only, and skips itself when no valid login exists:

```bash
uv run linkedin-ads-auth                     # fresh login if your token was revoked
uv run pytest -m live -v
# LINKEDIN_TEST_ACCOUNT_ID=123456 to pin the ad account under test
```

## Building and publishing

```bash
uv build
```

Publishing to PyPI is handled by `.github/workflows/pypi-publish.yml` via
[PyPI trusted publishing](https://docs.pypi.org/trusted-publishers/) whenever a GitHub release is
published — no API token needed once the trusted publisher is configured on the PyPI project.

## License

MIT License - see LICENSE file for details.
