LinkIntel · Internal

LinkIntel MCP: X Reference Briefs

A paid, read-only MCP server for the selected public X references in your content brief. Add it by URL with a bearer token: no repository clone, no local process and nothing to keep running. prepare_content_brief reads stored evidence only; it never searches X or makes a paid X request. Legacy LinkedIn analytics tools remain optional for existing customers.

Want the agent-ready quick start? Give your assistant the public LinkIntel skill.

POST https://www.getlinkintel.com/api/mcp
Authorization: Bearer lnki_...
Transport
HTTP
Auth
Bearer token
Tools
15 core · 16 with owned X
Refresh
Daily, 06:15 UTC
History
90 days

Install

Three steps. If you signed up through the site, the first is already done and the second is offered to you as a block you can hand straight to your assistant.

The short way

Paid subscribers get their token after Polar entitlement is confirmed, and /setup shows it once alongside a ready-made instruction block to paste into Claude. The manual steps below are for operators minting tokens by hand.

  1. 1

    Get a token

    Tokens map to exactly one LinkIntel profile and start with lnki_. Signing up issues one for you. To mint one by hand instead, use the admin API, authenticating with the Supabase service role key:

    $ curl -s -X POST https://www.getlinkintel.com/api/mcp/tokens     -H "Authorization: Bearer $SUPABASE_SERVICE_ROLE_KEY"     -H "Content-Type: application/json"     -d '{"label":"hermes"}'

    The response carries the token once. Only a hash is stored, so it cannot be shown again. Put it in a password manager now.

    {
      "token": "lnki_...",
      "label": "hermes",
      "profile_id": "...",
      "note": "Store this now. Only a hash is kept, so it cannot be shown again."
    }

    See Managing tokens for listing and revoking. There is also a CLI, if you have the repository checked out:

    $ pnpm --filter @linkintel/private-linkedin-mcp token:new --label="hermes"
  2. 2

    Add the server

    For Claude Code:

    $ claude mcp add --transport http linkintel https://www.getlinkintel.com/api/mcp     --header "Authorization: Bearer $LINKINTEL_TOKEN"
    
    $ claude mcp list | grep linkintel   # expect: Connected

    For Claude Desktop and other clients that read a JSON config:

    {
      "mcpServers": {
        "linkintel": {
          "type": "http",
          "url": "https://www.getlinkintel.com/api/mcp",
          "headers": { "Authorization": "Bearer lnki_..." }
        }
      }
    }

    Prefer an environment variable over pasting the token into a config file that may end up in a backup or a screen share.

  3. 3

    Confirm it works

    Ask the agent to call prepare_content_brief with a topic and cohort you saved in setup. A healthy response includes only stored, cited evidence:

    {
      "topic": "launch lessons",
      "cohort": "developer tools",
      "reference_posts": [{ "citation": { "platform": "x", "url": "https://x.com/..." } }],
      "limitations": ["Stored public-reference evidence only; it is not a reach prediction."]
    }

    To check without an agent, the endpoint speaks plain JSON-RPC over POST:

    $ curl -s -X POST https://www.getlinkintel.com/api/mcp     -H "Authorization: Bearer $LINKINTEL_TOKEN"     -H "Content-Type: application/json"     -H "Accept: application/json, text/event-stream"     -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Tools

The X-first tool is prepare_content_brief. The LinkedIn tools below remain available only when an existing customer has chosen that optional source; owned-X snapshots add one conditional tool when configured.

ToolPurpose
get_data_status()Available platforms and connection state, then LinkedIn freshness, token expiry and request budget. Reach for this first whenever a number looks wrong.
sync_now(days)Report the scheduled-only refresh policy. It never starts a LinkedIn sync.
get_performance_summary(period)Totals, daily series, baseline and previous-period comparison. 7d, 14d, 30d or 90d.
get_daily_trend(metric, days)One metric as a time series with deltas against its baseline.
detect_anomalies(days)Days meaningfully above or below the rolling baseline.
get_post_performance(post_url)Stored lifetime metrics for one registered post.
compare_periods(a, b)Two explicit date ranges, side by side. End dates are exclusive.
log_post(post_url, text, theme)Register a post for the scheduled measurement queue.
get_top_posts(metric, limit)Rank registered posts. Only knows posts added with log_post, because LinkedIn cannot list your posts.
get_content_insights()Registered posts summarised by theme.
prepare_content_brief(topic, cohort)The X-first tool: a cached brief from the signed-in customer’s selected public X reference cohort, with citations, freshness and limitations. It defaults to X references; include legacy LinkedIn metrics only for a clear LinkedIn request.
get_owned_x_performance(limit)Only listed when owned-X collection is configured. Reads stored owner-account snapshots only; it never fetches X or joins a public-reference brief.
import_posts(posts)Register many posts at once from share URNs you already hold. Costs no LinkedIn budget.
import_from_typefully(api_key, account_id)Pull every published LinkedIn post out of a Typefully account. Omit account_id to list the social sets first. Connecting Typefully at /setup instead stores the key, so imports can be repeated as you publish.
import_from_postiz(api_key, account_id, base_url)Pull every published LinkedIn post out of a Postiz account, cloud or self-hosted. Omit account_id to list the channels first. Pass base_url for a self-hosted instance. Connecting Postiz at /setup instead stores the key.
backfill_post_metrics(days)Report registered posts waiting for scheduled measurement. It does not call LinkedIn.

