Skip to main content
This page is auto-generated from the MCP server’s tools/list. Run scripts/sync-tools.py after backend changes to refresh it.

generate_post

Generate a new LinkedIn post and persist it as a draft.

Inputs

  • subject: required. What the post should be about. Free text.
  • type: optional. High-level intent: ‘actionable’ → tips, methods, operational lessons ‘aspirational’ → success stories, positive messaging ‘introspective’ → reflections, takes, vented frustration ‘promotional’ → launches, lead magnets, valuesales When omitted, the type is auto-detected from the subject.
  • language: optional. Defaults to the user’s UI preference.
  • tone: optional. Same vocabulary as the UI (‘standard’, ‘friendly’, etc).
  • hook: optional. A custom opening line. Inserted verbatim.
  • guideline: optional. Extra instructions appended to the prompt.
  • signature: optional. Custom signature.
  • writing_style_urn: optional.

Writing style resolution

  • DEFAULT (writing_style_urn empty) — uses the user’s configured style: if their UI has profilePostsActive=true with a profile URN attached, that profile’s recent posts feed the prompt as few-shot examples. If profilePostsActive=false, no style is injected.
  • “Generate in MY style” → leave writing_style_urn empty.
  • “Generate in the style of <other person>” — multi-step flow:
    1. Call list_my_writing_styles() to check if that person is already imported on the user’s account.
    2. If not, call find_writing_style_profile("<name or URL>") to locate a candidate URN with metadata. Profiles with ready_to_use=true can be attached immediately.
    3. Call add_writing_style_profile(profile_url) to attach the profile to the user’s account. If the response is status: 'scraping_in_progress' or 'import_started', tell the user to wait 1-3 minutes and re-call add_writing_style_profile (idempotent — it flips to ‘ready’ once enough posts are scraped).
    4. Once status: 'ready' or 'already_imported', call generate_post(..., writing_style_urn=<the returned urn>). This overrides profilePostsActive for THIS call only.

Rate limits

Capped at 3 generations per minute, 20 per day, and 50 per week. A 429 response indicates which window was hit (rate_limited_generation) — surface the limit reset hint to the user verbatim.

Errors that need user action

  • credits_insufficient (402) → user needs to upgrade.
  • writing_style_not_imported (400) → the URN wasn’t on the user’s account; route through the import flow first.
  • rate_limited_generation (429) → cap hit; surface counts + hint.

list_my_writing_styles

List the LinkedIn profiles the user has imported as writing-style candidates, including which one is currently active in the UI. Use this before find_writing_style_profile when the user asks to generate “in the style of <person>” — if the person is already in their imported list, skip search/import and pass the URN directly to generate_post.

Returns

{success, count, writing_styles: [WritingStyleEntry, ...]} Each WritingStyleEntry has:
  • urn: opaque LinkedIn URN (use it as writing_style_urn)
  • display_name: human-readable name
  • profile_url: full LinkedIn URL (may be null on legacy entries)
  • is_active: True for the currently active style (the UI default)
  • valid_posts: how many of this profile’s posts we have ingested
  • ready_to_use: True iff valid_posts >= 30 (below that the generated style will be weak)
No inputs.

find_writing_style_profile

Search the global LinkedIn profile catalog by name or LinkedIn URL. Use this AFTER list_my_writing_styles, when the person the user wants to mimic is not already imported on their account. The returned results indicate ready_to_use (true iff the profile already has
= 30 valid posts ingested in our DB).
  • query: a person’s full name (e.g. “Gary Vaynerchuk”) OR a LinkedIn URL. Both shapes are handled.
  • limit: up to 25, default 10.

Returns

{success, query, count, results: [...]} Each result has: urn, full_name, headline, profile_url, followers, valid_posts, ready_to_use. After picking a candidate, call add_writing_style_profile(profile_url) to attach it to the user’s account. If the candidate’s ready_to_use is false, the import will still succeed but the import endpoint will return status: 'scraping_in_progress' until enough posts are ingested (~2 minutes).

