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.
- 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:
- 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 with
ready_to_use=true can 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', 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.
| Input | Type | Default | Description |
|---|
subject | string | — | Subject |
type | any | null | Type |
language | any | null | Language |
tone | any | null | Tone |
hook | any | null | Hook |
guideline | any | null | Guideline |
signature | any | null | Signature |
writing_style_urn | any | null | Writing Style Urn |
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).
| Input | Type | Default | Description |
|---|
query | string | — | Query |
limit | integer | 10 | Limit |
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.
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.
| Input | Type | Default | Description |
|---|
profile_url | string | — | Profile Url |
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.
| Input | Type | Default | Description |
|---|
status | string | 'scheduled' | Status |
limit | integer | 20 | Limit |
offset | integer | 0 | Offset |
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:
- Call
list_my_posts(status="published").
- Find the row whose
linked_in_url_post contains the URN.
- Use that row’s
id here.
Posts published outside MagicPost are not in this table and cannot
be fetched with this tool.
| Input | Type | Default | Description |
|---|
post_id | string | — | Post Id |
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.
| Input | Type | Default | Description |
|---|
content | string | — | Content |
post_id | any | null | Post Id |
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.
| Input | Type | Default | Description |
|---|
post_id | string | — | Post Id |
at_iso | string | — | At Iso |
timezone | any | null | Timezone |
confirm_same_day | boolean | true | Confirm Same Day |
confirm_exact_slot | boolean | true | Confirm Exact Slot |
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).
| Input | Type | Default | Description |
|---|
post_id | string | — | Post Id |
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.
| Input | Type | Default | Description |
|---|
post_id | string | — | Post Id |
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. 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_post contains the URN.
- 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.
| Input | Type | Default | Description |
|---|
period | string | '30d' | Period |
top_n | integer | 5 | Top N |
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.
| Input | Type | Default | Description |
|---|
max_wait_seconds | integer | 90 | Max Wait Seconds |
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)
- 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.
| Input | Type | Default | Description |
|---|
query | any | null | Query |
creator_name | any | null | Creator Name |
poster_urn | any | null | Poster Urn |
languages | any | null | Languages |
formats | any | null | Formats |
post_types | any | null | Post Types |
days_range | any | null | Days Range |
date_from | any | null | Date From |
date_to | any | null | Date To |
min_likes | any | null | Min Likes |
max_likes | any | null | Max Likes |
min_words | any | null | Min Words |
max_words | any | null | Max Words |
sort_by | string | 'top_likes' | Sort By |
page | integer | 1 | Page |
limit | integer | 10 | Limit |
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_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.
- 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}.
| Input | Type | Default | Description |
|---|
icp_id | any | null | Icp Id |
level | any | null | Level |
contact_status | any | null | Contact Status |
search | any | null | Search |
favorite | any | null | Favorite |
limit | integer | 20 | Limit |
offset | integer | 0 | Offset |
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.
| Input | Type | Default | Description |
|---|
prospect_id | string | — | Prospect Id |
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/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 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.
⚠️ 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.
Two-step confirmation flow:
- 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.
- 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.
| Input | Type | Default | Description |
|---|
post_urn | string | — | Post Urn |
icp_ids | any | null | Icp Ids |
confirm | boolean | true | Confirm |
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.
| Input | Type | Default | Description |
|---|
prospect_id | string | — | Prospect Id |
contact_status | any | null | Contact Status |
tags | any | null | Tags |
comment | any | null | Comment |
is_favorite | any | null | Is Favorite |
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, 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.
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.
| Input | Type | Default | Description |
|---|
period | string | '30d' | Period |
member_user_id | any | null | Member User Id |
top_n | integer | 3 | Top N |
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.
| Input | Type | Default | Description |
|---|
member_user_id | any | null | Member User Id |
status | string | 'scheduled' | Status |
limit | integer | 20 | Limit |
offset | integer | 0 | Offset |
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:
list_org_member_posts(member_user_id=..., status=...) → read the
target row’s id.
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.
| Input | Type | Default | Description |
|---|
member_user_id | string | — | Member User Id |
post_id | string | — | Post 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.
| Input | Type | Default | Description |
|---|
member_user_id | string | — | Member User Id |
post_id | string | — | Post Id |
at_iso | string | — | At Iso |
timezone | any | null | Timezone |
confirm_same_day | boolean | true | Confirm Same Day |
confirm_exact_slot | boolean | true | Confirm Exact Slot |