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.
create_or_update_post
Create a new draft post (no post_id) or update an existing one’s text. Text content only — media (images/videos) must be added from the MagicPost web UI for v1. On success returns{success: True, post_id, created: bool} — use post_id (NOT id)
when chaining into schedule_post or publish_post_now.
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.
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.
get_metrics_summary
Get a compact summary of the user’s LinkedIn analytics. Returns top-line totals (impressions, likes, comments, reposts) and the top N posts by impressions. Useperiod 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.
refresh_my_posts
Trigger a fresh pull of the user’s LinkedIn posts and wait until done. Use this when the user explicitly asks to refresh their analytics. The tool polls the job status every 2 seconds until it completes or max_wait_seconds elapses. Default 90s is enough for typical refreshes.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/D + icebreaker).
Get valid ids from
list_icps. - level: qualification level filter (A is the best fit, D the worst).
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/D against it.
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. ⚠️ CONSUMES LEAD CREDITS — 1 credit per scored prospect. 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, 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.
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.
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.