add_writing_style_profile

Attach a LinkedIn profile to the user’s account as a writing-style candidate, scraping its posts if needed. Idempotent — re-calling with the same URL is safe. Use this exact re-call pattern when the response indicates scraping is in progress.

Inputs

Returns

One of these status values (HTTP status varies):
  • ‘already_imported’ (200) — URN was already on the user’s account. Proceed straight to generate_post.
  • ‘ready’ (200) — profile had >= 30 valid posts. Just attached. Proceed straight to generate_post.
  • ‘scraping_in_progress’ (202) — profile is known but understocked. A scrape was queued; the URN is already attached. Tell the user to wait ~2 minutes and re-call this same tool — it will flip to ‘ready’ once enough posts are scraped.
  • ‘import_started’ (202) — profile was unknown to our DB; an initial scrape was queued. Tell the user to wait ~3 minutes and re-call this tool.
  • ‘quota_exceeded’ (402) — user’s plan limit reached. Surface the hint to the user verbatim.
  • ‘trial_not_eligible’ (403) — trial accounts cannot import new writing styles. The user must upgrade. Generation in the user’s own style is still available.
  • ‘invalid_url’ (400) — URL doesn’t resolve to a LinkedIn profile.
  • ‘import_failed’ (4xx) — could not fetch the profile. Verify the URL is correct and the profile is public.
Each response shape (when present) includes urn, display_name, profile_url, valid_posts. Use the returned urn as writing_style_urn in generate_post once status is ready or already_imported.

list_my_posts

List the user’s posts filtered by lifecycle status.
  • ‘draft’: created but not scheduled and not yet published
  • ‘scheduled’: planified for future publication (sorted by schedule date asc)
  • ‘published’: already on LinkedIn (sorted by created_at desc)
Default is ‘scheduled’ because the most common LLM-driven question is “what’s coming up?”. When the user asks about drafts or published posts explicitly, pass the matching status — do NOT rely on the default.

get_post

Fetch the full record for a single MagicPost owned by the user. OWN POSTS ONLY. This tool returns error_type='post_not_found' (404) for any post you don’t own — including posts of your org teammates, even if you are an org admin. To read a TEAMMATE’s post, use get_org_member_post(member_user_id=..., post_id=...) instead. So if the post id came from list_org_member_posts or relates to another member, do NOT call this tool. post_id MUST be the MagicPost UUID (e.g. “16a63f9a-0e13-43eb-b502-…”). It is NOT the LinkedIn share URN exposed as linkedin_share_urn by get_metrics_summary. Passing a URN returns error_type='invalid_post_id_format' (400). To resolve a linkedin_share_urn into a MagicPost UUID:
  1. Call list_my_posts(status="published").
  2. Find the row whose linked_in_url_post contains the URN.
  3. Use that row’s id here.
Posts published outside MagicPost are not in this table and cannot be fetched with this tool. Besides the raw post record, the response carries:
  • publication: {state, scheduled_at, published_at, linkedin_url, error}. state is one of draft, scheduled, publishing, retrying, published, failed. On failed or retrying, error gives message and, when LinkedIn refused the post, linkedin_status (e.g. 422) and linkedin_body (LinkedIn’s raw response). Check it right after a scheduled time to fix and republish without waiting for the email.
  • scheduled_comments: the comments armed on the post.

create_or_update_post

Create a new draft post (no post_id) or update an existing one’s text. This tool only creates or updates text. To add an image, video, or PDF, use attach_media_to_post for a chat-provided file, or the prepare_post_media_upload / complete_post_media_upload flow for a local file in a code or CLI client. On success returns {success: True, post_id, created: bool} — use post_id (NOT id) when chaining into schedule_post or publish_post_now.

attach_media_to_post

