MCP Tools Reference

View as Markdown

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
platform string No Filter by platform: twitter, linkedin, instagram, threads, tiktok, youtube, facebook, bluesky, mastodon, telegram
fromDate string No Start date (ISO 8601): 2026-02-01T00:00:00Z
toDate string No End date (ISO 8601): 2026-02-28T23:59:59Z
page number No Page number (default: 1)
limit number No Results per page (default: 20, max: 100)
sortBy string No Sort field: createdAt, updatedAt, scheduledTime (default: createdAt)
sortOrder string No Sort direction: asc or desc (default: desc)
responseFormat string No detailed (default) returns full post content; concise truncates each post's content to a preview to save tokens

Example prompts:

"Show my scheduled posts"
"List all posts from last week"
"What LinkedIn posts are scheduled for next month?"
"Show me failed posts"
"List my drafts"

Python example:

import asyncio
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
 
async def list_scheduled_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()
 
            # List scheduled posts
            result = await session.call_tool("list_posts", {
                "status": "scheduled",
                "limit": 50
            })
            print(result.content[0].text)
 
asyncio.run(list_scheduled_posts())

Response example:

{
  "success": true,
  "posts": [
    {
      "postGroupId": "67a1b2c3d4e5f6a7b8c9d0e1",
      "content": "Excited to share our latest update!",
      "status": "scheduled",
      "scheduledTime": "2026-02-20T14:00:00Z",
      "platforms": [
        {
          "platformId": "linkedin-123456",
          "platform": "linkedin",
          "status": "scheduled"
        }
      ],
      "createdAt": "2026-02-19T10:30:00Z",
      "updatedAt": "2026-02-19T10:30:00Z",
      "mediaUrls": []
    }
  ],
  "pagination": {
    "totalItems": 1,
    "totalPages": 1,
    "page": 1,
    "limit": 20,
    "hasNextPage": false,
    "hasPrevPage": false
  }
}

create_post

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 422 IDEMPOTENCY_KEY_CONFLICT; retrying while the first call is still running returns 409 IDEMPOTENCY_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_urlcomplete_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. coverUrlcoverurl) is rejected with a validation error rather than silently dropped. These seven platforms accept settings:

{
  "platformSettings": {
    "instagram": {
      "videoType": "REELS | STORIES",
      "shareToFeed": true,
      "coverUrl": "https://.../cover.jpg"
    },
    "tiktok": {
      "viewerSetting": "PUBLIC_TO_EVERYONE | MUTUAL_FOLLOW_FRIENDS | FOLLOWER_OF_CREATOR | SELF_ONLY",
      "allowComments": true, "allowDuet": true, "allowStitch": true,
      "commercialContent": false, "brandOrganic": false, "brandedContent": false
    },
    "youtube": {
      "privacy": "public | unlisted | private",
      "title": "string", "madeForKids": false,
      "tags": ["string"], "categoryId": "string",
      "playlist": { "id": "string", "platformId": "string" }
    },
    "threads": { "replyControl": "everyone | accounts_you_follow | mentioned_only" },
    "twitter": {
      "replyTo": "https://x.com/user/status/123456789 or numeric ID",
      "quoteTweet": "https://x.com/user/status/987654321 or numeric ID"
    },
    "telegram": { "disableNotification": false, "disableWebPagePreview": false, "protectContent": false },
    "linkedin": {
      "repostEnabled": true,
      "repostParentUrn": "urn:li:share:123456",
      "repostVisibility": "PUBLIC | CONNECTIONS"
    }
  }
}

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 asyncio
from datetime import datetime, timedelta
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
 
async 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())

Response example:

{
  "success": true,
  "postGroupId": "67a1b2c3d4e5f6a7b8c9d0e1",
  "scheduledTime": "2026-07-20T09:00:00.000Z"
}

LinkedIn Mentions:

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)

Response example:

{
  "success": true,
  "postGroupId": "67a1b2c3d4e5f6a7b8c9d0e1",
  "status": "scheduled",
  "scheduledTime": "2026-07-20T14:30:00.000Z",
  "platformSettings": {},
  "platforms": ["linkedin-abc123"],
  "posts": [
    {
      "_id": "67a1b2c3d4e5f6a7b8c9d0e2",
      "platform": "linkedin",
      "platformId": "abc123",
      "content": "Excited to share our latest update!",
      "status": "scheduled",
      "postedId": null,
      "permalink": null
    }
  ],
  "media": []
}

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)

Response example:

{
  "success": true,
  "message": "Post updated successfully",
  "scheduledTime": "2026-03-01T15:00:00.000Z",
  "postGroup": {
    "_id": "67a1b2c3d4e5f6a7b8c9d0e1",
    "status": "scheduled",
    "content": "Corrected launch announcement.",
    "platforms": ["linkedin-ABC123", "threads-DEF456"],
    "scheduledTime": "2026-03-01T15:00:00Z"
  }
}

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 aiohttp
 
async 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:

  1. Fast path — pass mediaUrls (public https URLs) to create_post/update_post; the server downloads them. No upload steps.
  2. Upload danceget_upload_urlPUT 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())

Response example:

{
  "connections": [
    {
    "platformId": "twitter-123456789",
    "username": "@yourcompany",
    "displayName": "Your Company",
    "profileImageUrl": "https://pbs.twimg.com/profile_images/...",
    "profileUrl": null,
    "tokenStatus": "valid",
    "tokenExpiresIn": null,
    "accessTokenExpiresAt": null,
    "lastSuccessfulPost": null,
    "lastError": null,
    "subscriptionType": "Premium"
    },
    {
    "platformId": "linkedin-Tz9W5i6ZYG",
    "username": "Your Company Page",
    "displayName": "Your Company",
    "profileImageUrl": "https://media.licdn.com/...",
    "profileUrl": "https://www.linkedin.com/company/your-company",
    "tokenStatus": "valid",
    "tokenExpiresIn": "82d 4h",
    "accessTokenExpiresAt": "2026-06-15T12:00:00.000Z",
    "lastSuccessfulPost": "2026-03-10T09:30:00.000Z",
    "lastError": null,
    "subscriptionType": null
    },
    {
    "platformId": "youtube-UCxxxxxxxxxxxx",
    "username": "Your Channel",
    "displayName": null,
    "profileImageUrl": "https://...",
    "profileUrl": null,
    "tokenStatus": "valid",
    "tokenExpiresIn": null,
    "accessTokenExpiresAt": null,
    "lastSuccessfulPost": "2026-03-11T18:00:00.000Z",
    "lastError": null,
    "subscriptionType": null
    }
  ]
}

Response fields:

Field Type Description
platformId string 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)

Response example:

{
  "success": true,
  "comment": {
    "id": "7434695495614312448",
    "commentUrn": "urn:li:comment:(urn:li:ugcPost:xxx,7434695495614312448)",
    "message": "Great insights! Thanks for sharing."
  }
}

linkedin_delete_comment

Delete a comment from a LinkedIn post.

Parameters:

Parameter Type Required Description
postedId string Yes LinkedIn post URN the comment belongs to
commentId string Yes Comment URN to delete
platformId string Yes Platform connection ID

Example prompts:

"Delete my comment from this post"
"Remove the comment I just posted"

Python example:

async def delete_comment():
    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_delete_comment", {
                "postedId": "urn:li:ugcPost:7429953213384187904",
                "commentId": "urn:li:comment:(urn:li:ugcPost:xxx,7434695495614312448)",
                "platformId": "linkedin-abc123"
            })
            print(result.content[0].text)

LinkedIn Reshare Tool

linkedin_create_reshare

Reshare an existing LinkedIn post to your feed, optionally with commentary.

Parameters:

Parameter Type Required Description
platformId string Yes LinkedIn platform connection ID
parent string Yes Post URN to reshare, such as urn:li:share:123456 or urn:li:ugcPost:123456
commentary string No Commentary shown above the reshare (maximum 3,000 characters)
visibility string No PUBLIC or CONNECTIONS (default: PUBLIC; CONNECTIONS is for personal accounts)

Example prompt:

"Reshare urn:li:share:123456 on linkedin-abc123 with the commentary 'Worth reading'"

LinkedIn Feed Retrieval Tools (Coming Soon — Requires LinkedIn Approval)

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)

Response example:

{
  "success": true,
  "posts": [
    {
      "id": "urn:li:share:7123456789",
      "commentary": "Excited to share our latest product update!",
      "publishedAt": 1709294400000,
      "visibility": "PUBLIC",
      "lifecycleState": "PUBLISHED"
    }
  ],
  "pagination": {
    "start": 0,
    "count": 10,
    "total": 45
  },
  "cached": false
}

linkedin_post_comments

Retrieve comments on a specific 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
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)

Response example:

{
  "success": true,
  "comments": [
    {
      "id": "6636062862760562688",
      "commentUrn": "urn:li:comment:(urn:li:activity:123,6636062862760562688)",
      "actor": "urn:li:person:xxx",
      "message": "Great insights! Thanks for sharing.",
      "created": { "time": 1582160678569 },
      "likesSummary": { "totalLikes": 5 },
      "commentsCount": 2
    }
  ],
  "pagination": {
    "start": 0,
    "count": 20,
    "total": 89
  },
  "cached": false
}

linkedin_post_reactions

Retrieve reactions on a specific 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
start number No Starting index for pagination (default: 0)
count number No Number of reactions to retrieve (default: 50, max: 100)

Example prompts:

"Who liked my LinkedIn post?"
"Show reactions on this post"
"Get all reactions on my announcement"
"List people who reacted to my post"

Python example:

async def get_post_reactions():
    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_reactions", {
                "postedId": "urn:li:ugcPost:7429953213384187904",
                "platformId": "linkedin-abc123",
                "count": 100
            })
            print(result.content[0].text)

Response example:

{
  "success": true,
  "reactions": [
    {
      "id": "urn:li:reaction:(urn:li:person:xxx,urn:li:activity:123)",
      "reactionType": "LIKE",
      "actor": "urn:li:person:xxx",
      "created": { "time": 1686183251857 }
    },
    {
      "id": "urn:li:reaction:(urn:li:person:yyy,urn:li:activity:123)",
      "reactionType": "PRAISE",
      "actor": "urn:li:person:yyy",
      "created": { "time": 1686183300000 }
    }
  ],
  "pagination": {
    "start": 0,
    "count": 50,
    "total": 456
  },
  "cached": false
}

Reaction types returned:

Type Description
LIKE Standard thumbs up
PRAISE Clapping hands
EMPATHY Heart/love
INTEREST Lightbulb (insightful)
APPRECIATION Thank you
ENTERTAINMENT Funny/laughing

Error Handling

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:

Error Message Cause
"API key required..." Missing or invalid API key
"Platform ID not found" Invalid platform connection ID
"Post not found" Invalid post group ID
"API error: 429" Rate limited by the API
"API error: 502" Platform API temporarily unavailable

Example error handling in Python:

try:
    result = await session.call_tool("create_post", {
        "content": "Hello world!",
        "platforms": ["linkedin-abc123"],
        "scheduledTime": "2026-03-01T14:00:00Z"
    })
    print(result.content[0].text)
except Exception as e:
    print(f"Tool error: {e}")

Best Practices

  1. Always get connections first — Use list_connections to get valid platform IDs before creating posts

  2. Use ISO 8601 dates — All dates should be in format 2026-03-01T14:00:00Z

  3. Respect character limits — Check platform limits before creating posts

  4. Handle errors gracefully — Check for error responses in tool results

  5. Batch operations — When posting to multiple platforms, include all platform IDs in a single create_post call