npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

linkedin-ads-full-readwrite-mcp

v0.2.6

Published

MCP server for LinkedIn Ads with full read and write access — manage campaigns, creatives, audiences, budgets, assets, and Lead Gen Form attachments from any MCP host.

Readme

linkedin-ads-full-readwrite-mcp

A Model Context Protocol server that gives any MCP-aware host (Claude Desktop, IDE agents, custom clients) full read and write access to a LinkedIn Ads account. Manage campaigns, creatives, audiences, budgets, assets, page-post sponsorship, and Lead Gen Form attachments from natural-language prompts.

// In your MCP host config (e.g. Claude Desktop):
{
  "mcpServers": {
    "linkedin-ads": {
      "command": "npx",
      "args": ["-y", "linkedin-ads-full-readwrite-mcp@latest"],
      "env": {
        "LINKEDIN_CLIENT_ID": "...",
        "LINKEDIN_CLIENT_SECRET": "...",
        "LINKEDIN_AD_ACCOUNT_ID": "..."
      }
    }
  }
}

Then ask your assistant: "List my LinkedIn ad campaigns, find the ones that have spent more than €500 with a CTR under 0.3%, and pause them."

Why this exists

LinkedIn's Marketing Developer Platform is gated — full API access requires explicit approval per developer, so no general-purpose write-capable MCP has been distributed. This package fills that gap for anyone who has been approved into the Advertising API. Open source, MIT-licensed, no telemetry, no third-party services.

Requirements

  • Node.js 18 or newer.
  • An MCP-aware host such as Claude Desktop.
  • A LinkedIn account with administrative access to the LinkedIn Page you want to advertise from. You need Super Admin role on that Page in order to associate a developer application with it.
  • A LinkedIn Ads account, with your LinkedIn user holding an ACCOUNT_MANAGER or higher role on it.

The rest of this README walks you through everything else from scratch.

Setup & installation

There are eight steps, in order. Plan for roughly an hour the first time you do this end-to-end — most of it will be waiting for LinkedIn to approve API access. Some applications are approved within minutes; others take 2 to 4 weeks.

Step 1 — Create a LinkedIn developer application

  1. Open the LinkedIn Developer Portal and sign in.
  2. Click Create app.
  3. Fill in:
    • App name — anything descriptive. Example: My Ads Integration.
    • LinkedIn Page — the company Page you want the application to be associated with. You must be a Super Admin on this Page.
    • Privacy policy URL — required, must resolve to a real page.
    • App logo — upload anything reasonable; LinkedIn requires it.
  4. Accept the terms and click Create app.

Note the Client ID and Client Secret that appear under the Auth tab. You will use both shortly.

Step 2 — Verify the company association

LinkedIn requires a one-time verification that confirms the developer application belongs to the Page. Without it, you cannot request access to the Advertising API.

  1. On the application's Settings tab, find the Verify button next to your Page name and click it.
  2. Click Generate URL.
  3. The URL must be opened by a Super Admin of the associated Page. If that is you, open it in the same browser session (you are already signed in). If it is someone else, send them the URL by email or LinkedIn Messaging — it is valid for 30 days.
  4. The Super Admin clicks Approve.

After approval, the company association is permanent and cannot be undone.

Step 3 — Apply for Advertising API access

  1. On the application's Products tab, find Advertising API in the list. Click Request access.
  2. Complete the access form. The most important field is the use-case description — LinkedIn rejects applications whose use-case is ambiguous or appears to be a third-party platform when it is actually first-party.

If you are managing your own ad account (the common case for individual advertisers), the framing that works is:

A first-party integration that manages our own LinkedIn ad campaigns. It is not a multi-tenant platform and will not be offered to or used by any third party. We are the sole advertiser, the sole user of the integration, and the owner of the single ad account against which the integration operates.

LinkedIn explicitly disallows using member data for sales prospecting, list building, or contact enrichment — so do not describe any of those in the form, even tangentially.

  1. Submit. LinkedIn responds by email. Typical turnaround is 2 to 4 weeks; some applications come back within the same day. Status is also visible on the Products tab.

Step 4 — Get your nine-digit Ad Account ID

  1. Open Campaign Manager.
  2. Pick the ad account you want to operate against.
  3. The nine-digit ID is in the URL after /accounts/ and on the account-selector dropdown. Save it.

If you have multiple accounts (sometimes LinkedIn auto-creates accounts when you use the "Boost this post" quick action), pick the one with the right name, currency, and status (ACTIVE).