Attach one chat-provided image, video, or PDF to a draft. Call this after create_or_update_post and before schedule_post or publish_post_now. The post must be an unscheduled, unpublished draft, must not contain a poll. Set replace_existing=True to replace ALL existing attachments with this file after validation succeeds. The post ID and scheduled comments are preserved. alt_text is limited to 1000 characters; the separate title is limited to 400 characters. For a draft created by create_member_post, pass the same member_user_id here, then use schedule_member_post. Only an org super_admin can attach media to a member-owned draft. media_file is a temporary file reference supplied by the chat host. Never invent or reuse its download_url: it is consumed during this tool call and copied into permanent MagicPost storage.

prepare_post_media_upload

Prepare a short-lived upload for a local image, video, or PDF. Use this when the MCP client can upload local bytes over HTTPS, such as Claude Code. This remote server cannot read a local file path. Upload the bytes according to upload_request, then call complete_post_media_upload with the returned upload_id. For a member-owned draft, pass the member_user_id returned by list_org_members and pass it again when completing the upload. To replace all existing attachments, pass replace_existing=True here AND when completing. Existing media stays until completion succeeds.

complete_post_media_upload

Validate an uploaded local file and attach it to its draft. Call this after prepare_post_media_upload and the HTTPS upload. For a member-owned draft, pass the same member_user_id used to prepare it. Re-calling a successfully completed upload is safe. Use replace_existing=True to replace all attachments, keeping the post and its comments. alt_text: max 1000 characters; title: max 400.

update_post_media

Edit an existing draft attachment without losing its scheduled comments. Read media_url from get_post’s file_post; never invent it. Cancel any schedule first. Supply alt_text (max 1000 characters) and/or title (max 400); omitted fields stay unchanged, an empty string clears one. This does not replace the file. Use the upload tools with replace_existing=True for replacement. Published posts cannot change.

schedule_post_comment

Arm a comment that MagicPost publishes on LinkedIn under the user’s post, minutes_after minutes after the post goes live (0 = right after publication). Typical use: the “first comment” with the link. The post must be one of the user’s own posts that is NOT published yet: a draft, or a post already scheduled with schedule_post. Arm the comment before or after scheduling, it follows the post’s real publication time. A post can carry several comments; call once per comment.
  • text: max 1250 characters (LinkedIn’s limit).
  • minutes_after: 0 to 10080 (one week). Default 0.
Returns {success, post_id, comment_id, text, minutes_after, ...}. Use comment_id with delete_post_comment to remove it; to change a comment, delete it and schedule a new one. get_post lists the post’s comments under scheduled_comments. Errors: post_already_published (409) when the post is live or being published; failed_tag when a mentioned member can’t be tagged; scheduled_comments_forbidden when the user’s plan doesn’t include scheduled comments (do not retry). The call is not retried automatically: on a network error, check get_post before calling again, or the comment may be armed twice.

delete_post_comment

Remove a comment armed with schedule_post_comment, before it is published on LinkedIn. A comment already published (or being published) cannot be removed here: comment_already_published (409).

list_received_comments

List the comments people left on one of the user’s published LinkedIn posts: text, author (name, headline, profile URL), date, and whether it is a reply (with its parent comment). Pass exactly one of post_id (MagicPost UUID of a post published through MagicPost) or linkedin_activity_id (from list_linkedin_post_metrics, works for any post of the user). Only the user’s own posts are allowed. This reads what MagicPost has already collected, it costs nothing. Comments are collected by a third-party provider, not by LinkedIn’s official API, and only when a sync runs: compare comments_stored with post.comments_on_linkedin and look at last_sync. If comments are missing or stale, call sync_received_comments, wait about a minute, then call this tool again. synced: false means the post is not imported yet (just published): retry later.

sync_received_comments

