Tools
The eight MCP tools — arguments, what comes back, and what each one refuses.
Last updated on
Every tool is scoped to your workspace by the connection itself — none of them takes an organization or workspace argument. A failed call comes back as a normal tool result with an error message, not a broken connection.
Every tool also declares an output schema and returns structured content, so an assistant reads typed fields rather than parsing text.
Resources
Three read-only resources sit alongside the tools. An assistant fetches them when it needs them, so they cost nothing until then.
| URI | What it holds |
|---|---|
postonus://workspace/brand-voice | How your workspace wants to sound. Worth reading before drafting anything. |
postonus://platforms/rules | Character limits, media counts and types, thread and first-comment support, per platform. |
postonus://workspace/quota | Your plan and how much of this month's per-platform allowance is left. |
list_channels
No arguments.
Returns your connected channels — each with its health and the limits the platform enforces — plus the active workspace:
{
"workspace": { "id": "org_...", "name": "Acme", "timezone": "Europe/Berlin", "otherWorkspaces": [] },
"channels": [
{
"id": "int_...", "platform": "BLUESKY", "name": "acme.bsky.social",
"connectStatus": "ACTIVE", "tokenStatus": "VALID", "healthy": true,
"maxChars": 300, "maxCharsWithMedia": null, "countUnit": "grapheme",
"supportsThreads": true, "supportsFirstComment": false, "maxFirstCommentChars": null,
"maxMediaCount": 4, "supportedMediaTypes": ["IMAGE", "VIDEO", "GIF"]
}
]
}The limits are there so an assistant can size a post before calling schedule_post instead of discovering each one from a rejection. countUnit: "grapheme" means the platform counts a family emoji as one character where String.length would charge eleven.
otherWorkspaces lists workspaces you belong to that this connection cannot reach. A connection made over OAuth resolves your default workspace only; use an API key bound to a specific workspace to reach another one.
get_best_times
| Argument | Type | Required |
|---|---|---|
platforms | array of platform names | yes, at least one |
count | number, 1–20 | no, defaults to 5 |
Returns upcoming send times as concrete instants, soonest first — pass one straight to schedule_post:
{
"timezone": "Europe/Berlin",
"slots": [
{ "scheduledAt": "2026-09-01T06:00:00.000Z", "platforms": ["LINKEDIN", "LINKEDIN_PAGE"], "strength": 10, "source": "consensus" }
],
"platformsWithoutData": ["BLUESKY"]
}source says where a slot came from: consensus when several of the platforms you asked about share a window, platform when they do not intersect and each platform's own windows are used, default when we hold no research for any of them. platformsWithoutData names the platforms in the last case, so a generic answer is never mistaken for a researched one.
This is the same resolution useOptimalTime performs, so the two always agree.
list_queue
| Argument | Type | Required |
|---|---|---|
statuses | array of DRAFT, SCHEDULED, PUBLISHING, PUBLISHED, FAILED | no, defaults to ["DRAFT", "SCHEDULED", "FAILED"] |
limit | number, 1–100 | no, defaults to 20 |
Returns posts with their per-platform schedules. The default covers everything you can still act on; widen statuses to read published history.
This is where postId and scheduleId come from, so anything you intend to edit, delete, or retry has to be listed here first.
schedule_post
| Argument | Type | Required |
|---|---|---|
content | string | one of this or thread |
thread | array of 2–25 strings | one of this or content |
platforms | array of platform names | one of this or channelIds |
channelIds | array of channel ids | one of this or platforms |
scheduledAt | ISO 8601 datetime | one of this, useOptimalTime, or asDraft |
useOptimalTime | boolean | one of this, scheduledAt, or asDraft |
asDraft | boolean | one of this, scheduledAt, or useOptimalTime |
media | array of references from import_media | no |
perChannelContent | map of channel id to text | no |
firstComment | map of channel id to text | no |
Stages a post. It never publishes immediately — there is no "publish now" option here, unlike the raw API's PUBLISH_NOW.
Returns the post's resulting state: its text, media, and every platform's schedule id, time, and status, plus a dashboard link. You never have to read the queue back to find out what your own call did.
Targeting a specific channel
platforms posts to every connected channel on those platforms. channelIds names exact channels, which is the only way to pick between two pages on the same platform.
Per-channel copy
perChannelContent maps a channel id to text that replaces the shared body on that channel. This is how one idea ships as 280 characters on X and 3000 on LinkedIn in a single post, rather than being written down to the smallest limit.
Media
Import each file with import_media first, then pass what it returned in media, adding altText.
Threads
Pass thread instead of content and you get a real reply chain, not a run of separate posts: the segments publish in order, each one replying to the last. You choose where the breaks land, so numbering and punchlines stay where you put them.
{ "thread": ["1/3 the setup", "2/3 the middle", "3/3 the payoff"], "platforms": ["BLUESKY"], "scheduledAt": "2026-09-01T09:00:00Z" }Each segment is checked against the platform's limit on its own, and each one spends a post of your monthly allowance — a 5-segment thread costs 5. The post's own text is the segments joined together, which is what you see in list_queue and on the calendar.
Refuses when:
- No body, no target, or no timing is given. All the missing pieces are named in one message rather than one per call.
- Both
contentandthread, bothplatformsandchannelIds, orasDrafttogether with a send time. - None of the requested platforms has a connected, active channel.
contentor any thread segment is longer than the platform allows — it is rejected, not truncated. CheckmaxCharsfromlist_channelsfirst.- A target platform does not publish threads. Facebook takes a long post directly, so there is nothing to split and the call is refused rather than flattened.
supportsThreadsonlist_channelssays which platforms can. - The thread is over 25 segments.
- Your workspace has hit its monthly post quota for a platform, or cannot schedule at all (a Free workspace with no card) — see Limits.
edit_post
| Argument | Type | Required |
|---|---|---|
postId | string | yes |
content | string | no, one of this or thread |
thread | array of 2–25 strings | no, one of this or content |
platforms | array of platform names | no, one of this or channelIds |
channelIds | array of channel ids | no, one of this or platforms |
scheduledAt | ISO 8601 datetime | no, one of this or scheduleTimes |
scheduleTimes | array of { scheduleId, scheduledAt } | no, one of this or scheduledAt |
media | array of references from import_media | no |
perChannelContent | map of channel id to text | no |
firstComment | map of channel id to text | no |
Rewrites a staged post in place, so fixing a typo does not leave a second post on the calendar. Pass only what changes — anything you omit stays as it is, including the attached media and the time. Returns the post's resulting state.
Moving a post
scheduledAt moves every platform's copy. scheduleTimes moves named copies individually, which is what makes one post go out at different times on different platforms — deliberate when you want it, surprising when you do not, so the two are separate arguments and you cannot pass both.
This replaces the old reschedule_post tool, which moved a single platform's schedule while looking like it moved the post.
platforms and channelIds replace the target channels rather than adding to them: a post staged to Bluesky and LinkedIn, edited with platforms: ["BLUESKY"], loses its LinkedIn schedule. media likewise replaces the whole media set. An edit never changes a post's status — a draft stays a draft, a scheduled post stays scheduled.
Refuses when:
- Nothing is passed to change.
- Both
contentandthread, bothplatformsandchannelIds, or bothscheduledAtandscheduleTimes. - Any schedule on the post has already published.
- The post is publishing right now. Wait for it to finish.
retry_schedule
| Argument | Type | Required |
|---|---|---|
scheduleId | string | yes |
Re-queues one failed platform copy of a post. Find the id with list_queue and statuses: ["FAILED"]. Returns the post's resulting state.
Only a failed schedule can be retried. If the failure was the content or a disconnected channel, fix that first — a retry on its own will fail the same way.
import_media
| Argument | Type | Required |
|---|---|---|
url | public URL of an image or video | yes |
Copies a file into your workspace so it can be attached to a post:
{ "mediaId": "med_...", "url": "https://cdn.postonus.com/...", "type": "IMAGE", "bytes": 184320 }Pass that object back in schedule_post or edit_post under media, adding altText.
Refuses a private or local address, anything that is not an image or video, and files over 200MB.
create_media_upload
| Argument | Type | Required |
|---|---|---|
fileName | name to store the file under | yes |
fileType | MIME type, e.g. image/png | yes |
fileSize | size in bytes | yes |
checksum | base64 SHA-256 of the bytes | yes |
For a file you hold the bytes of, when there is no public URL to give import_media. Reserves
storage against your plan quota and returns a presigned upload:
{
"uploadId": "upl_...",
"uploadUrl": "https://...",
"requiredHeaders": { "content-type": "image/png", "x-amz-checksum-sha256": "..." },
"expiresAt": "2026-08-25T10:00:00.000Z"
}PUT the bytes to uploadUrl with exactly those headers, then call complete_media_upload. Storage
verifies the checksum, so a body that does not match it is rejected.
Single-part only, which caps a file at 100MB. Use postonus media upload from the CLI for anything
larger — it splits the file into parts.
complete_media_upload
| Argument | Type | Required |
|---|---|---|
uploadId | from create_media_upload | yes |
checksum | the same digest you sent | yes |
Turns a finished upload into a media row, and returns the same shape import_media does:
{ "mediaId": "med_...", "url": "https://cdn.postonus.com/...", "type": "IMAGE", "bytes": 184320 }The media row only exists after this call. Calling it twice with the same checksum is safe.
delete_post
| Argument | Type | Required |
|---|---|---|
postId | string | yes |
Deletes a post and every schedule and thread segment on it. This cannot be undone. The tool is marked destructive, so an assistant that respects tool annotations asks you before calling it.
Returns { "deleted": true, "postId": "pst_..." }.
Refuses when:
- Any schedule on the post has already published. A published post is real on a real platform, and deleting the local row would leave it with no record here.
- Someone else in the workspace created the post. Delete those from the dashboard.
A caller using an x-api-key gets no prompt, because there is nobody to prompt. Treat a key that can reach delete_post as a key that can delete any unpublished post its user created.