Step 5 — Authorize your developer app on the Ad Account

LinkedIn's Marketing API requires this step even though your user has admin permissions on the ad account. The developer application itself must be explicitly allow-listed against each ad account it will write to. Reads work without this step; writes return Your application is not configured to access the related advertiser account(s) until you do it.

On Development Tier (the default after Advertising API approval) you can allow-list up to 5 ad accounts. Reads remain unlimited.

  1. In the Developer Portal, open your application and go to the Products tab.
  2. Find the Advertising API product card. Click View Ad Accounts on it.
  3. Paste the nine-digit Ad Account ID from Step 4 into the field and save.
  4. Repeat for any other ad accounts you want the integration to manage.

Skipping this step is the single most common reason a freshly-published integration "works for reads but fails for writes." If a tool ever returns a PERMISSION-category error with the text "please ensure you have added the account ID(s) in your Account Management list", this is what is missing.

Step 6 — Authenticate

This package ships a --login flow that handles the OAuth 2.0 three-legged round-trip for you. It runs a loopback HTTP server on port 8765, opens LinkedIn's consent page in your browser, captures the authorization code, and exchanges it for a long-lived refresh token.

  1. In a terminal, run:

    LINKEDIN_CLIENT_ID="<your client id>" \
    LINKEDIN_CLIENT_SECRET="<your client secret>" \
    npx -y linkedin-ads-full-readwrite-mcp --login --save

    On Windows PowerShell, the equivalent is:

    $env:LINKEDIN_CLIENT_ID = "<your client id>"
    $env:LINKEDIN_CLIENT_SECRET = "<your client secret>"
    npx -y linkedin-ads-full-readwrite-mcp --login --save
  2. The script will tell you to register a redirect URL on your developer application. Go to Developer Portal → your app → Auth tab → Authorized redirect URLs for your app and add exactly:

    http://127.0.0.1:8765/callback

    Save. Then press Enter in the terminal to continue.

  3. Your browser opens to LinkedIn's consent screen. Review the requested scopes and click Allow.

  4. The terminal prints your access token and refresh token, and reports that the refresh token has been saved to your OS auth cache:

    • Windows: %LOCALAPPDATA%\linkedin-ads-full-readwrite-mcp\auth.json
    • macOS: ~/.config/linkedin-ads-full-readwrite-mcp/auth.json
    • Linux: $XDG_CONFIG_HOME/linkedin-ads-full-readwrite-mcp/auth.json (default ~/.config/...)

    The file is written at mode 0o600 (owner read/write only). The refresh token is long-lived (LinkedIn's default is 12 months). Re-run this command when it expires.

Step 7 — Verify with --test

Confirm the full auth round-trip works before wiring into your MCP host:

LINKEDIN_CLIENT_ID="..." LINKEDIN_CLIENT_SECRET="..." \
  npx -y linkedin-ads-full-readwrite-mcp --test

You should see a list of ad accounts the OAuth principal can access, with name, status, currency, and your role on each.

Step 8 — Wire into your MCP host

Add the package to your host's MCP server configuration. The location of this file depends on the host:

| Host | Config file | |---|---| | Claude Desktop (macOS) | ~/Library/Application Support/Claude/claude_desktop_config.json | | Claude Desktop (Windows) | %APPDATA%\Claude\claude_desktop_config.json | | Other MCP hosts | See the host's documentation |

Add this entry inside the mcpServers block:

"linkedin-ads": {
  "command": "npx",
  "args": ["-y", "linkedin-ads-full-readwrite-mcp@latest"],
  "env": {
    "LINKEDIN_CLIENT_ID": "<your client id>",
    "LINKEDIN_CLIENT_SECRET": "<your client secret>",
    "LINKEDIN_AD_ACCOUNT_ID": "<your nine-digit ad account id>"
  }
}

The refresh token does not need to appear in the host config — it is read from the OS auth cache by default.

Fully restart the host (quit from the system tray, not just close the window). The tools are now available in any new conversation.

Configuration

| Env var | Required | Default | Purpose | |---|---|---|---| | LINKEDIN_CLIENT_ID | yes | — | OAuth client ID from the developer portal. | | LINKEDIN_CLIENT_SECRET | yes | — | OAuth client secret. | | LINKEDIN_REFRESH_TOKEN | yes* | — | Long-lived refresh token. Optional if --login --save was used (the auth cache file is read as a fallback). | | LINKEDIN_AD_ACCOUNT_ID | required for write tools | — | The nine-digit ad account ID. URN form is constructed internally. Pinning this prevents prompt-injection from steering writes to the wrong account. | | LINKEDIN_API_VERSION | no | 202605 | LinkedIn API version header. Bump deliberately when adopting a newer version. | | LINKEDIN_LOGIN_PORT | no | 8765 | Loopback port for the --login OAuth callback. Change if port 8765 is in use. | | LINKEDIN_ADS_MCP_CONFIRM_WRITES | no | true | Set to false to skip the confirm: true requirement on destructive operations. | | LINKEDIN_ADS_MCP_DRY_RUN | no | false | Set to true to log mutate operations without calling LinkedIn. | | LINKEDIN_ADS_MCP_MAX_BUDGET | no | 1000 | Soft daily-budget ceiling in account currency major units. | | LINKEDIN_ADS_MCP_LOG_LEVEL | no | info | One of error, warn, info, debug. |

Credential handling — recommended practices

The package is designed so that secrets never need to appear anywhere outside your own machine. The recommendations below match how the package itself handles them and reduce the chance of accidental leaks.

What lives where

  • Client ID and Client Secret — values issued by LinkedIn to your developer application. They identify the application, not the user. Both belong in your MCP host config file under env (see Step 8 above), or in environment variables for ad-hoc CLI use.
  • Refresh token — a long-lived credential that lets the package mint short-lived access tokens. The --login --save flow writes this to a per-user file at mode 0o600. Do not move it, do not put it in a host config file, do not commit it.
  • Access token — short-lived. Held only in process memory, never written to disk. Auto-refreshed when it expires.

Recommendations

  1. Use --login --save rather than passing the refresh token via environment variables. It keeps the long-lived credential in one specific file with strict permissions, instead of in shell history, host config files, or process environments where it can leak.
  2. Keep host config files local to the machine. The host config contains your client secret. Do not commit it to version control. Do not sync it through cloud storage that other people can see. If you do use cloud sync (such as Dropbox or OneDrive), confirm the file is encrypted at rest and not shared.
  3. Use one developer app per person, not a shared one. Each user has their own client secret and their own refresh token. This contains the blast radius if a credential is compromised and lets you revoke one user's access without affecting anyone else.
  4. Do not paste secrets into chat, email, ticketing systems, or other communication tools. If you need to share a config with a colleague, share the structure with placeholders and have them generate their own values.
  5. Do not pass secrets as command-line arguments in scripts. Process lists, shell history, and CI logs all capture command lines. Use environment variables or files.
  6. Rotate periodically. Regenerate the client secret from the developer portal at least once a year, and immediately if you have any reason to believe it was exposed. Re-run --login --save after rotating.
  7. Revoke unused redirect URLs. Earlier versions of this package picked a random loopback port on each login run, which left stale entries on the developer app. Remove anything that is not http://127.0.0.1:8765/callback.
  8. Pin LINKEDIN_AD_ACCOUNT_ID in production-style use. With the account pinned, calls against any other account are refused — a layer of defence against prompt-injection or copy-paste mistakes.

Tool catalogue

v0.2 exposes 67 tools across 10 categories. Every mutate tool defaults to DRAFT/PAUSED and every destructive tool requires confirm: true. See the Safety guardrails section for details.

Ad accounts (3)

  • list_ad_accounts — accounts the OAuth principal can access, enriched with name, currency, status, type, and role. Use this first to confirm authentication.
  • get_ad_account — full details for one account.
  • list_ad_account_users — users with roles on an account.

Campaign groups (8)

  • list_campaign_groups / get_campaign_group
  • create_campaign_group — defaults to DRAFT. Supports total budget, run schedule, objective type.
  • update_campaign_group — partial update.
  • enable_campaign_group / pause_campaign_group
  • archive_campaign_groupconfirm: true required.
  • bulk_archive_campaign_groups — up to 100 IDs per call, per-item result returned.

Campaigns (13)

  • list_campaigns / get_campaign
  • create_campaign — full create with cost type, bid, daily/total budget, run schedule, locale, targeting, audience-expansion, off-site delivery. Defaults to DRAFT.
  • update_campaign — partial update including targeting replacement.
  • enable_campaign / pause_campaign
  • archive_campaignconfirm: true required.
  • move_campaign_to_group — re-parent to a different campaign group; confirm: true required.
  • clone_campaign — duplicate settings and targeting into a new DRAFT.
  • bulk_pause_campaigns / bulk_enable_campaigns / bulk_archive_campaigns
  • attach_conversion_actions_to_campaign

Targeting (9)

  • search_targeting_facets — typeahead resolver for any facet (locations, industries, job titles, skills, companies, etc.). Returns URNs ready to drop into a targeting object.
  • resolve_geo_urn — convenience wrapper for the LOCATION facet.
  • set_campaign_targeting — replace a campaign's targeting in one call.
  • add_targeting_inclusions / add_targeting_exclusions — append URNs to one facet without rewriting the rest.
  • clear_campaign_targeting — clear one facet or all targeting; confirm: true required.
  • get_audience_size_estimate — reach estimate for a given targeting object.
  • list_saved_audiences / apply_saved_audience_to_campaign — read account-scoped saved audience templates and apply one to a campaign.

The user-facing targeting object accepts a friendly { include: [urn], exclude: [urn] } shape on each facet — locations, industries, job_functions, job_titles, skills, seniorities, company_sizes, companies, groups, member_ages, member_genders, schools, degrees, fields_of_study, years_of_experience, interface_locales. The package translates it into LinkedIn's targetingCriteria envelope internally.

Assets (7)

Assets are LinkedIn's reusable media library. Upload once, get an asset URN, reference from multiple creatives.

  • list_assets — by type (IMAGE / VIDEO / DOCUMENT).
  • get_asset — auto-detects type from the URN.
  • upload_image_asset — two-step register-then-PUT flow; accepts a local path or a public URL.
  • upload_document_asset — for Document Ad PDFs.
  • upload_video_asset — multi-part upload with per-chunk PUT and finalize.
  • archive_asset / bulk_archive_assetsconfirm: true required.

Creatives (13)

  • list_creatives — by ad account, optionally scoped to one campaign.
  • get_creative — by urn:li:sponsoredCreative: URN.
  • create_single_image_creative — accepts an asset URN or uploads an image inline.
  • create_document_ad_creative — Document Ads; supports inline PDF upload and Lead Gen Form attachment.
  • create_video_creative — accepts an asset URN or uploads a video inline.
  • create_text_ad_creative — text ad with optional image.
  • create_sponsored_post_creative — sponsor an existing organic Page post by share URN.
  • update_creative — partial update of intro, headline, description, destination, CTA, status, lead form.
  • enable_creative / pause_creative
  • archive_creativeconfirm: true required.
  • bulk_pause_creatives / bulk_archive_creatives

Lead Gen Forms (5)

Form creation is out of scope (Lead Sync API requires a separate approval). The package attaches and detaches existing forms.

  • list_lead_gen_forms / get_lead_gen_form
  • attach_lead_gen_form_to_creative / detach_lead_gen_form_from_creative
  • list_leads — lead submissions. Requires the Lead Sync API scope on the OAuth principal — surfaces a clear permission error if not granted.

Analytics (3)

  • get_campaign_performance — by campaign IDs or campaign group IDs, with pivot on CAMPAIGN, CREATIVE, MEMBER_COMPANY, MEMBER_INDUSTRY, MEMBER_JOB_TITLE, MEMBER_JOB_FUNCTION, MEMBER_SENIORITY, MEMBER_COUNTRY_V2, and more.
  • get_creative_performance — per-creative metrics.
  • get_account_performance — account-level, with full pivot set.

Date range is passed as { start: "2026-06-01", end: "2026-06-30" }. Default metrics include impressions, clicks, cost (USD and local currency), website conversions, one-click leads, and video stats; override via the optional metrics field.

Page posts (3)

  • list_page_posts — organic posts on a Page, by urn:li:organization: URN.
  • get_page_post — one post by share URN.
  • create_direct_sponsored_content — create a "dark" post that exists only for sponsorship, not on the organic feed. Returns a share URN ready for create_sponsored_post_creative.

Conversion tracking (3, read-only)

Setup of new conversion actions is done in Campaign Manager.

  • list_conversion_actions / get_conversion_action
  • get_campaign_conversion_performance — per-campaign conversion counts over a date range, optionally filtered to specific conversion action IDs.

Safety guardrails

This package can spend money on your behalf. The following safeguards are mandatory and enforced by default:

  • Default-paused. Every create_* tool defaults to DRAFT or PAUSED status. The host must explicitly enable a campaign or creative before any spend begins.
  • Confirm-required. Archive, move, and bulk operations require an explicit confirm: true input. Override with LINKEDIN_ADS_MCP_CONFIRM_WRITES=false only for scripted use.
  • Budget ceiling. create_campaign and update_campaign reject daily_budget above LINKEDIN_ADS_MCP_MAX_BUDGET.
  • Account scoping. If LINKEDIN_AD_ACCOUNT_ID is set, calls against any other account are rejected.
  • Dry-run mode. LINKEDIN_ADS_MCP_DRY_RUN=true makes every mutate tool log what it would do and return success without calling the API. Use this for the first day with a new host.
  • No telemetry. The only outbound HTTP traffic is to api.linkedin.com and www.linkedin.com.

Troubleshooting

You need to pass the "client_id" parameter on the LinkedIn consent page. The URL was truncated before reaching LinkedIn. This usually means a shell or browser opener mishandled the & character. Re-run --login; the package opens the URL through the OS URL handler directly rather than routing through a shell, so this should not happen with v0.1.1 and later.

The redirect_uri does not match the registered value. The redirect URL registered on the developer app does not match http://127.0.0.1:8765/callback exactly. Go to the Auth tab on your app and add that URL verbatim, including the scheme, the IP 127.0.0.1 (not localhost), the port, and the /callback path. Save and retry.

No LinkedIn refresh token available. You have not run --login --save yet, or LINKEDIN_REFRESH_TOKEN is not set in the environment that the MCP host is launching the server in. Re-run --login --save and restart the host.

Port 8765 is already in use. Something else on your machine is occupying that port. Set LINKEDIN_LOGIN_PORT to a free port, re-run --login, and update the redirect URL on the developer app to match.

Auth works for --test but the MCP host shows "tools not available". The host has not picked up the new config. Fully quit it (from the system tray on Windows, from the menu on macOS — closing the window is not enough) and reopen.

Multiple ad accounts show up unexpectedly. LinkedIn auto-creates an ad account each time you use the "Boost this post" quick action on a personal post. They show up alongside your main account. Pin the right one with LINKEDIN_AD_ACCOUNT_ID; the others will be ignored.

Reads work but writes fail with PERMISSION and "please ensure you have added the account ID(s) in your Account Management list". The developer application has not been allow-listed against this ad account. See Step 5 of the setup — open the Developer Portal, find your app, go to Products → Advertising API → View Ad Accounts, and paste the nine-digit account ID. No restart or republish needed; LinkedIn picks up the change on the next API call.

Limitations

The package does not support (in some cases because LinkedIn gates them behind separate approvals):

  • Lead Gen Form creation. Requires separate Lead Sync API approval. Attaching existing forms to creatives is supported.
  • Matched Audiences and Predictive Audiences. Private products that require additional approval after Advertising API access is granted.
  • Conversion-action creation. Read existing conversion actions and attach them to campaigns; create new ones in Campaign Manager.
  • Sponsored Messaging / InMail (Conversations API).
  • Event Ads.

How it works

The package is a stdio-transport MCP server written in plain ES-module JavaScript. It wraps the LinkedIn Marketing REST API through Node's built-in fetch, with:

  • 3-legged OAuth using refresh tokens for long-lived access. Access tokens are cached in memory with a 60-second safety buffer and auto-refreshed on 401.
  • LinkedIn's required headers (LinkedIn-Version, X-Restli-Protocol-Version: 2.0.0) on every call.
  • Exponential backoff with jitter on 429 rate-limit responses, up to three retries.
  • URN construction and parsing (urn:li:sponsoredCampaign:12345) handled internally so tools accept plain integer IDs.
  • Structured error classification mapping HTTP status and LinkedIn service codes to AUTHENTICATION, PERMISSION, VALIDATION, NOT_FOUND, RATE_LIMIT, INTERNAL, or UNKNOWN.

Security model

  • Secrets enter only through environment variables. The package never prompts for them, never logs them, and never transmits them anywhere other than the LinkedIn OAuth endpoint.
  • The refresh-token cache file is written at mode 0o600 (owner read/write only) and lives in the standard per-user app data location.
  • The files allowlist in package.json prevents stray files (.env, node_modules, local auth caches) from being included in the published tarball.
  • No outbound HTTP except to LinkedIn endpoints. No analytics, no error reporting, no update checks.

Changelog

See CHANGELOG.md for the per-version history of additions, changes, and fixes.

License

MIT.