Fetch the latest comments of one of the user’s published posts from LinkedIn (through MagicPost’s data provider), in the background. Same references as list_received_comments. Uses one unit of the user’s daily comment sync quota (returned in sync_quota); a sync already running or started less than a few minutes ago returns already_running and costs nothing. On comment_sync_quota_reached, do not retry: read the stored comments. Returns status: running right away. Wait about a minute (longer for posts with hundreds of comments), then call list_received_comments: last_sync.state tells whether the sync is done.

score_post

Run MagicPost’s Post AI score on a LinkedIn post: the same scan as the editor’s “Score IA”. Pass plain text to check a post before creating a draft, or the post_id of one of the user’s posts (its current text is scored, unless text is also given). Returns {success, grade, score, fluency_score, language, flagged, post_id, scanner_version}:
  • grade: A (doesn’t sound AI) to E (sounds too AI), the grade shown in the editor.
  • score: 0 to 100, the higher the more AI-sounding.
  • flagged: each flagged passage with excerpt (the exact words), category (the AI tell family, e.g. contrast_formula, here_is), severity (strong tells lower the grade, weak ones are informative) and explanation. Rewrite strong ones first. There is no suggested rewrite: propose your own fix for each passage, keeping the author’s meaning and voice.
Each scan uses the user’s daily AI score quota (a post_id whose text did not change since its last scan is free). On ai_score_quota_reached, do not retry before reset_at.

add_idea_to_notebook

Save a post idea to the user’s MagicPost Idea notebook, to write later. The idea appears at the top of the notebook’s backlog column, like an idea typed in the app. Use this when the user asks things like:
  • “Save this idea in my MagicPost notebook”
  • “Add these 3 topics to my ideas” For several ideas, call the tool once per idea.

Parameters

  • idea: the idea itself, short (a topic, an angle, a hook). Max 2000 characters.
  • notes: optional longer context kept with the idea (sources, angle, personal anecdote, outline). Max 20000 characters.
This does NOT write a post. To draft a post now, use generate_post or create_or_update_post.

Returns

{success, idea_id, idea, notes, created_at}. The call is not retried: on a network error, check with the user before calling again, or the idea may be saved twice.

schedule_post

Schedule a post for future publication on LinkedIn.
  • at_iso: ISO 8601 datetime. If naive (no offset), it is interpreted in the user’s timezone. If it has an offset, it is converted.
  • timezone: optional IANA tz name override (e.g. ‘Europe/Paris’). Falls back to the user’s stored timezone, then UTC.
  • confirm_same_day: when True (default), the call returns 200 with success: false, warning: 'same_day_posts' if the user already has other posts scheduled on the same day. Show the warning to the user, and retry with confirm_same_day=false only after they confirm.
  • confirm_exact_slot: same behavior for posts scheduled within ±5 minutes (a stronger signal of a likely mistake). This check is evaluated FIRST and short-circuits the same-day check, so an exact-slot warning (warning: 'exact_slot_conflict') will be returned alone even if there are other same-day posts.
When LinkedIn token is expired, returns actionable: true with a hint — surface it to the user verbatim, do not retry blindly. Returns error_type='daily_post_limit_reached' (429) with limit and retry_at when this slot would exceed the number of posts the LinkedIn profile may publish per 24 hours (published and scheduled posts both count). Tell the user and pick a later slot: do not retry.

cancel_scheduled_post

Cancel a scheduled post. Idempotent: returns success even if the post is no longer scheduled. Returns 409 if the post is already published (cannot un-publish).

publish_post_now

Publish a post to LinkedIn immediately (no schedule). On LinkedIn token expired, returns actionable: true with a reconnect hint. Returns 409 if the post is already published. Returns error_type='daily_post_limit_reached' (429) with limit and retry_at when the LinkedIn profile already reached its number of posts per 24 hours: tell the user, do not retry.

list_linkedin_post_metrics

List per-post LinkedIn metrics for an inclusive date range. Includes posts published natively on LinkedIn as well as posts published through MagicPost, provided they have been imported into MagicPost analytics. Use YYYY-MM-DD for date_from and date_to. Results are ordered newest first and paginated. When has_more is true, call this tool again with cursor=next_cursor and the same date range. Continue until has_more is false. linkedin_activity_id and linkedin_post_id are not MagicPost UUIDs and cannot be passed to editing, scheduling, or publishing tools. Metrics that were not collected are null, not 0. Beyond impressions, reactions, comments and reposts, each post carries LinkedIn’s extended analytics: reach (unique members who saw the post), saves, link_clicks, premium_cta_clicks, profile_views (profile visits generated by the post), sends (times the post was sent in LinkedIn messages) and followers_gained (followers LinkedIn attributes to the post). They are null until LinkedIn has been asked for them, which advanced_metrics_available tells you (advanced_metrics_updated_at gives the date). A 0 means LinkedIn answered zero, so never read a null as a zero. When the connected identity is a company page, LinkedIn does not measure saves, premium_cta_clicks, profile_views, sends or followers_gained at all: they come back null and are listed in advanced_metrics_unavailable_for_profile_type. Say the metric does not exist for pages rather than reporting zero.

get_metrics_summary

Get a compact summary of the user’s LinkedIn analytics. Returns top-line totals, follower growth for the selected period, and the top N posts by impressions. totals and averages_per_post cover impressions, likes, comments and reposts, plus LinkedIn’s extended analytics: reach (unique members reached), saves, link_clicks, premium_cta_clicks, profile_views, sends and followers_gained (followers LinkedIn attributes to the posts). These are null when no post in the window carries them, and their average is computed over the posts that do, not over every post in the window. advanced_metrics_coverage.posts_with_data gives that count per metric, so compare it to totals.posts_count before reading an extended figure as a period total. On a company page, LinkedIn does not measure saves, premium_cta_clicks, profile_views, sends or followers_gained: they are null and listed in advanced_metrics_coverage.unavailable_for_profile_type, so say the metric does not exist for pages rather than reporting zero. followers.gained is null when LinkedIn did not provide enough snapshots to cover the whole period. Check followers.coverage; available_range_gained is explicitly limited to the returned coverage dates. followers.daily returns compact {date, gained} points, so use one multi-day call instead of one request per day when building a roll-up. These are profile-level daily gains, not followers attributed to a specific post. Use period to scope the window. For most questions like “how did I perform recently?” use ‘30d’. Identifiers — IMPORTANT: top_posts[].linkedin_share_urn is the LinkedIn share URN, NOT the MagicPost UUID expected by get_post, schedule_post, cancel_scheduled_post, publish_post_now, or create_or_update_post. Passing it to those tools returns error_type='invalid_post_id_format'. To act on a top post, resolve the URN to a MagicPost UUID:
  1. Call list_my_posts(status="published").
  2. Find the row whose linked_in_url_post contains the URN.
  3. Use that row’s id (UUID) with get_post / schedule_post / etc.
Posts published outside MagicPost have no MagicPost UUID and cannot be acted on via the MCP tools.

get_metrics_freshness

Check when LinkedIn post metrics were last updated. Call this before refresh_my_posts. When should_refresh is false, reuse the stored metrics instead of starting another refresh. When no metrics timestamp exists, data_available is false and should_refresh is true.

refresh_my_posts

Trigger a fresh pull of the user’s LinkedIn posts and wait until done. Check get_metrics_freshness first. If should_refresh is false, do not start another refresh unless the user explicitly requires a forced refresh. It is a slow, expensive call: the backend scrapes LinkedIn synchronously and only responds once the refresh is finished, which can take up to a minute or two. Call it once and wait for the result — do NOT call it again back-to-back if it errors, as each call re-triggers the full scrape. max_wait_seconds is only an upper bound on how long this tool waits; with the current backend the call returns as soon as the refresh is done, so the default is fine. If the result ever comes back with status ‘timeout’, the refresh is still running in the background — tell the user to check back shortly instead of calling again.

search_inspiration_posts

Search the global LinkedIn inspiration catalogue (NOT the user’s own posts) for high-performing examples to draw inspiration from. Use this when the user asks things like:
  • “Find me top posts about <topic>”
  • “Show me viral French posts on remote work this month”
  • “What’s <creator> posting that gets traction?”

Filters

  • query: free-text. Matches against post text + theme (PostgreSQL FTS).
  • creator_name: partial match on poster’s display name. ⚠️ ILIKE scan — slower than poster_urn. Prefer poster_urn when you already have it (e.g. from a previous result).
  • poster_urn: exact LinkedIn URN — indexed, fast.
  • languages: e.g. [“French”, “English”]. Use the pycountry English name (not ISO codes, not native names).
  • formats: subset of {text, image, video, carousel, article}.
  • post_types: subset of {actionable, aspirational, introspective, promotional}. Case-insensitive on input; backend matches Capitalize.
  • days_range: shortcut “posts from the last N days”. Ignored when date_from/date_to are set.
  • date_from / date_to: ISO date strings (YYYY-MM-DD). Inclusive.
  • min_likes / max_likes / min_words / max_words: numeric bounds. Default minimums (20 likes, 80 words, 5 comments) are applied by the backend to keep the catalogue noise-free even when filters are sparse.

Sorting

  • ‘recent’ → posted_at DESC
  • ‘top_likes’ → num_likes DESC (default — best for inspiration)
  • ‘top_comments’ → num_comments DESC (proxy for discussion-driving)

Pagination

  • page: 1-indexed. Start at 1, increment to walk through results.
  • limit: up to 12 per call. Default 10. The response includes has_more so you know when to stop.

Returns

{success, page, per_page, total, has_more, count, posts, filters_applied} Each post has: id, text (full), theme, type, format, language, posted_at (ISO), num_likes, num_comments, num_reposts, words, poster_name, poster_urn, poster_headline, profile_followers_count, post_url, image_url, video_url, document_cover_urls.

Common workflows

  • “Get me 5 great hooks on <topic>”: query=<topic>, limit=5, sort_by=‘top_likes’.
  • “Latest carousels by <creator>”: creator_name=<name>, formats=[‘carousel’], sort_by=‘recent’.
  • “Inspiration close to what I’m writing”: pass the user’s draft theme as query, leave other filters open.

list_leads

List the user’s LinkedIn leads (prospects detected from the engagement on their posts and qualified against their ICPs).

Filters

  • icp_id: scope the list to one ICP. When set, each lead embeds its qualification against that ICP (level A/B/C + icebreaker). Get valid ids from list_icps.
  • level: qualification level filter (A is the best fit, C the weakest listed — D-scored profiles are not leads: never billed, never listed). Only effective when icp_id is also provided.
  • contact_status: pipeline status slug. Defaults are ‘not_contacted’, ‘contacted’, ‘in_discussion’, ‘to_follow_up’, ‘won’, ‘lost’, ‘not_relevant’ (users can customize them). ⚠️ A lead that was never touched has contact_status null, and this filter only matches explicitly-set statuses — so filtering on ‘not_contacted’ does NOT return untouched leads. To find leads the user hasn’t contacted yet, list WITHOUT this filter and read each item’s contact_status (null or ‘not_contacted’ = not contacted).
  • search: free-text match on the lead’s name/headline.
  • favorite: True to show only favorited leads.

Pagination

  • limit: up to 25 per call (default 20).
  • offset: 0-indexed. Use offset + has_more from the response to paginate.
Returns {items, count, limit, offset, has_more}.

get_lead

Full detail of one lead: profile, interactions (likes/comments on the user’s posts), qualification reasoning, icebreaker, and CRM sync state (deliveries to connected external CRMs). Get prospect_id values from list_leads.

list_icps

List the user’s ICPs (Ideal Customer Profiles). Needed to get the icp_id values used by list_leads (scoping) and detect_leads (targeting). Each ICP describes the kind of prospect the user wants to detect; leads are scored A/B/C against it (a D-scored profile is not a lead: free, and never listed). No inputs.

get_leads_summary

Leads quota status: trial state (is_trial, trial_days_remaining, trial_ends_at), monthly quota (leads_monthly_included, leads_consumed_in_period, leads_remaining), top-ups (topup_leads_remaining, topups_detail), and renewal date (period_end). Call this before detect_leads if the user asks how many credits they have left. No inputs.

get_detection_status

Poll the status of running lead detections (they are async and typically take 2-15 minutes). Returns {in_progress, scoring_in_progress, all_terminal}. all_terminal: true means every detection is done — the new leads are available via list_leads. No inputs.

detect_leads

Detect new leads from the engagement (likes + comments) on one of the user’s LinkedIn posts. post_urn accepts a numeric activity ID, urn:li:activity:…, or the share_urn returned by get_post (urn:li:share:… / urn:li:ugcPost:…). The post must be synced to the connected LinkedIn profile; direct reposts are excluded. A post_not_found error requires correcting the reference or connected profile, not retrying the same call. ⚠️ CONSUMES LEAD CREDITS — 1 credit per prospect scored with at least a Medium ICP fit (level A/B/C; weak D fits are free and discarded). The exact cost is NOT known upfront: every engager matching an ICP gets scored (potentially dozens on a popular post), so never promise a number. ⚠️ OTHER SIDE EFFECTS — a completed detection also:
  • pushes the qualified leads to the user’s connected external CRMs (Pipedrive/HubSpot/Lemlist/Zapier) — an external write that is hard to undo;
  • sends a recap email if the user enabled it. The confirm-step response tells you which apply (crm_destinations, email_recap_enabled) — relay them to the user before confirming.
Two-step confirmation flow:
  1. First call with confirm=true (default): returns success: false, warning: 'credits_consumption' with the targeted ICPs, the remaining credits, crm_destinations (enabled CRM push rules; empty list = no CRM push) and email_recap_enabled. Show all of this to the user and ask them to confirm.
  2. After the user confirms, retry with confirm=false to actually start the detection (returns success: true with a queued payload).
  • icp_ids: optional list of ICP ids to score against (from list_icps). Defaults to all the user’s active ICPs.
  • Detection runs in the background — poll get_detection_status afterwards. Returns error_type: 'leads_quota_exhausted' (402) when no credits remain.
  • Re-detecting an already-processed post is a NO-OP until its engagement has grown: the response comes back with success: false and queued: 0 (already_synced in results[]). Tell the user nothing was launched in that case — do not claim a detection started.
  • results[] can MIX queued and rejected ICPs (e.g. queued: 2 with one already_synced entry). Report both to the user: which ICPs were launched and which were skipped, and why.

update_lead

Update a lead’s pipeline fields. At least one field is required.
  • contact_status: pipeline status slug (see list_leads for the default values; users can customize them).
  • tags: full replacement of the lead’s tag list.
  • comment: free-text note on the lead.
  • is_favorite: star/unstar the lead.
Only the provided fields are modified. Returns the updated lead.

list_org_members

List the active members of your organisation. Only callable by org super_admins. Returns name, email, status, whether each member is themselves an org_admin, and whether their LinkedIn account is currently connected to MagicPost. Use this first when the user asks anything team-scoped, so you know the set of valid member_user_id values to pass to the other org tools. No inputs.

get_org_metrics_summary

Compact analytics for the org or one specific member.
  • When member_user_id is omitted: returns one compact summary per team member (name, totals, follower growth, top posts). Use this for “how is the team doing?” questions.
  • When member_user_id is set: returns ONE detailed summary for that member, same shape as get_metrics_summary for the caller’s own account.
