A paid, read-only MCP server for the selected public X references in your content brief. Add it by URL; OAuth 2.1 handles authentication in your browser with no token to type or share. prepare_content_brief reads stored evidence only; it never searches X or makes a live X request. Legacy LinkedIn analytics tools remain optional for existing customers.
Want the agent-ready quick start? Give your assistant the public LinkIntel skill. Machine-readable: OpenAPI, MCP server card.
POST https://www.getlinkintel.com/api/mcp OAuth 2.1 · Authorization: Bearer (from OAuth flow or fallback token)
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.
For most clients and most users: add the server by URL, and the browser handles sign-in and payment. OAuth 2.1 works on Claude Code, Claude.ai, Claude Desktop, and any MCP client that supports the authorization spec. If your client does not support OAuth, fall back to the bearer token handoff from /setup. The manual steps below are for operators and fallback scenarios.
OAuth 2.1 is the recommended path. Add the endpoint by URL; no token needed upfront. When the client makes its first request, it gets a 401 with authorization details. The browser opens for Google sign-in, checkout if you are new, and consent. After that, the client authenticates with the OAuth token it obtained.
For clients without OAuth support, skip to step 2 (bearer token fallback).
If your client does not support OAuth 2.1, get a bearer token instead. Go to https://www.getlinkintel.com/agent, create an account with Google, review and confirm the $39/month offer, then copy the private MCP handoff shown on /setup.
Tokens start with lnki_ and map to exactly one profile.
For Claude Code, add the server with:
$ 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.
If using OAuth 2.1, make your first MCP request. The client will get a 401 with authorization details and open the browser for sign-in. If using a bearer token, no additional auth step is needed. Either way, ask the agent to call list_references, then prepare_content_brief with one of the topic and cohort pairs it returns. 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"}'The X-first path is find_account_posts to discover by handle, add_references to collect, and prepare_content_brief to write from what is stored. 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, and reference collection adds two.
| Tool | Purpose |
|---|---|
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. |
find_account_posts(platform, handle, count) | Only listed when reference collection is configured. Looks up a public account's most recent original posts (replies and reposts excluded) by handle and returns URLs, dates and short previews. Draws on the same daily read budget as add_references and stores nothing. |
add_references(platform, urls, topic, cohort, confirmed_by_user) | Only listed when reference collection is configured. Stores the public X posts you supply as reference evidence. Included in your subscription; the agent confirms the exact list with you on the call. Reads only the URLs given, and never searches X. |
list_references(include_posts) | The stored reference topics and cohorts with their post counts. Reads stored data only, and gives prepare_content_brief its exact labels. Set include_posts to see each group's individual posts before removing any. |
remove_references(platform, urls, topic, cohort, confirmed_by_user) | Deletes stored reference posts: specific URLs, or a whole topic and cohort group. The agent confirms the exact posts or group with you first. Never reaches X or any other platform. |
prepare_content_brief(topic, cohort, platform) | A cached brief from the signed-in customer’s selected public X reference cohort, with citations, freshness and limitations. Set platform to "x" (default) or "linkedin" to pick which of the customer's own posts it reads, since length and structure do not transfer between platforms: owned X posts are recent and unfiltered by topic, owned LinkedIn posts are theme-filtered, and a LinkedIn brief marks the always-X stored references cross_platform: true. |
get_owned_performance(platform, 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. Makes no LinkedIn request. |
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 /account 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 /account instead stores the key. |
backfill_post_metrics(days) | Report registered posts waiting for scheduled measurement. It does not call LinkedIn. |
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 /account. 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.
“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.
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.
| Status | Meaning | What to do |
|---|---|---|
| fresh | Current and complete. At most one day behind, which is normal between UTC midnight and the morning sync. | Nothing. |
| incomplete | Current, but the window has holes, so totals understate reality. | Wait for the scheduled sync. |
| stale | More than one day behind. The scheduled sync is not keeping up. | Check get_data_status and the next scheduled sync. |
| empty | No 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.
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 }
}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 request budget.
| Caller | Purpose |
|---|---|
| Scheduled nightly job | Three-day self-healing sync |
The nightly job keeps stored data current. sync_now reports that policy without triggering a refresh.
status: "partial". Nothing is thrown away.Every outbound call, including 429 retries, is atomically reserved against a per-profile cap before it reaches LinkedIn.
Cost is asymmetric, and this drives the whole backfill design:
| Metric group | Cost | 3 days | 90 days |
|---|---|---|---|
| The seven DAILY metrics | 7 requests for any window size | 7 | 7 |
LINK_CLICKS | 1 request per day | 3 | 90 |
MEMBERS_REACHED | 1 request per day, skipped above 7 days | 3 | 0 |
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.
Every MCP tool reads or writes LinkIntel storage only, so users can ask questions as often as they like without spending LinkedIn quota.
| Tool | First call | Repeat |
|---|---|---|
get_data_status, get_daily_trend, detect_anomalies, get_content_insights | 0 | 0 |
get_performance_summary, compare_periods | 0 | 0 |
get_post_performance, log_post | 0 | 0 |
sync_now | 0 | reports 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.
Scheduled requests are blocked locally before they reach LinkedIn, and the budget resets at UTC midnight. Stored metrics keep working throughout.
One token maps to exactly one LinkIntel profile. It is the only thing standing between a request and someone's stored references and analytics, so it is treated accordingly.
401. Nothing distinguishes them, so the endpoint cannot be used to discover which tokens are real.Email support@getlinkintel.com and it will be revoked, which takes effect on the very next request. Nothing else needs to be rotated: the token only reads and writes one profile’s LinkIntel data. OAuth clients never see a long-lived token at all.
| Symptom | Cause and fix |
|---|---|
401 A valid bearer token is required | The 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 only | Something 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 tool | The profile behind your token has not added the optional LinkedIn source. X-reference briefs do not require LinkedIn authorisation. |
LinkedIn safety budget reached | Expected once 75 requests are spent in a UTC day. Stored metrics still work and live reach returns null. Resets at UTC midnight. |
Tools return zeros | Check get_data_status. A status of empty means no data, not no activity. |
Failed to connect in claude mcp list | Confirm 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. |
Known constraints, stated plainly, so nobody rediscovers them the hard way.
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.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.ugcPost or share URNs. Activity IDs are tried as ugcPost first and share on a 404. Posts are never enumerated or scraped.log_post exists: the content corpus is built from posts an agent explicitly registers.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.