@channel47/linkedin-ads-mcp
v0.1.0
Published
LinkedIn Ads MCP Server - Query and mutate LinkedIn Marketing API campaign data via versioned REST
Maintainers
Readme
@channel47/linkedin-ads-mcp
MCP server for LinkedIn Ads using the LinkedIn Marketing API (versioned REST, default LinkedIn-Version: 202605).
This server exposes four tools expected by channel47 LinkedIn workflows:
list_accountsqueryanalyticsmutate
Installation
Standalone
npx @channel47/linkedin-ads-mcp@latestMonorepo Development
cd mcps
npm install
npm run testClaude Code
claude mcp add linkedin-ads --env LINKEDIN_ADS_ACCESS_TOKEN=<token> -- npx @channel47/linkedin-ads-mcp@latestOr as JSON config:
{
"mcpServers": {
"linkedin-ads": {
"command": "npx",
"args": ["@channel47/linkedin-ads-mcp@latest"],
"env": {
"LINKEDIN_ADS_ACCESS_TOKEN": "<token>"
}
}
}
}Getting Credentials
- Create an app at developer.linkedin.com and associate it with a LinkedIn Company Page.
- Request access to the Advertising API product (approval required).
- Complete the 3-legged OAuth flow with the
r_adsscope (read) plusrw_ads(mutations) andr_ads_reporting(analytics) to obtain a member access token (valid 60 days). - 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.
Configuration
Required (one of the two auth modes)
| Variable | Description |
|----------|-------------|
| LINKEDIN_ADS_ACCESS_TOKEN | Static LinkedIn OAuth access token, used directly |
| 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. |
Optional
| Variable | Description |
|----------|-------------|
| LINKEDIN_ADS_ACCOUNT_ID | Default ad account ID used when account_id is omitted |
| LINKEDIN_ADS_API_VERSION | LinkedIn-Version header override in YYYYMM format (default 202605) |
| LINKEDIN_ADS_READ_ONLY | Set to true to disable live mutations |
| LINKEDIN_ADS_REQUEST_TIMEOUT_MS | HTTP request timeout in milliseconds (default 30000) |
Tool Reference
list_accounts
List accessible ad accounts from GET /rest/adAccounts?q=search with cursor (pageSize/pageToken) pagination.
Params:
status(optional):ACTIVE,CANCELED,DRAFT,PENDING_DELETION,REMOVED— string or array, applied server-side via the search findertype(optional):BUSINESS,ENTERPRISElimit(optional, default 1000)
Returns id, name, status, currency, type, test flag, organization reference, and serving statuses per account.
query
Entity reads for one ad account:
| entity | Endpoint | Finder |
|----------|----------|--------|
| campaigns | /rest/adAccounts/{id}/adCampaigns | q=search |
| campaign_groups | /rest/adAccounts/{id}/adCampaignGroups | q=search |
| creatives | /rest/adAccounts/{id}/creatives | q=criteria |
Params:
entity(required)account_id(optional ifLINKEDIN_ADS_ACCOUNT_IDexists; accepts123orurn:li:sponsoredAccount:123)status(optional): e.g.ACTIVE,PAUSED,DRAFT,ARCHIVED— for creatives this filtersintendedStatuscampaign_ids(creatives only): restrict creatives to these campaignslimit(default 100; creatives paginate at LinkedIn's max page size of 100, others at 1000)
analytics
Metrics from GET /rest/adAnalytics?q=analytics.
Params:
pivot(required):ACCOUNT,CAMPAIGN_GROUP,CAMPAIGN,CREATIVEstart(required) /end(optional):YYYY-MM-DD; encoded as the Rest.lidateRangeexpressiontime_granularity:ALL(default),DAILY,MONTHLYentity_type(defaultaccount) +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.fields: defaults toimpressions, clicks, costInLocalCurrency, externalWebsiteConversions, dateRange, pivotValues
Notes:
- LinkedIn allows at most 20 metric fields per call; the server enforces this.
- adAnalytics has no pagination — LinkedIn caps responses at 15,000 elements. Narrow the date range or entity list if you hit the cap.
mutate
Mutation tool with dry-run safety by default.
Operation format:
{
"entity": "campaign",
"action": "update",
"id": "123456789",
"params": {
"dailyBudget": { "amount": "75", "currencyCode": "USD" }
}
}Supported entities: campaign, campaign_group, creative
Supported actions:
create— POST to the entity collection under the account. New entities default toDRAFTstatus (intendedStatusfor creatives), LinkedIn's safe non-serving state; pass an explicit status to override. The account URN is filled in automatically; creative creates requireparams.campaign.update— Rest.li partial update: POST to the entity item withX-RestLi-Method: PARTIAL_UPDATEand body{ "patch": { "$set": { ... } } }pause/enable/archive— status shortcuts via partial update (PAUSED/ACTIVE/ARCHIVED; creatives useintendedStatus)
Top-level params:
operations(required)dry_run(defaulttrue): 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 APIpartial_failure(defaulttrue)
Safety notes:
archiveis hard to reverse — preferpause.- Deletion (
PENDING_DELETION) is deliberately not exposed. - Creative IDs are URNs (
urn:li:sponsoredCreative:123); plain numeric IDs are accepted and converted.
Behavior Notes
- Every request sends
Authorization: Bearer <token>,LinkedIn-Version(default202605, override viaLINKEDIN_ADS_API_VERSION), andX-Restli-Protocol-Version: 2.0.0. - Query strings use Rest.li 2.0 encoding:
List(...)params keep literal parens/commas with percent-encoded items (campaigns=List(urn%3Ali%3AsponsoredCampaign%3A123)), anddateRange=(start:(year:2026,month:6,day:1),end:(...))keeps literal structure.URLSearchParamsis never used for these. - API retries once on HTTP
429, using theRetry-Afterheader when available (fallback60s). - Requests are aborted on timeout (default
30000ms, configurable viaLINKEDIN_ADS_REQUEST_TIMEOUT_MS). - Created entity IDs are read from the
x-restli-idresponse header. - LinkedIn API versions are supported for roughly one year; bump
LINKEDIN_ADS_API_VERSIONif requests start failing with version errors.
Read-Only Mode
Set 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.
Development Commands
cd linkedin-ads
npm test
node server/index.jsLicense
MIT
