{"_id":"@channel47/linkedin-ads-mcp","name":"@channel47/linkedin-ads-mcp","dist-tags":{"beta":"0.1.0","latest":"0.1.0"},"versions":{"0.1.0":{"name":"@channel47/linkedin-ads-mcp","version":"0.1.0","description":"LinkedIn Ads MCP Server - Query and mutate LinkedIn Marketing API campaign data via versioned REST","main":"server/index.js","bin":{"linkedin-ads-mcp":"server/index.js"},"type":"module","scripts":{"start":"node server/index.js","test":"node --test *.test.js test/*.test.js","prepublishOnly":"npm test"},"keywords":["mcp","model-context-protocol","linkedin-ads","linkedin-marketing-api","analytics","advertising","claude"],"author":{"name":"channel47"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/channel47/mcps.git","directory":"linkedin-ads"},"bugs":{"url":"https://github.com/channel47/mcps/issues"},"homepage":"https://github.com/channel47/mcps/tree/main/linkedin-ads#readme","dependencies":{"@modelcontextprotocol/sdk":"^1.29.0"},"engines":{"node":">=18.0.0"},"_id":"@channel47/linkedin-ads-mcp@0.1.0","gitHead":"b40ed5fec586628f184bd1fecd8838760b4e67e9","_nodeVersion":"24.1.0","_npmVersion":"11.3.0","dist":{"integrity":"sha512-aREy9F7BPkVFN8u6dRJADZ+7jaI61FpZmtxyylJ+I0mswDZDr5s0GV1zkvNFZ1JxeZryCcR2UY0zYx9o0C76gQ==","shasum":"fa395cc4f7450138f9f99c0374982d5d9803eeb2","tarball":"https://registry.npmjs.org/@channel47/linkedin-ads-mcp/-/linkedin-ads-mcp-0.1.0.tgz","fileCount":20,"unpackedSize":75964,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIF5d78u2n5lV6pSvoSQRm8Qj9ZVytRYP/jOe4YVj5TZlAiBWevwrSWEuXlnWvbA71sn9lN6A60sBeN/I4HgDRzQoRA=="}]},"_npmUser":{"name":"ctrlswing","email":"jacksondean.me@gmail.com"},"directories":{},"maintainers":[{"name":"ctrlswing","email":"jacksondean.me@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/linkedin-ads-mcp_0.1.0_1783745130880_0.24887530316348205"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-11T04:45:30.776Z","0.1.0":"2026-07-11T04:45:31.042Z","modified":"2026-07-11T04:45:31.279Z"},"maintainers":[{"name":"ctrlswing","email":"jacksondean.me@gmail.com"}],"description":"LinkedIn Ads MCP Server - Query and mutate LinkedIn Marketing API campaign data via versioned REST","homepage":"https://github.com/channel47/mcps/tree/main/linkedin-ads#readme","keywords":["mcp","model-context-protocol","linkedin-ads","linkedin-marketing-api","analytics","advertising","claude"],"repository":{"type":"git","url":"git+https://github.com/channel47/mcps.git","directory":"linkedin-ads"},"author":{"name":"channel47"},"bugs":{"url":"https://github.com/channel47/mcps/issues"},"license":"MIT","readme":"# @channel47/linkedin-ads-mcp\n\nMCP server for LinkedIn Ads using the LinkedIn Marketing API (versioned REST, default `LinkedIn-Version: 202605`).\n\nThis server exposes four tools expected by channel47 LinkedIn workflows:\n\n- `list_accounts`\n- `query`\n- `analytics`\n- `mutate`\n\n## Installation\n\n### Standalone\n\n```bash\nnpx @channel47/linkedin-ads-mcp@latest\n```\n\n### Monorepo Development\n\n```bash\ncd mcps\nnpm install\nnpm run test\n```\n\n### Claude Code\n\n```bash\nclaude mcp add linkedin-ads --env LINKEDIN_ADS_ACCESS_TOKEN=<token> -- npx @channel47/linkedin-ads-mcp@latest\n```\n\nOr as JSON config:\n\n```json\n{\n  \"mcpServers\": {\n    \"linkedin-ads\": {\n      \"command\": \"npx\",\n      \"args\": [\"@channel47/linkedin-ads-mcp@latest\"],\n      \"env\": {\n        \"LINKEDIN_ADS_ACCESS_TOKEN\": \"<token>\"\n      }\n    }\n  }\n}\n```\n\n## Getting Credentials\n\n1. Create an app at [developer.linkedin.com](https://developer.linkedin.com/) and associate it with a LinkedIn Company Page.\n2. Request access to the **Advertising API** product (approval required).\n3. Complete the 3-legged OAuth flow with the `r_ads` scope (read) plus `rw_ads` (mutations) and `r_ads_reporting` (analytics) to obtain a member access token (valid 60 days).\n4. Either paste that token into `LINKEDIN_ADS_ACCESS_TOKEN`, or — if your app is enabled for programmatic refresh — supply client ID/secret and the refresh token (valid 1 year) and let the server refresh automatically.\n\n## Configuration\n\n### Required (one of the two auth modes)\n\n| Variable | Description |\n|----------|-------------|\n| `LINKEDIN_ADS_ACCESS_TOKEN` | Static LinkedIn OAuth access token, used directly |\n| `LINKEDIN_ADS_CLIENT_ID` + `LINKEDIN_ADS_CLIENT_SECRET` + `LINKEDIN_ADS_REFRESH_TOKEN` | OAuth refresh flow. When all three are set the server exchanges the refresh token at `https://www.linkedin.com/oauth/v2/accessToken`, caches the access token in memory, and refreshes ~5 minutes before expiry. Takes precedence over the static token. |\n\n### Optional\n\n| Variable | Description |\n|----------|-------------|\n| `LINKEDIN_ADS_ACCOUNT_ID` | Default ad account ID used when `account_id` is omitted |\n| `LINKEDIN_ADS_API_VERSION` | `LinkedIn-Version` header override in `YYYYMM` format (default `202605`) |\n| `LINKEDIN_ADS_READ_ONLY` | Set to `true` to disable live mutations |\n| `LINKEDIN_ADS_REQUEST_TIMEOUT_MS` | HTTP request timeout in milliseconds (default `30000`) |\n\n## Tool Reference\n\n### `list_accounts`\n\nList accessible ad accounts from `GET /rest/adAccounts?q=search` with cursor (`pageSize`/`pageToken`) pagination.\n\n**Params:**\n- `status` (optional): `ACTIVE`, `CANCELED`, `DRAFT`, `PENDING_DELETION`, `REMOVED` — string or array, applied server-side via the search finder\n- `type` (optional): `BUSINESS`, `ENTERPRISE`\n- `limit` (optional, default 1000)\n\nReturns id, name, status, currency, type, test flag, organization reference, and serving statuses per account.\n\n### `query`\n\nEntity reads for one ad account:\n\n| `entity` | Endpoint | Finder |\n|----------|----------|--------|\n| `campaigns` | `/rest/adAccounts/{id}/adCampaigns` | `q=search` |\n| `campaign_groups` | `/rest/adAccounts/{id}/adCampaignGroups` | `q=search` |\n| `creatives` | `/rest/adAccounts/{id}/creatives` | `q=criteria` |\n\n**Params:**\n- `entity` (required)\n- `account_id` (optional if `LINKEDIN_ADS_ACCOUNT_ID` exists; accepts `123` or `urn:li:sponsoredAccount:123`)\n- `status` (optional): e.g. `ACTIVE`, `PAUSED`, `DRAFT`, `ARCHIVED` — for creatives this filters `intendedStatus`\n- `campaign_ids` (creatives only): restrict creatives to these campaigns\n- `limit` (default 100; creatives paginate at LinkedIn's max page size of 100, others at 1000)\n\n### `analytics`\n\nMetrics from `GET /rest/adAnalytics?q=analytics`.\n\n**Params:**\n- `pivot` (required): `ACCOUNT`, `CAMPAIGN_GROUP`, `CAMPAIGN`, `CREATIVE`\n- `start` (required) / `end` (optional): `YYYY-MM-DD`; encoded as the Rest.li `dateRange` expression\n- `time_granularity`: `ALL` (default), `DAILY`, `MONTHLY`\n- `entity_type` (default `account`) + `entity_ids`: plain IDs are converted to sponsored URNs and sent as the matching facet param (`accounts=List(...)`, `campaigns=List(...)`, ...). When omitted, the report is scoped to the account.\n- `fields`: defaults to `impressions, clicks, costInLocalCurrency, externalWebsiteConversions, dateRange, pivotValues`\n\n**Notes:**\n- LinkedIn allows at most **20 metric fields** per call; the server enforces this.\n- adAnalytics has **no pagination** — LinkedIn caps responses at 15,000 elements. Narrow the date range or entity list if you hit the cap.\n\n### `mutate`\n\nMutation tool with dry-run safety by default.\n\n**Operation format:**\n\n```json\n{\n  \"entity\": \"campaign\",\n  \"action\": \"update\",\n  \"id\": \"123456789\",\n  \"params\": {\n    \"dailyBudget\": { \"amount\": \"75\", \"currencyCode\": \"USD\" }\n  }\n}\n```\n\n**Supported entities:** `campaign`, `campaign_group`, `creative`\n\n**Supported actions:**\n- `create` — POST to the entity collection under the account. New entities default to `DRAFT` status (`intendedStatus` for creatives), LinkedIn's safe non-serving state; pass an explicit status to override. The account URN is filled in automatically; creative creates require `params.campaign`.\n- `update` — Rest.li partial update: POST to the entity item with `X-RestLi-Method: PARTIAL_UPDATE` and body `{ \"patch\": { \"$set\": { ... } } }`\n- `pause` / `enable` / `archive` — status shortcuts via partial update (`PAUSED` / `ACTIVE` / `ARCHIVED`; creatives use `intendedStatus`)\n\n**Top-level params:**\n- `operations` (required)\n- `dry_run` (default `true`): LinkedIn has **no server-side validate-only mode**, so dry run performs local validation and returns a preview of the exact requests (method, path, headers, body) without calling the API\n- `partial_failure` (default `true`)\n\n**Safety notes:**\n- `archive` is hard to reverse — prefer `pause`.\n- Deletion (`PENDING_DELETION`) is deliberately not exposed.\n- Creative IDs are URNs (`urn:li:sponsoredCreative:123`); plain numeric IDs are accepted and converted.\n\n## Behavior Notes\n\n- Every request sends `Authorization: Bearer <token>`, `LinkedIn-Version` (default `202605`, override via `LINKEDIN_ADS_API_VERSION`), and `X-Restli-Protocol-Version: 2.0.0`.\n- Query strings use Rest.li 2.0 encoding: `List(...)` params keep literal parens/commas with percent-encoded items (`campaigns=List(urn%3Ali%3AsponsoredCampaign%3A123)`), and `dateRange=(start:(year:2026,month:6,day:1),end:(...))` keeps literal structure. `URLSearchParams` is never used for these.\n- API retries once on HTTP `429`, using the `Retry-After` header when available (fallback `60s`).\n- Requests are aborted on timeout (default `30000ms`, configurable via `LINKEDIN_ADS_REQUEST_TIMEOUT_MS`).\n- Created entity IDs are read from the `x-restli-id` response header.\n- LinkedIn API versions are supported for roughly one year; bump `LINKEDIN_ADS_API_VERSION` if requests start failing with version errors.\n\n## Read-Only Mode\n\nSet `LINKEDIN_ADS_READ_ONLY=true` to remove the `mutate` tool entirely. Dry runs are unaffected in normal mode; live execution is blocked even if a mutate call slips through.\n\n## Development Commands\n\n```bash\ncd linkedin-ads\nnpm test\nnode server/index.js\n```\n\n## License\n\nMIT\n","readmeFilename":"README.md","_rev":"1-93ee2737c3973e90e95f4599fb2325a2"}