Legacy LinkedIn analytics: what it cannot do

LinkedIn’s official API can measure any post you name, but it cannot list which posts exist. Enumerating a member’s own posts requires the r_member_social permission, which LinkedIn has closed and is not accepting requests for. So get_top_posts and get_content_insights cover only posts registered with log_post.

Account-level analytics are unaffected: all 90 stored days work without registering anything. detect_anomalies is usually the better route to “what worked”, since a standout day points straight at the post responsible.

To fill the corpus in bulk, connect a source at /setup. Typefully and Postiz are the most complete routes because they record every post they published, along with the LinkedIn share URN needed to measure it. Publishing through something else? POST /api/ingest/posts takes a post URL from any tool that can make an HTTP request, authorised by a webhook secret that can register posts and nothing else. See the integrations. Failing that, the 90-day export from LinkedIn’s creator analytics downloads immediately and its post URLs carry the URN type in the slug (-share- or -ugcPost-), which is exactly what the analytics API measures. The full data archive is the weakest option: it takes days and its URLs often carry only an activity id, which cannot be resolved, so those rows are counted and reported rather than registered as posts that could never be measured.

Try it

“What does my saved X reference brief support about this topic, and what is still a guess?”

The agent should call prepare_content_brief with the exact saved topic and cohort, then separate cited observations from hypotheses it suggests testing. It must not search X, import another post, or present reference performance as the customer’s own baseline.

Trusting the numbers

An empty window and a quiet fortnight both produce zeros. Every metric-returning tool carries a freshness block so the agent can tell them apart.

StatusMeaningWhat to do
freshCurrent and complete. At most one day behind, which is normal between UTC midnight and the morning sync.Nothing.
incompleteCurrent, but the window has holes, so totals understate reality.Wait for the scheduled sync.
staleMore than one day behind. The scheduled sync is not keeping up.Check get_data_status and the next scheduled sync.
emptyNo stored metrics at all. The zeros are not measurements.Wait for the scheduled sync.

The block also reports days_stale, how much of the requested window actually has rows, and a plain-language warning when something is off.

Tools degrade rather than fail

Unique period members_reached is not stored for arbitrary date windows, because it can never be summed from daily figures. The tool returns every stored metric and sets reach to null with a reach_unavailable note. A null never becomes a zero, and a missing value is never compared against a real one to manufacture a delta.

"reach_unavailable": "Official unique period reach is not stored for arbitrary date windows...",
"metrics": {
  "impressions":     { "value": 10640, "previous": 9701, "delta": 939,  "percent_change": 9.7 },
  "members_reached": { "value": null,  "previous": null, "delta": null, "percent_change": null }
}

How syncing works

The sync runs automatically every day at 06:15 UTC. Nothing needs to be typed for the data to stay current, and your laptop does not need to be on.

The logic lives in scheduled jobs. MCP requests only read the stored results, so a conversation can never consume the shared LinkedIn allowance.

CallerPurpose
Scheduled nightly jobThree-day self-healing sync

The nightly job keeps stored data current. sync_now reports that policy without triggering a refresh.

Backfills are resumable

  • Per-day windows are fetched newest first, so an interrupted backfill leaves the recent days complete.
  • Days that already hold a value are skipped, so re-running only pays for what is still missing.
  • If the budget runs out mid-run, everything already fetched is saved and the tool returns status: "partial". Nothing is thrown away.
  • Rows where nothing was measured are never written, since their nulls would sum to zero and read as a genuine day of no activity.

Request budget

Every outbound call, including 429 retries, is atomically reserved against a per-profile cap before it reaches LinkedIn.

Safety cap
75 / UTC day
LinkedIn limit
100 / member / day
Headroom
25 requests
3-day sync
~13 requests

Cost is asymmetric, and this drives the whole backfill design:

Metric groupCost3 days90 days
The seven DAILY metrics7 requests for any window size77
LINK_CLICKS1 request per day390
MEMBERS_REACHED1 request per day, skipped above 7 days30

Because a 90-day pull of the core metrics costs only seven requests, long backfills are cheap for everything that matters. Windows longer than seven days skip the daily MEMBERS_REACHED series deliberately: it is non-additive and is never summed into a period total, so it is a diagnostic not worth one request per day.

What repeated use actually costs