top_n defaults to 3 in team view (response stays token-efficient for an LLM); set higher when drilling into a single member. summary.followers.gained is null when LinkedIn did not provide enough snapshots to cover the whole period. Check follower coverage; available_range_gained only covers the returned dates. summary.followers.daily contains profile-level daily gains. Identifiers — IMPORTANT: top_posts[].linkedin_share_urn is the LinkedIn share URN, NOT the MagicPost UUID expected by get_post, get_org_member_post, schedule_post, cancel_scheduled_post, publish_post_now, schedule_member_post, or create_or_update_post. Passing it to those tools returns error_type='invalid_post_id_format'. To act on a top post, resolve the URN to a MagicPost UUID via list_org_member_posts(member_user_id=..., status="published") and match the URN against the row’s linked_in_url_post field. Then read its full record with get_org_member_post. Posts published outside MagicPost have no MagicPost UUID and cannot be acted on.

list_org_member_posts

List posts across the org, optionally scoped to one member.
  • When member_user_id is set: only that member’s posts. Useful for “show me Alice’s drafts” or “what is Bob scheduling next?”.
  • When omitted: posts grouped by member (each with limit posts starting at offset). Use for “what’s the team posting this week?”.
status is the lifecycle filter (draft / scheduled / published), same semantics as list_my_posts. Members and members’ posts are returned even if the LLM client doesn’t have a direct relationship with them — org-admin permission is sufficient. Each returned post row exposes its MagicPost UUID as id. To read ONE post’s full record (full text, media, stats…), pass that id to get_org_member_post(member_user_id=..., post_id=...) — NOT to get_post, which only reads YOUR own posts and returns post_not_found for a teammate’s post.

get_org_member_post

Fetch the full record of ONE post owned by a member of your org. Org-admin read counterpart to get_post (which only reads YOUR own posts). Use this when you already have a member’s post UUID — typically from list_org_member_posts — and need its full content/metadata (full text, schedule date, LinkedIn URL, stats, media, etc.). Only callable by org super_admins. Both arguments are REQUIRED:
  • member_user_id: the post owner’s user id, exactly as returned by list_org_members (user_record_id) or list_org_member_posts. It must be a member of YOUR org.
  • post_id: the MagicPost UUID of the post (e.g. “df1cbd56-a7c0-41f4-b929-5d242bed12eb”), as returned in the id field by list_org_member_posts. This is NOT the linkedin_share_urn from get_org_metrics_summary — passing a URN returns error_type='invalid_post_id_format' (400).
Typical chain:
  1. list_org_member_posts(member_user_id=..., status=...) → read the target row’s id.
  2. get_org_member_post(member_user_id=..., post_id=<that id>).
Error shapes:
  • error_type='org_admin_required' (403): you are not an org admin.
  • error_type='member_not_in_org' (404): member_user_id is not in your org.
  • error_type='post_not_found' (404): the post does not exist, is deleted, or does NOT belong to member_user_id (IDOR guard — you cannot read another member’s post by passing the wrong owner).
Do NOT use get_post for a teammate’s post — it is scoped to your own posts and will return post_not_found for anything you don’t own.

create_member_post

Create a draft owned by a specific organisation member. Only callable by org super_admins. Get member_user_id from list_org_members. The returned post_id belongs to that member and can be passed to schedule_member_post. To attach media first, call attach_media_to_post with both the returned post_id and the same member_user_id.

schedule_member_post

Schedule a post on behalf of an org member. Only callable by org super_admins. The post must belong to member_user_id (you can’t move someone else’s post by accident). Defaults:
  • timezone falls back to the member’s stored userTimezone (NOT yours). Set it explicitly if you want to schedule in your own timezone instead.
  • at_iso is interpreted in that timezone unless the string already has an offset.
Same warning flow as schedule_post (same_day_posts / exact_slot_conflict) — computed against the MEMBER’s existing scheduled posts. Show the warning to the user, then retry with confirm_*=false to force through. If the member’s LinkedIn token has expired, the response has actionable: true with a hint telling you to ASK THE MEMBER to reconnect — only they can, not you. Surface that hint verbatim.