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=truewith a profile URN attached, that profile’s recent posts feed the prompt as few-shot examples. IfprofilePostsActive=false, no style is injected. - “Generate in MY style” → leave writing_style_urn empty.
- “Generate in the style of
<other person>” — multi-step flow:- Call list_my_writing_styles() to check if that person is already imported on the user’s account.
- If not, call
find_writing_style_profile("<name or URL>")to locate a candidate URN with metadata. Profiles withready_to_use=truecan be attached immediately. - 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). - Once
status: 'ready'or'already_imported', callgenerate_post(..., writing_style_urn=<the returned urn>). This overridesprofilePostsActivefor 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)
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 indicateready_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
- profile_url: full LinkedIn URL (https://www.linkedin.com/in/…).
Returns
One of thesestatus 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.
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)
get_post
Fetch the full record for a single MagicPost owned by the user. OWN POSTS ONLY. This tool returnserror_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:
- Call
list_my_posts(status="published"). - Find the row whose
linked_in_url_postcontains the URN. - Use that row’s
idhere.
post record, the response carries:
publication:{state, scheduled_at, published_at, linkedin_url, error}.stateis one of draft, scheduled, publishing, retrying, published, failed. On failed or retrying,errorgivesmessageand, when LinkedIn refused the post,linkedin_status(e.g. 422) andlinkedin_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 bycreate_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 toupload_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 samemember_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.
{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 ofpost_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 insync_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 plaintext 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(strongtells lower the grade,weakones are informative) andexplanation. Rewrite strong ones first. There is no suggested rewrite: propose your own fix for each passage, keeping the author’s meaning and voice.
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.
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.
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, returnsactionable: 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 fordate_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:
- Call
list_my_posts(status="published"). - Find the row whose
linked_in_url_postcontains the URN. - Use that row’s
id(UUID) withget_post/schedule_post/ etc.
get_metrics_freshness
Check when LinkedIn post metrics were last updated. Call this beforerefresh_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. Checkget_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. Preferposter_urnwhen 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_toare 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_moreso 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_idis 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_morefrom the response to paginate.
{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 theicp_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 beforedetect_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.
- 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) andemail_recap_enabled. Show all of this to the user and ask them to confirm. - After the user confirms, retry with confirm=false to actually
start the detection (returns
success: truewith 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_statusafterwards. Returnserror_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: falseandqueued: 0(already_syncedin 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: 2with onealready_syncedentry). 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_leadsfor 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.
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 validmember_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_idis 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_idis set: returns ONE detailed summary for that member, same shape asget_metrics_summaryfor 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_idis 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
limitposts starting atoffset). 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 toget_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 bylist_org_members(user_record_id) orlist_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 theidfield bylist_org_member_posts. This is NOT thelinkedin_share_urnfromget_org_metrics_summary— passing a URN returnserror_type='invalid_post_id_format'(400).
list_org_member_posts(member_user_id=..., status=...)→ read the target row’sid.get_org_member_post(member_user_id=..., post_id=<that id>).
error_type='org_admin_required'(403): you are not an org admin.error_type='member_not_in_org'(404):member_user_idis not in your org.error_type='post_not_found'(404): the post does not exist, is deleted, or does NOT belong tomember_user_id(IDOR guard — you cannot read another member’s post by passing the wrong owner).
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. Getmember_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 tomember_user_id (you can’t move someone else’s post by accident).
Defaults:
timezonefalls back to the member’s stored userTimezone (NOT yours). Set it explicitly if you want to schedule in your own timezone instead.at_isois interpreted in that timezone unless the string already has an offset.
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.