Every MCP tool reads or writes LinkIntel storage only, so users can ask questions as often as they like without spending LinkedIn quota.

ToolFirst callRepeat
get_data_status, get_daily_trend, detect_anomalies, get_content_insights00
get_performance_summary, compare_periods00
get_post_performance, log_post00
sync_now0reports scheduled refresh policy

Scheduled jobs persist the metrics they collect. A post published minutes ago may stay pending until the next measurement run, and unique reach remains unavailable for arbitrary period windows rather than triggering a live API call.

When the cap is reached

Scheduled requests are blocked locally before they reach LinkedIn, and the budget resets at UTC midnight. Stored metrics keep working throughout.

Tokens

One token maps to exactly one LinkedIn profile. It is the only thing standing between a request and someone's analytics, so it is treated accordingly.

  • Only a SHA-256 hash is stored. A database leak yields no working credentials, and the raw token exists exactly once, at the moment it is issued.
  • Unknown, malformed and revoked tokens all return the same 401. Nothing distinguishes them, so the endpoint cannot be used to discover which tokens are real.
  • Revocation applies on the very next request. There is no cache to wait out.
  • Every use updates a last-seen timestamp, so a token that is no longer in use is easy to spot and retire.
  • Tokens are not passwords and grant no access to your LinkedIn account. They only read analytics LinkedIn has already published to the profile owner.

Managing tokens

All three operations live at https://www.getlinkintel.com/api/mcp/tokens and take the Supabase service role key as a bearer. That is the right privilege level rather than a new one: anyone holding that key can already write to the tokens table directly, so this endpoint is convenience, not an escalation.

MethodDoesBody
POSTCreate. Returns the raw token once, with 201.{"label","profile_id?"}
GETList. Metadata only; hashes are never selected.none
DELETERevoke. Effective immediately.{"label"} or {"id"}

profile_id is optional while exactly one profile exists. Add a second and it becomes required, so a token can never silently land on the wrong profile.

$ curl -s https://www.getlinkintel.com/api/mcp/tokens     -H "Authorization: Bearer $SUPABASE_SERVICE_ROLE_KEY"

$ curl -s -X DELETE https://www.getlinkintel.com/api/mcp/tokens     -H "Authorization: Bearer $SUPABASE_SERVICE_ROLE_KEY"     -H "Content-Type: application/json"     -d '{"label":"hermes"}'

If a token leaks

Say so and it will be revoked, which takes effect immediately. Nothing else needs to be rotated: the token grants read access to one profile’s analytics and nothing more. Replacing it is a one-line command.

Troubleshooting

SymptomCause and fix
401 A valid bearer token is requiredThe token is missing, mistyped, or has been revoked. All three return the same error on purpose, so nobody can probe for which tokens exist. Check the header is Authorization: Bearer lnki_... and ask for a fresh token if unsure.
405 This endpoint accepts POST onlySomething issued a GET, usually a browser. The endpoint is JSON-RPC over POST and has nothing to show in a browser tab.
connected: false for a legacy analytics toolThe profile behind your token has not added the optional LinkedIn source. X-reference briefs do not require LinkedIn authorisation.
LinkedIn safety budget reachedExpected once 75 requests are spent in a UTC day. Stored metrics still work and live reach returns null. Resets at UTC midnight.
Tools return zerosCheck get_data_status. A status of empty means no data, not no activity.
Failed to connect in claude mcp listConfirm the URL is exactly the endpoint above and the Authorization header actually reached the client. Then try the curl in step 3, which shows the raw error.

Limits

Known constraints, stated plainly, so nobody rediscovers them the hard way.

  • LinkedIn reports roughly a one-day lag, and says aggregate reshares, reactions and comments can differ from its own UI.
  • Period members_reached is fetched as a single official TOTAL window at tool time. Daily reach windows are kept only as a non-additive diagnostic and are never summed into period reach.
  • Days backfilled in a window longer than seven days have no daily members_reached value by design. Period reach is unaffected.
  • FOLLOWER_GAINED_FROM_CONTENT and PROFILE_VIEW_FROM_CONTENT currently return LinkedIn 500s. They are reported as non-blocking warnings and probed weekly, not on every sync.
  • Per-post calls accept normal activity URLs and raw ugcPost or share URNs. Activity IDs are tried as ugcPost first and share on a 404. Posts are never enumerated or scraped.
  • There is no official way to enumerate historical post URNs, which is why log_post exists: the content corpus is built from posts an agent explicitly registers.

The ceiling on going multi-user

LinkedIn’s Development tier allows 500 requests per day across the whole app. At roughly 13 requests per profile per day, that caps the product at about 35 to 40 connected profiles. Getting past it needs a LinkedIn tier increase, not a code change.

Read-only, official API only. Source lives in apps/mcp-server. Deployment and credential runbook steps are kept in the repository README rather than here.