Complete reference for the 14 active Publora MCP tools with parameters, examples, and code snippets. Media can be attached two ways: the fast path (pass public https URLs via mediaUrls on create_post/update_post) or the upload dance (get_upload_url → HTTP PUT → complete_media). Three additional LinkedIn feed-retrieval tools (linkedin_posts, linkedin_post_comments, linkedin_post_reactions) are pending LinkedIn approval of the r_member_social permission — see LinkedIn Feed Retrieval Tools below. LinkedIn analytics and workspace-management features are available via the REST OpenAPI reference, not MCP.
Note: Most tools return the backend API object. list_connections deliberately wraps the backend list as { "connections": [...] } for MCP structured content. list_posts also supports a concise mode that truncates content previews and adds response-format metadata.
Posts Tools
list_posts
List posts with optional filters for status, platform, and date range.
Parameters:
Parameter
Type
Required
Description
status
string
No
Filter by status: draft, scheduled, published, failed, partially_published
Create and schedule a post to one or more platforms.
Parameters:
Parameter
Type
Required
Description
content
string
Yes
Post text content
platforms
string[]
Yes
Array of platform connection IDs (from list_connections)
scheduledTime
string
No
When to publish (ISO 8601): 2026-03-01T14:00:00Z. Omit to create a draft.
mediaUrls
string[]
No
Up to 10 public https image/video URLs, downloaded server-side and attached before validation. Pass together with scheduledTime to attach and schedule in one call.
platformSettings
object
No
Per-platform publishing options (see schema below). Strict — unknown platforms or keys are rejected.
idempotencyKey
string
No
Retry key (min length 1), forwarded as the Idempotency-Key header. Reusing it with the identical request replays the original response without creating another post.
Draft behavior:scheduledTime is optional in both MCP and REST — omit it to create a draft. Publishable media-required platforms (Instagram, TikTok, YouTube) must be created as a draft first (or given media via mediaUrls), then scheduled once media is attached. Pinterest is connect-only: passing media may satisfy registry validation, but it cannot be published because scheduler dispatch is not implemented.
Use idempotencyKey whenever a retry is possible. An agent that retries after a network timeout has no way to know whether the first create_post landed — without a key it creates a second post. Pass a fresh unique key (e.g. a UUID) per distinct post; on retry, resend the same key with the same arguments and Publora replays the original result instead of posting again. Reusing a key with different arguments returns 422IDEMPOTENCY_KEY_CONFLICT; retrying while the first call is still running returns 409IDEMPOTENCY_IN_FLIGHT (wait and retry the identical call — do not switch keys).
A scheduledTime in the past is not taken literally. Under 5 minutes late it is always clamped with SCHEDULED_TIME_COERCED. At 5+ minutes it is scheduled to become 400 SCHEDULED_TIME_IN_PAST on 2026-08-25, unless production configuration overrides the date either way.
Publishable media-required platforms (Instagram, TikTok, YouTube): scheduling one of these with no media fails validation with MEDIA_REQUIRED (HTTP 400, { "error": "Validation failed", "validation": {…} }; the error's suggestions name the exact recovery tool calls). To satisfy it: pass mediaUrls in the same create_post call, or create a draft (omit scheduledTime), attach with get_upload_url → complete_media, then update_post with status: "scheduled". Do not schedule Pinterest; it is connect-only.
platformSettings via MCP — supported on create_post and update_post. The schema is strict: a mistyped platform or key (e.g. coverUrl → coverurl) is rejected with a validation error rather than silently dropped. These seven platforms accept settings:
For X, replyTo and quoteTweet are intended for inbound engagement and your own posts. Self-serve X API tiers accept the target only when its author mentioned the connected account in that same post, quoted one of its posts, or the connected account wrote the target itself. The fields may be combined, and quote posts may include media. Publora cannot prevalidate the relationship: an unrelated target is accepted by the tool but fails permanently at publish time with X_REPLY_NOT_AUTHORIZED.
For LinkedIn repost settings, CONNECTIONS is personal-profile-only. A company-page repost must use PUBLIC or scheduling returns 400.
YouTube custom thumbnails are not settable here (they need the separate multipart thumbnail endpoint, which MCP does not expose).
Example prompts:
"Schedule 'Hello world!' to LinkedIn for tomorrow at 9am""Post 'We're hiring!' to Twitter and LinkedIn right now""Schedule this announcement to all my accounts for Monday"
Python example:
import asynciofrom datetime import datetime, timedeltafrom mcp import ClientSessionfrom mcp.client.streamable_http import streamablehttp_clientasync def schedule_post(): headers = {"Authorization": "Bearer sk_YOUR_API_KEY"} async with streamablehttp_client("https://mcp.publora.com", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() # First get connections to find platform IDs connections = await session.call_tool("list_connections", {}) # Schedule a post for tomorrow at 9am UTC tomorrow_9am = (datetime.utcnow() + timedelta(days=1)).replace( hour=9, minute=0, second=0, microsecond=0 ).isoformat() + "Z" result = await session.call_tool("create_post", { "content": "Excited to share our latest product update!", "platforms": ["linkedin-abc123"], "scheduledTime": tomorrow_9am }) print(result.content[0].text)asyncio.run(schedule_post())
You can @mention people and companies in LinkedIn posts using this syntax in your content:
@{urn:li:person:ACoAABcD1234EfG|Serge Bulaev} # Mention a person@{urn:li:organization:107107343|Acme Corp Inc} # Mention a company
Example:
"Great insights from @{urn:li:person:ACoAABcD1234EfG|Serge Bulaev} at @{urn:li:organization:107107343|Creative Content Crafts Inc}!"
Important: The display name must exactly match the LinkedIn profile name (case-sensitive), including company suffixes like "Inc", "LLC", etc.
Platform limits:
Platform
Characters
Images
Video
Special Features
LinkedIn
3,000
10
500MB
Documents, @mentions
X/Twitter
280 (25K premium)
4
140s
Auto-threading
Instagram
2,200
10
900s Reels, 3600s feed, 60s carousel
Reels & Stories supported
Threads
500 (10,000 with text attachment)
20
5min / 1 GB
Threading disabled
TikTok
2,200
35
10min / 4 GB
Image carousel or video
YouTube
5,000 desc
0
12h / 256 GB
Shorts support
Facebook
63,206
10
45min / 2 GB
Page posts, Reels
Bluesky
300
4
3 min / 100 MB
Auto-facet detection
Mastodon
500*
4
~99 MB
Instance-variable
Telegram
4,096 (1,024 captions)
10
24h / 50 MB
Automatic Markdown parsing
*Varies by instance
Note: LinkedIn's "10 images" is the multi-image upload limit, not a carousel. Organic carousels on LinkedIn are not supported via the API (carousels are only available for sponsored/ad content). To share multi-page content organically, use LinkedIn document (PDF) posts instead.
Note: Multi-threaded nested posts on Threads are temporarily unavailable. Single posts and carousel posts to Threads continue to work normally.
get_post
Get details of a specific post group.
Parameters:
Parameter
Type
Required
Description
postGroupId
string
Yes
Post group ID (e.g., 67a1b2c3d4e5f6a7b8c9d0e1)
Example prompts:
"Show me the details of post 67a1b2c3d4e5f6a7b8c9d0e1""What's the status of my last scheduled post?""Get info about my recent post"
Python example:
async def get_post_details(): headers = {"Authorization": "Bearer sk_YOUR_API_KEY"} async with streamablehttp_client("https://mcp.publora.com", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool("get_post", { "postGroupId": "67a1b2c3d4e5f6a7b8c9d0e1" }) print(result.content[0].text)
Note:get_post returns group-level status, scheduledTime, platformSettings, platforms, and media[], plus one posts[] entry per platform target. Each post includes nullable platformId, postedId, and permalink; internal thread-part IDs are not exposed.
update_post
Edit a draft or scheduled post — its text, its target accounts, its schedule, or its status — without deleting and recreating it.
Parameters:
Parameter
Type
Required
Description
postGroupId
string
Yes
Post group ID
content
string
No
Replacement base post text. Rewrites every target that has no explicit per-account override. "" is allowed while the post stays a draft.
platforms
string[]
No
Replacement target set — replaces the whole array, it is not merged. Call list_connections first and copy each platformId verbatim. [] is allowed only while the post remains a draft.
status
string
No
New status: draft or scheduled
scheduledTime
string
No
New scheduled time (ISO 8601)
mediaUrls
string[]
No
Public https URLs (≤10) to download and append to the post's media.
platformSettings
object
No
Per-platform options to merge (same strict schema as create_post).
idempotencyKey
string
No
Retry key (min length 1), forwarded as the Idempotency-Key header. Reusing it with the identical request prevents repeated media appends and replays the original response.
Note: Provide at least one of content, platforms, status, scheduledTime, mediaUrls, or platformSettings. update_post is not idempotent by default — repeating a call with mediaUrls appends the same media a second time. Pass idempotencyKey to make a retry safe: the repeated call replays the original response instead of appending again.
Editing content and targets: a content edit rewrites each platform post to its effective text and preserves explicit per-account overrides. A platforms edit deletes the platform posts for IDs you drop and creates them for IDs you add — added connections are validated for ownership and plan entitlement, and adding a target to a scheduled post re-runs scheduling limits plus full content/media validation. Scheduling with an empty target set returns PLATFORMS_REQUIRED. The write is all-or-nothing: a rejected edit leaves the post exactly as it was.
Not editable: published, failed, or partially published posts return 400 POST_NOT_EDITABLE. A post already publishing — or with a platform post in pending/processing — returns 409 POST_PUBLISH_IN_PROGRESS, and an edit that loses a race with another write returns 409 POST_GROUP_VERSION_CONFLICT. On either 409, call get_post before retrying instead of repeating the call blindly.
scheduledTime handling: omitting it keeps the current time. Under 5 minutes late is always clamped with SCHEDULED_TIME_COERCED; 5+ minutes is scheduled to become strict on 2026-08-25 unless production configuration overrides the date either way.
Example prompts:
"Fix the typo in tomorrow's post — it should say 'launch', not 'lunch'""Add my Threads account to that scheduled post""Drop LinkedIn from Friday's post and keep the rest""Reschedule post 67a1b2c3d4e5f6a7b8c9d0e1 to Friday at 3pm""Change my draft to scheduled""Move tomorrow's post to next week""Update the post to publish at 10am instead"
Python example:
async def edit_post(): headers = {"Authorization": "Bearer sk_YOUR_API_KEY"} async with streamablehttp_client("https://mcp.publora.com", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() # platforms REPLACES the stored set — read the current IDs first. await session.call_tool("list_connections", {}) result = await session.call_tool("update_post", { "postGroupId": "67a1b2c3d4e5f6a7b8c9d0e1", "content": "Corrected launch announcement.", "platforms": ["linkedin-ABC123", "threads-DEF456"], "scheduledTime": "2026-03-01T15:00:00Z" }) print(result.content[0].text)
Successful responses may carry warnings. When present, warnings is an array of { code, message, ... } objects (e.g. SCHEDULED_TIME_COERCED, which also carries requested and effective). The key is omitted entirely when there are no warnings. Surface these to the user — the call succeeded, but the API changed something about the request. Full list: Error Handling.
Note: Top-level scheduledTime is always present and is null for drafts. The nested postGroup.scheduledTime is conditional and appears only when a scheduled time is set.
Note: Only posts with status draft or scheduled can be updated. Attempting to update a post in any other status (e.g., published, failed, partially_published) returns a 400 error: "Cannot update post: post is currently in {status} status".
delete_post
Delete a post from all platforms.
Parameters:
Parameter
Type
Required
Description
postGroupId
string
Yes
Post group ID to delete
Example prompts:
"Delete post 67a1b2c3d4e5f6a7b8c9d0e1""Cancel my scheduled post for tomorrow""Remove all my draft posts"
Python example:
async def delete_post(): headers = {"Authorization": "Bearer sk_YOUR_API_KEY"} async with streamablehttp_client("https://mcp.publora.com", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool("delete_post", { "postGroupId": "67a1b2c3d4e5f6a7b8c9d0e1" }) print(result.content[0].text)
Response example:
{ "success": true}
get_upload_url
Get a presigned URL to upload media files.
Parameters:
Parameter
Type
Required
Description
postGroupId
string
Yes
Post group ID to attach media to
fileName
string
Yes
File name (e.g., photo.jpg)
contentType
string
Yes
MIME type (e.g., image/jpeg, video/mp4)
type
string
Yes
Media type: image or video
Upload-layer acceptance:
Type
Formats
Images
The MCP tool accepts an image/* MIME string; scheduling applies the target platform's format allowlist
Videos
The MCP tool accepts a video/* MIME string; scheduling applies the target platform's format allowlist
⚠ Attaching media demotes a scheduled post to draft. Calling get_upload_url on an already-scheduled post demotes it back to draft (postGroupDemoted: true in the response). Attach media on a draft, then schedule with update_post. Companion media tools: complete_media (finalize/validate an uploaded mediaId — optional, the scheduling gate probes lazily) and delete_media (remove one attached media file; also demotes a scheduled post). Media attached via mediaUrls needs neither.
Python example:
import aiohttpasync def upload_image_to_post(): headers = {"Authorization": "Bearer sk_YOUR_API_KEY"} async with streamablehttp_client("https://mcp.publora.com", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() # Get upload URL result = await session.call_tool("get_upload_url", { "postGroupId": "67a1b2c3d4e5f6a7b8c9d0e1", "fileName": "product-photo.jpg", "contentType": "image/jpeg", "type": "image" }) # Parse the response - contains uploadUrl, fileUrl, and mediaId # Response: { "success": true, "uploadUrl": "https://...", "fileUrl": "https://...", "mediaId": "..." } import json data = json.loads(result.content[0].text) upload_url = data["uploadUrl"] # Upload file using the presigned URL async with aiohttp.ClientSession() as http: with open("product-photo.jpg", "rb") as f: await http.put(upload_url, data=f.read())
complete_media
Finalize a file uploaded via get_upload_url (probes the object, persists type/metadata). Call it after the presigned PUT succeeds. (Optional — scheduling also finalizes pending media — but calling it early surfaces format/probe errors before publish.) Not needed for media attached via mediaUrls.
Parameters:
Parameter
Type
Required
Description
mediaId
string
Yes
The mediaId returned by get_upload_url.
delete_media
Remove a media slot from a post (detaches and deletes the underlying file). Deleting media from a scheduled post demotes it to draft — re-schedule with update_post afterward.
Parameters:
Parameter
Type
Required
Description
mediaId
string
Yes
The mediaId of the slot to remove (see the media array in get_post).
Two ways to attach media:
Fast path — pass mediaUrls (public https URLs) to create_post/update_post; the server downloads them. No upload steps.
Upload dance — get_upload_url → PUT the bytes to the presigned URL → complete_media. Use delete_media to drop a slot.
Connections Tool
list_connections
List all connected social media accounts.
Parameters: None
Example prompts:
"Show my connected accounts""What platforms am I connected to?""List my social media accounts""Which accounts do I have linked?"
Python example:
async def list_connections(): headers = {"Authorization": "Bearer sk_YOUR_API_KEY"} async with streamablehttp_client("https://mcp.publora.com", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool("list_connections", {}) print(result.content[0].text)asyncio.run(list_connections())
Unique ID for creating posts (e.g., twitter-123456789)
username
string
Platform username or handle
displayName
string
Display name on the platform
profileImageUrl
string
Profile image URL
profileUrl
string/null
URL to the profile on the platform
tokenStatus
string
Token health: valid, expiring_soon, expired, unknown. The authoritative signal — see the note below.
tokenExpiresIn
string/null
Human-readable time until expiration (e.g., "7d 3h"); null when there is no expiry date to report
accessTokenExpiresAt
string/null
Effective credential expiry, derived the same way as tokenStatus. null when no authoritative date exists — always for YouTube, and for platforms that do not expire on a schedule (Facebook, X/Twitter, Mastodon, Bluesky).
lastSuccessfulPost
string/null
ISO 8601 timestamp of the last successful post via this connection
lastError
object/null
Last error details: { message: string, occurredAt: string }
subscriptionType
string/null
Detected platform subscription tier when available (used for X Premium/PremiumPlus limits)
Deciding whether a connection needs reconnecting: read tokenStatus. Do not compare accessTokenExpiresAt against the current date.
tokenStatus already accounts for how each platform refreshes credentials and for connections the platform has revoked. Comparing the date yourself produces wrong advice in both directions: a healthy YouTube connection reports null (its token is refreshed on demand before each publish, and Google publishes no refresh-token lifetime), while a revoked connection can report expired with a date that is null or still in the future. Prompt the user to reconnect when tokenStatus is expired, and warn when it is expiring_soon.
LinkedIn Reactions
linkedin_create_reaction
React to a LinkedIn post.
Parameters:
Parameter
Type
Required
Description
postedId
string
Yes
LinkedIn post URN
platformId
string
Yes
Platform connection ID
reactionType
string
Yes
Reaction type (see below)
Reaction types:
Type
Description
LIKE
Standard like
PRAISE
Clapping hands
EMPATHY
Heart/love
INTEREST
Lightbulb/insightful
APPRECIATION
Thank you
ENTERTAINMENT
Funny/laughing
Example prompts:
"Like this LinkedIn post""React with PRAISE to post xyz""Add a heart reaction to my colleague's post"
Python example:
async def react_to_post(): headers = {"Authorization": "Bearer sk_YOUR_API_KEY"} async with streamablehttp_client("https://mcp.publora.com", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool("linkedin_create_reaction", { "postedId": "urn:li:share:7123456789", "platformId": "linkedin-abc123", "reactionType": "LIKE" }) print(result.content[0].text)
linkedin_delete_reaction
Remove a reaction from a LinkedIn post.
Parameters:
Parameter
Type
Required
Description
postedId
string
Yes
LinkedIn post URN
platformId
string
Yes
Platform connection ID
Example prompts:
"Remove my reaction from this post""Unlike the LinkedIn post"
LinkedIn Comment Tools
linkedin_create_comment
Post a comment on a LinkedIn post.
Parameters:
Parameter
Type
Required
Description
postedId
string
Yes
LinkedIn post URN (e.g., urn:li:share:123456 or urn:li:ugcPost:123456)
platformId
string
Yes
Platform connection ID
message
string
Yes
Raw input up to 10,000 characters; after mention processing, the text sent to LinkedIn must be at most 1,250 characters. Supports mentions: @{urn:li:person:ID|Name} or @{urn:li:organization:ID|Company}
parentComment
string
No
Parent comment URN for nested replies
Example prompts:
"Comment 'Great insights!' on this LinkedIn post""Post a comment on my latest LinkedIn update""Reply to this comment with 'Thanks for sharing!'""Comment on this post mentioning @{urn:li:person:ACoAABcD1234EfG|Jane Smith}"
Python example:
async def comment_on_post(): headers = {"Authorization": "Bearer sk_YOUR_API_KEY"} async with streamablehttp_client("https://mcp.publora.com", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool("linkedin_create_comment", { "postedId": "urn:li:ugcPost:7429953213384187904", "platformId": "linkedin-abc123", "message": "Great insights! Thanks for sharing." }) print(result.content[0].text)
Status: DISABLED - These tools are not yet available. They require the r_member_social permission, which is RESTRICTED and requires LinkedIn approval. The implementation is ready and will be enabled once LinkedIn approves the permission for Publora.
linkedin_posts
Retrieve posts authored by a LinkedIn account.
Parameters:
Parameter
Type
Required
Description
platformId
string
Yes
Platform connection ID (e.g., linkedin-XxxYyy)
start
number
No
Starting index for pagination (default: 0)
count
number
No
Number of posts to retrieve (default: 10, max: 100)
sortBy
string
No
Sort order: LAST_MODIFIED or CREATED (default: LAST_MODIFIED)
Example prompts:
"Show my recent LinkedIn posts""Get my last 10 LinkedIn posts""What have I posted on LinkedIn this month?""List my LinkedIn content"
Python example:
async def get_my_linkedin_posts(): headers = {"Authorization": "Bearer sk_YOUR_API_KEY"} async with streamablehttp_client("https://mcp.publora.com", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool("linkedin_posts", { "platformId": "linkedin-abc123", "count": 20, "sortBy": "LAST_MODIFIED" }) print(result.content[0].text)
LinkedIn post URN (e.g., urn:li:share:123456 or urn:li:ugcPost:123456)
platformId
string
Yes
Platform connection ID
start
number
No
Starting index for pagination (default: 0)
count
number
No
Number of comments to retrieve (default: 20, max: 100)
Example prompts:
"Show comments on my last LinkedIn post""Get all comments on this LinkedIn post""What are people saying about my post?""List comments on post urn:li:share:123456"
Python example:
async def get_post_comments(): headers = {"Authorization": "Bearer sk_YOUR_API_KEY"} async with streamablehttp_client("https://mcp.publora.com", headers=headers) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() result = await session.call_tool("linkedin_post_comments", { "postedId": "urn:li:ugcPost:7429953213384187904", "platformId": "linkedin-abc123", "count": 50 }) print(result.content[0].text)
Every successful tool result carries both a text block and a machine-readable structuredContent object (MCP spec). On failure the tool surfaces the backend's full structured payload — code, platform, and suggestions where available — not just a bare message. Common errors include: