protogrid

Search / com.ihateposting/mcp

iHatePosting

R1· needs a key

Draft, check and schedule posts to your social accounts from Claude, ChatGPT, Cursor or VS Code.

v0.12.4 · active · repository · website · descriptor JSON · versions

An agent can connect on its own if it already holds api_key.Remote endpoint behind a static API key or header. The registry never sees the value; the agent fills the placeholder.
Quality75good
Trust96of 100
Uptime, 30 days100%
Latency p50199 ms

Connect

8 clients · secrets stay placeholders
{
  "mcpServers": {
    "ihateposting": {
      "type": "http",
      "url": "https://ihateposting.com/api/mcp",
      "headers": {
        "Authorization": "Bearer ${api_key}"
      }
    }
  }
}
claude mcp add --transport http ihateposting https://ihateposting.com/api/mcp --header "Authorization: Bearer ${api_key}"
[mcp_servers.ihateposting]
url = "https://ihateposting.com/api/mcp"
bearer_token_env_var = "api_key"
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "ihateposting": {
      "type": "remote",
      "url": "https://ihateposting.com/api/mcp",
      "enabled": true,
      "headers": {
        "Authorization": "Bearer {env:api_key}"
      }
    }
  }
}
{
  "mcpServers": {
    "ihateposting": {
      "type": "http",
      "url": "https://ihateposting.com/api/mcp",
      "headers": {
        "Authorization": "Bearer ${api_key}"
      }
    }
  },
  "deeplink": "cursor://anysphere.cursor-deeplink/mcp/install?name=ihateposting&config=eyJ0eXBlIjoiaHR0cCIsInVybCI6Imh0dHBzOi8vaWhhdGVwb3N0aW5nLmNvbS9hcGkvbWNwIiwiaGVhZGVycyI6eyJBdXRob3JpemF0aW9uIjoiQmVhcmVyICR7YXBpX2tleX0ifX0%3D"
}
{
  "servers": {
    "ihateposting": {
      "type": "http",
      "url": "https://ihateposting.com/api/mcp",
      "headers": {
        "Authorization": "Bearer ${api_key}"
      }
    }
  }
}
{
  "mcpServers": {
    "ihateposting": {
      "httpUrl": "https://ihateposting.com/api/mcp",
      "headers": {
        "Authorization": "Bearer ${api_key}"
      }
    }
  }
}
extensions:
  "ihateposting":
    enabled: true
    name: "ihateposting"
    type: streamable_http
    uri: "https://ihateposting.com/api/mcp"
    headers:
      "Authorization": "Bearer ${api_key}"
    timeout: 300

Placeholders to fill from your own secret store: ${api_key}. The registry never accepts secret values.

Quality 75 / 100good

protocol88 · weight 25
  • pass
    protocol.modernsupports 2026-07-28 (server/discover)
  • pass
    protocol.statelessanswers without a session
  • pass
    protocol.transportstreamable HTTP
  • warn
    protocol.list_ttltools/list declares ttlMs 0, so clients re-fetch it on every use
authorization83 · weight 15
  • pass
    auth.prmProtected Resource Metadata (RFC 9728) published
  • pass
    auth.as_metadataauthorization server metadata reachable
  • warn
    auth.cimdonly Dynamic Client Registration, deprecated in 2026-07-28 in favour of CIMD
  • n/a
    auth.secret_in_urlno templated URL
tool hygiene86 · weight 30
  • pass
    tools.descriptionsevery tool has a description
  • warn
    tools.description_length6 tools have descriptions over 1,024 characters, which crowds the context
  • pass
    tools.schemasevery tool has an object input schema
  • pass
    tools.annotations17 of 17 tools declare readOnlyHint or destructiveHint
  • warn
    tools.directory_hints9 of 17 tools declare all four hints; missing: get_platform_rules (idempotentHint); list_media (idempotentHint); list_accounts (idempotentHint); …
  • pass
    tools.token_costabout 7,473 tokens to load every tool
  • pass
    tools.api_dumptools are not a one-to-one API dump
stability25 · weight 15
  • warn
    stability.changes48 tool changes in 30 days
  • fail
    stability.rug_pull3 tools kept their name but changed most of their description in 30 days; review before trusting them
dependenciesn/a · weight 15
  • n/a
    deps.known_vulns, deps.mcp_sdk_version, deps.resolvabledeps.dev does not know [email protected]

17 tools, about 7,473 tokens to load them all · tool set 147eb49d72d8 · tools last changed 2026-10-19 · checked 2026-10-20 09:31 UTC

Tool changes

  • update_postchangedinput schema
  • reschedule_postchangeddescription
    before

    Move a SCHEDULED post to a different date and time. ONE SEND AT A TIME: a scheduled post going to several accounts is stored as one post per send, so this moves just that platform and leaves the others where they are (an older row can still carry several platforms, and moves them together). A draft is refused: schedule a draft with update_post with action 'schedule', which checks each platform first and gives each its own post. Takes the post id from list_posts, plus scheduledDate (YYYY-MM-DD) and scheduledTime (e.g. '9:00 AM') read in the ACCOUNT OWNER'S timezone (`ownerTimezone` from whoami), not yours or the user's device. A post's `scheduledAt` is UTC (it ends in Z) — never copy its digits as a time; work from `scheduledLocal` (the owner's date and time) that list_posts and get_post give beside it, so 'an hour later' is an hour later. The reply's `scheduledLocal` is where the post now is. A post that has already published cannot be moved — its time is a record of what happened — and one that is publishing right now is refused until it finishes.

    after · 79% of words in common

    Move a SCHEDULED post to a different date and time. ONE SEND AT A TIME: a scheduled post going to several accounts is stored as one post per send, so this moves just that platform and leaves the others where they are (an older row can still carry several platforms, and moves them together). A draft is refused: schedule a draft with update_post with action 'schedule', which checks each platform first and gives each its own post. Takes the post id from list_posts, plus scheduledDate (YYYY-MM-DD) and scheduledTime (e.g. '9:00 AM') read in the ACCOUNT OWNER'S timezone (`ownerTimezone` from whoami), not yours or the user's device. A post's `scheduledAt` is UTC (it ends in Z) — never copy its digits as a time; work from `scheduledLocal` (the owner's date and time) that list_posts and get_post give beside it, so 'an hour later' is an hour later. The reply's `scheduledLocal` is where the post now is. A post that has already published cannot be moved — its time is a record of what happened — and one that is publishing right now is refused until it finishes. A FAILED post is not rescheduled either: there is no upcoming slot to move, and moving it would not re-send it. Send it again with retry_post, which re-queues only the sends that failed; reschedule_post is for a post that is still scheduled. Check `status` from list_posts before calling this — on 2026-10-15 one agent called it on ten failed posts in a minute and was refused ten times.

  • validate_postchangeddescription, input schema
    before

    Check a post against every platform you plan to send it to, WITHOUT creating anything. Runs the same checks create_post runs, so the answer is what will really happen: platform rules ('X counts this as 1200/280 characters', 'YouTube needs a title', 'TikTok slideshows take JPEG or WebP photos'), whether each account is connected and signed in, AND the account's own limits — trial expiry, whether the plan includes X, and the monthly posting allowances. Pass the same `action`/`scheduledDate`/`scheduledTime` you will pass to create_post: a cap counts the month the post is DUE, so checking without them answers a different question — and with no action it checks what create_post would make of no action, a draft, which no limit applies to. scheduledDate/scheduledTime are the account owner's wall clock in their timezone (`ownerTimezone` from whoami), and a date with an action other than 'schedule' is refused, as create_post refuses it. Anything not tied to one platform comes back under the key "account". It takes create_post's own `platforms` (names), `options`, `overrides` and `mediaIds`, so hand it exactly what you will hand create_post. `ok` is false only when something would stop the post; an issue with severity 'warn' (an image that will be cropped, images past a platform's cap) is worth telling the user but does not. Free to call as often as you like; use it before create_post rather than discovering a problem after the post is spent.

    after · 99% of words in common

    Check a post against every platform you plan to send it to, WITHOUT creating anything. Runs the same checks create_post runs, so the answer is what will really happen: platform rules ('X counts this as 1200/280 characters', 'YouTube needs a title', 'TikTok slideshows take JPEG or WebP photos'), whether each account is connected and signed in, AND the account's own limits — trial expiry, whether the plan includes X, and the monthly posting allowances. Pass the same `action`/`scheduledDate`/`scheduledTime` you will pass to create_post: a cap counts the month the post is DUE, so checking without them answers a different question — and with no action it checks what create_post would make of no action, a draft, which no limit applies to. scheduledDate/scheduledTime are the account owner's wall clock in their timezone (`ownerTimezone` from whoami), and a date with an action other than 'schedule' is refused, as create_post refuses it. Anything not tied to one platform comes back under the key "account". It takes create_post's own `platforms` (platform names or account ids — an id checks that one account), `options`, `overrides` and `mediaIds`, so hand it exactly what you will hand create_post. `ok` is false only when something would stop the post; an issue with severity 'warn' (an image that will be cropped, images past a platform's cap) is worth telling the user but does not. Free to call as often as you like; use it before create_post rather than discovering a problem after the post is spent.

  • delete_postchangeddescription
    before

    Delete a post from iHatePosting. ONE SEND AT A TIME: a post going to several accounts is stored as one post PER SEND, so deleting one cancels only that platform and leaves the rest scheduled — pass the `postId` that create_post returned on that particular send, or the id of the row list_posts shows for it. There is no need to cancel a whole multi-platform post to drop one platform from it. THIS DOES NOT UNPUBLISH ANYTHING: a post that has already gone out stays live on the network, and deleting it here only removes iHatePosting's record of it. For that reason a published post is refused unless you pass force: true, and you should only do that after telling the user in words that the platform post will remain. A post that is publishing at this moment is never deletable, forced or not — the worker is mid-flight on it. Deleting a scheduled post stops it going out.

    after · 70% of words in common

    Delete a post from iHatePosting. ONE SEND AT A TIME: a SCHEDULED post going to several accounts is stored as one post PER SEND, so deleting one cancels only that platform and leaves the rest scheduled — pass the `postId` that create_post returned on that particular send, or the id of the row list_posts shows for it. There is no need to cancel a whole multi-platform post to drop one platform from it. A DRAFT IS DIFFERENT: until it is scheduled, a draft is ONE post holding every platform it is aimed at (all its sends share one postId), so deleting it removes every platform — to drop one platform from a draft, use update_post with the remaining accountIds instead. The reply's `platformsRemoved` names every platform the delete took; for one send of a group, `stillScheduled` lists the sends still going out and `otherSends` the rest of the group (already published or failed), each with its status; their `scheduledAt` is UTC (it ends in Z) and `scheduledLocal` is the same moment in the account owner's timezone — report that one. THIS DOES NOT UNPUBLISH ANYTHING: a post that has already gone out stays live on the network, and deleting it here only removes iHatePosting's record of it. For that reason a published post is refused unless you pass force: true, and you should only do that after telling the user in words that the platform post will remain. A post that is publishing at this moment is never deletable, forced or not — the worker is mid-flight on it. Deleting a scheduled post stops it going out.

  • retry_postchangeddescription
    before

    Send a failed post again. ONE SEND AT A TIME, usually: a post to several accounts is stored as one post per send, so a post id normally names a single platform and retrying it touches only that one. (An older row, or a draft, can still carry several — without targetId every failed platform on that row is retried; with one, only that send.) Only a send that FAILED is retried — one that published is left alone, so this cannot double-post. If the cause was the account rather than the post (a revoked or paused connection) the answer says so, and retrying will not help until the user reconnects.

    after · 68% of words in common

    Send a failed post again. ONE SEND AT A TIME, usually: a post to several accounts is stored as one post per send, so a post id normally names a single platform and retrying it touches only that one. (An older row, or a draft, can still carry several — without targetId every failed platform on that row is retried; with one, only that send.) Only a send that FAILED is retried — one that published is left alone. But a send marked failed can already be live (a timeout after the network took the post), so after a timeout check the platform before retrying. A send our own checks SKIPPED is not retried — the answer says why, and update_post is the fix. If the cause was the account rather than the post (a revoked or paused connection) the answer says so, and retrying will not help until the user reconnects. The reply's `retried` counts what went back on the queue and `skipped` names every send it left behind, with the reason.

  • update_postchangeddescription, input schema
    before

    Rewrite a draft or scheduled post. THIS REPLACES THE WHOLE POST, so read it with get_post first and send back the complete set — anything you leave out is removed, including platforms and per-platform options. Note the shape differs from create_post: this one wants `accountIds` (the ids from list_accounts, NOT platform names) and it requires `action`. Two limits: a post that has already published cannot be rewritten at all, and a post that goes to several platforms and was saved as a group keeps the platforms it has — send back that row's own accountIds and everything else is editable, but adding or swapping an account is refused with a sentence saying so, because it would publish some platforms twice. To change which platforms a group posts to, delete it and create it again.

    after · 60% of words in common

    Rewrite a draft or scheduled post. THIS REPLACES THE WHOLE POST, so read it with get_post first and send back the complete set — anything you leave out is removed, including platforms and per-platform options. Note the shape differs from create_post: this one wants `accountIds` (the ids from list_accounts, NOT platform names) and it requires `action`. Two limits: a post that has already published cannot be rewritten at all, and a post that goes to several platforms and was saved as a group keeps the platforms it has — send back that row's own accountIds and everything else is editable, but adding or swapping an account is refused with a sentence saying so, because it would publish some platforms twice. To change which platforms a group posts to, delete it and create it again. A draft is not a group: to drop one platform from a draft, send it back without that account. SCHEDULING A DRAFT that goes to several platforms (action 'schedule' or 'now') turns it into one post per platform: the reply's `rows` lists every `postId` with its platforms, and the id you sent now names only its first platform — use the others' ids to move, cancel or retry them. A post that is being published right now cannot be edited; the answer says so — wait for it to finish. Like create_post, its reply's `warnings` name anything the post will not carry — pass them on. TIMES: scheduledDate/scheduledTime are the owner's wall clock (`ownerTimezone` from whoami); to keep a scheduled post's time, send back get_post's `scheduledLocal.date` and `scheduledLocal.time` — never the digits of its UTC `scheduledAt`. A date with an action other than 'schedule' is refused. The reply's `scheduledAt` (UTC) and `scheduledLocal` say when it now goes out.

  • get_postchangeddescription
    before

    Everything about ONE post: the shared text, each platform's own copy and options, the attached media, and per-send status with the live URL or the error. list_posts gives you the id; this explains what actually happened to it. Read it before update_post so you change what is there rather than overwriting it.

    after · 68% of words in common

    Everything about ONE post: the shared text, each platform's own copy and options, the attached media, and per-send status with the live URL or the error. list_posts gives you the id; this explains what actually happened to it. Read it before update_post so you change what is there rather than overwriting it. Its `scheduledAt` is UTC (it ends in Z); `scheduledLocal` is the same moment as the owner's date, time and timezone — the values to send back as scheduledDate/scheduledTime to keep the time, and the ones to tell the user.

  • reschedule_postchangeddescription, input schema
    before

    Move a scheduled post to a different date and time. ONE SEND AT A TIME: a post going to several accounts is stored as one post per send, so this moves just that platform and leaves the others where they are. Takes the post id from list_posts, plus scheduledDate (YYYY-MM-DD) and scheduledTime (e.g. '9:00 AM') read in the ACCOUNT OWNER'S timezone, not yours or the user's device. A post that has already published cannot be moved — its time is a record of what happened — and one that is publishing right now is refused until it finishes.

    after · 67% of words in common

    Move a SCHEDULED post to a different date and time. ONE SEND AT A TIME: a scheduled post going to several accounts is stored as one post per send, so this moves just that platform and leaves the others where they are (an older row can still carry several platforms, and moves them together). A draft is refused: schedule a draft with update_post with action 'schedule', which checks each platform first and gives each its own post. Takes the post id from list_posts, plus scheduledDate (YYYY-MM-DD) and scheduledTime (e.g. '9:00 AM') read in the ACCOUNT OWNER'S timezone (`ownerTimezone` from whoami), not yours or the user's device. A post's `scheduledAt` is UTC (it ends in Z) — never copy its digits as a time; work from `scheduledLocal` (the owner's date and time) that list_posts and get_post give beside it, so 'an hour later' is an hour later. The reply's `scheduledLocal` is where the post now is. A post that has already published cannot be moved — its time is a record of what happened — and one that is publishing right now is refused until it finishes.

  • list_accountschangeddescription
    before

    List the connected social accounts (platform, handle, health) available for posting. The response also names the iHatePosting account this connection acts for — if the accounts listed are not the ones you expect, check that `account.email` is the right person before assuming anything is missing.

    after · 80% of words in common

    List the connected social accounts (platform, handle, health) available for posting. The response also names the iHatePosting account this connection acts for — if the accounts listed are not the ones you expect, check that `account.email` is the right person before assuming anything is missing. `account.timezone` is the owner's timezone, the zone every scheduled time is read in.

  • list_postschangeddescription, input schema
    before

    List posts with per-platform status (draft/scheduled/published/failed) and live URLs. Rows sharing a `batchId` are ONE submission the person made to several accounts — report them as one post going to N places, not as N posts. ALWAYS CHECK `total` IN THE REPLY: it is the number of posts matching your filter, and `returned` is how many you got. If they differ you are holding one page, not the answer — page on with `cursor` until `hasMore` is false before you count anything or tell anyone a number. Use `status` and `from`/`to` to ask the real question instead of counting a page: for "how many are scheduled" pass status 'scheduled' and read `total`; for a month or a date range pass from/to, which match the SCHEDULED time.

    after · 63% of words in common

    List posts with per-platform status (draft/scheduled/published/failed) and live URLs. Rows sharing a `batchId` are ONE submission the person made to several accounts — report them as one post going to N places, not as N posts. ALWAYS CHECK `total` IN THE REPLY: it is the number of rows matching your filter, and `returned` is how many you got. If they differ you are holding one page, not the answer — page on with `cursor` until `hasMore` is false before you count anything or tell anyone a number. `total` counts ROWS: a scheduled post to several accounts is one row per platform (sharing a `batchId`), while a draft is one row however many platforms it has — so `total` is the number of sends, and the number of posts the person made is the rows with distinct `batchId`s (a row without one is a post on its own). A post you move LATER while paging can come round again on a later page — skip ids you have already handled; none are skipped. Use `status` and `from`/`to` to ask the real question instead of counting a page: for "how many are scheduled" pass status 'scheduled' and read `total` (sends); for a month or a date range pass from/to, which match the SCHEDULED time — a bare date is that whole day on the account owner's calendar, in the owner's timezone (the reply's `timezone`, `ownerTimezone` from whoami). Each post's `scheduledAt` is UTC (it ends in Z); `scheduledLocal` is the same moment as the owner's date and time — report that one, and send it back unchanged to reschedule_post or update_post.

Showing the latest 10. Full history: list_changes · trend: quality JSON.

Checks run on what our credential-free, read-only probes observe; tools are never called and no code audit is performed. How it is computed.

Trust 96 / 100

provenance85
liveness100
freshness100
hygiene100

+repository +dns-namespace +website +reachable +uptime-100% +updated-5d-ago

Derived from observable signals (official registry feed and our own credential-free probes); no code audit performed. How it is computed.

Dependencies

[email protected] npmthis version is not known to the dependency resolver

Dependency graphs from deps.dev; advisories from OSV.dev, including the GitHub Advisory Database and the PyPI Advisory Database; all CC BY 4.0. The graph is what a clean install of this version resolves today; nothing was installed or run. How it is checked.

Endpoints

remoteauthreachableuptime 30dp50protocollast ok
https://ihateposting.com/mcp/oauth streamable-httpoauth2 AS: https://ihateposting.com/oauthyes100%199 ms–2026-10-20
https://ihateposting.com/api/mcp streamable-httpapi_key ${api_key}yes99%207 ms2026-07-282026-10-20
packageregistryversionruntimesecrets
ihateposting-mcpnpm0.12.4npxIHATEPOSTING_API_KEY

Tools (17)

create_postdestructiveCreate a social post on iHatePosting. action 'draft' saves it (the default when no date is given, and the safe choice); 'schedule' needs scheduledDate (YYYY-MM-DD) + scheduledTime (e.g. '9:00 AM' or '21:00'), the account owner's wall clock in their timezone — `ownerTimezone` from whoami, not yours or the user's device; 'now' publishes immediately to a real audience. A date with any action but 'schedule', or with no action, is refused rather than dropped. Every `scheduledAt` in the reply is UTC (it ends in Z); `scheduledLocal` beside it is the same moment as the owner's date, time and timezone — tell the user that one. Platforms with requirements — Pinterest a board, YouTube a title, TikTok and Instagram media — are set through `options` and `mediaIds`; call get_platform_rules for what each one needs, and validate_post to check before you commit. The reply lists every send it created, each naming its account (handle, displayName, nickname) AND carrying its own `postId` — tell the user what it says rather than what you asked for; they are not always the same. Its `warnings` name anything the post will not carry (images past a platform's cap, a signed-out account, a Pro-only option) — pass them on. A SCHEDULED (or 'now') multi-platform post is stored as one post per send, so that `postId` is what cancels or reschedules a single platform later without touching the others. A draft keeps every platform on ONE postId (every send in the reply shows the same one) until it is scheduled with update_post — deleting that draft deletes it for every platform.
delete_postdestructiveDelete a post from iHatePosting. ONE SEND AT A TIME: a SCHEDULED post going to several accounts is stored as one post PER SEND, so deleting one cancels only that platform and leaves the rest scheduled — pass the `postId` that create_post returned on that particular send, or the id of the row list_posts shows for it. There is no need to cancel a whole multi-platform post to drop one platform from it. A DRAFT IS DIFFERENT: until it is scheduled, a draft is ONE post holding every platform it is aimed at (all its sends share one postId), so deleting it removes every platform — to drop one platform from a draft, use update_post with the remaining accountIds instead. The reply's `platformsRemoved` names every platform the delete took; for one send of a group, `stillScheduled` lists the sends still going out and `otherSends` the rest of the group (already published or failed), each with its status; their `scheduledAt` is UTC (it ends in Z) and `scheduledLocal` is the same moment in the account owner's timezone — report that one. THIS DOES NOT UNPUBLISH ANYTHING: a post that has already gone out stays live on the network, and deleting it here only removes iHatePosting's record of it. For that reason a published post is refused unless you pass force: true, and you should only do that after telling the user in words that the platform post will remain. A post that is publishing at this moment is never deletable, forced or not — the worker is mid-flight on it. Deleting a scheduled post stops it going out.
get_analyticsHow the posts actually performed: impressions, engagement and follower numbers per connected channel, plus the top posts. Use this to answer 'how did that do', 'which platform is working' or 'what should I post more of' — it reports what iHatePosting collected from each network, not an estimate. Some platforms report nothing (they are named in `unmeasuredPlatforms`), so a zero there means 'not measurable', not 'no reach'. If a plan ever gates a range, the answer says so in a sentence with `locked: true` rather than returning silently empty numbers.
get_platform_rulesread-onlyWhat each platform will and will not accept: character limit, whether media is required, how many images, video formats and length, and EVERY per-platform option it takes — `options` per platform, each with its exact key, type, allowed values, default when left out, and whether it is required (e.g. Pinterest needs boardId, YouTube needs ytTitle and ytMadeForKids; YouTube's privacy is ytPrivacy, Instagram's first comment is firstComment). Call this BEFORE writing a post — it is the difference between composing something that publishes and something the platform rejects.
get_postread-onlyEverything about ONE post: the shared text, each platform's own copy and options, the attached media, and per-send status with the live URL or the error. list_posts gives you the id; this explains what actually happened to it. Read it before update_post so you change what is there rather than overwriting it. Its `scheduledAt` is UTC (it ends in Z); `scheduledLocal` is the same moment as the owner's date, time and timezone — the values to send back as scheduledDate/scheduledTime to keep the time, and the ones to tell the user.
get_upload_ticketInternal: issues a short-lived ticket (10 minutes, reused by the upload box for its own requests) that the upload box uses to list, preview, upload and delete files. The upload box calls this itself.
list_accountsread-onlyList the connected social accounts (platform, handle, health) available for posting. The response also names the iHatePosting account this connection acts for — if the accounts listed are not the ones you expect, check that `account.email` is the right person before assuming anything is missing. `account.timezone` is the owner's timezone, the zone every scheduled time is read in.
list_mediaread-onlyThe images and videos already in the user's iHatePosting library, newest first, with the `id` that create_post's `mediaIds` wants. Call this whenever a platform needs a file — Instagram, Pinterest, TikTok and YouTube all do — to see whether what you need is already uploaded. Returns ids and dimensions only; there is no downloadable link, by design.
list_pinterest_boardsread-onlyThe Pinterest boards you can pin to, with the `id` that create_post's `options.pinterest.boardId` requires. Pinterest REFUSES a pin without a board, and a board id is an opaque number you cannot guess — call this before creating any Pinterest post. If several Pinterest accounts are connected, pass `accountId` from list_accounts, because boards belong to one account and pinning to another account's board fails. An empty list with no error means the account is connected and simply has no boards yet — make one in Pinterest first.
list_postsread-onlyList posts with per-platform status (draft/scheduled/published/failed) and live URLs. Rows sharing a `batchId` are ONE submission the person made to several accounts — report them as one post going to N places, not as N posts. ALWAYS CHECK `total` IN THE REPLY: it is the number of rows matching your filter, and `returned` is how many you got. If they differ you are holding one page, not the answer — page on with `cursor` until `hasMore` is false before you count anything or tell anyone a number. `total` counts ROWS: a scheduled post to several accounts is one row per platform (sharing a `batchId`), while a draft is one row however many platforms it has — so `total` is the number of sends, and the number of posts the person made is the rows with distinct `batchId`s (a row without one is a post on its own). A post you move LATER while paging can come round again on a later page — skip ids you have already handled; none are skipped. Use `status` and `from`/`to` to ask the real question instead of counting a page: for "how many are scheduled" pass status 'scheduled' and read `total` (sends); for a month or a date range pass from/to, which match the SCHEDULED time — a bare date is that whole day on the account owner's calendar, in the owner's timezone (the reply's `timezone`, `ownerTimezone` from whoami). Each post's `scheduledAt` is UTC (it ends in Z); `scheduledLocal` is the same moment as the owner's date and time — report that one, and send it back unchanged to reschedule_post or update_post.
open_upload_widgetread-onlyShow the person their iHatePosting media box: they can upload an image or video from their own device, browse what is already in their library with every file previewable, see how much storage their plan leaves them, pick a file to use, or delete one. THIS IS THE ANSWER WHENEVER SOMEONE NAMES A FILE ON THEIR OWN COMPUTER — a path like C:\clips\launch.mp4 or ~/Movies/clip.mov is not something any tool here can read, and explaining that is not an answer: open this instead, in the same reply, and they pick the file from a picker right in the conversation. Also use it for anything they pasted or attached here. Do NOT try to send those bytes yourself — they would travel as your own output and arrive cut short. If the file already has a public address, use upload_media with `url` instead.
reschedule_postdestructiveMove a SCHEDULED post to a different date and time. ONE SEND AT A TIME: a scheduled post going to several accounts is stored as one post per send, so this moves just that platform and leaves the others where they are (an older row can still carry several platforms, and moves them together). A draft is refused: schedule a draft with update_post with action 'schedule', which checks each platform first and gives each its own post. Takes the post id from list_posts, plus scheduledDate (YYYY-MM-DD) and scheduledTime (e.g. '9:00 AM') read in the ACCOUNT OWNER'S timezone (`ownerTimezone` from whoami), not yours or the user's device. A post's `scheduledAt` is UTC (it ends in Z) — never copy its digits as a time; work from `scheduledLocal` (the owner's date and time) that list_posts and get_post give beside it, so 'an hour later' is an hour later. The reply's `scheduledLocal` is where the post now is. A post that has already published cannot be moved — its time is a record of what happened — and one that is publishing right now is refused until it finishes. A FAILED post is not rescheduled either: there is no upcoming slot to move, and moving it would not re-send it. Send it again with retry_post, which re-queues only the sends that failed; reschedule_post is for a post that is still scheduled. Check `status` from list_posts before calling this — on 2026-10-15 one agent called it on ten failed posts in a minute and was refused ten times.
retry_postdestructiveSend a failed post again. ONE SEND AT A TIME, usually: a post to several accounts is stored as one post per send, so a post id normally names a single platform and retrying it touches only that one. (An older row, or a draft, can still carry several — without targetId every failed platform on that row is retried; with one, only that send.) Only a send that FAILED is retried — one that published is left alone. But a send marked failed can already be live (a timeout after the network took the post), so after a timeout check the platform before retrying. A send our own checks SKIPPED is not retried — the answer says why, and update_post is the fix. If the cause was the account rather than the post (a revoked or paused connection) the answer says so, and retrying will not help until the user reconnects. The reply's `retried` counts what went back on the queue and `skipped` names every send it left behind, with the reason.
update_postdestructiveRewrite a draft or scheduled post. THIS REPLACES THE WHOLE POST, so read it with get_post first and send back the complete set — anything you leave out is removed, including platforms and per-platform options. Note the shape differs from create_post: this one wants `accountIds` (the ids from list_accounts, NOT platform names) and it requires `action`. Two limits: a post that has already published cannot be rewritten at all, and a post that goes to several platforms and was saved as a group keeps the platforms it has — send back that row's own accountIds and everything else is editable, but adding or swapping an account is refused with a sentence saying so, because it would publish some platforms twice. To change which platforms a group posts to, delete it and create it again. A draft is not a group: to drop one platform from a draft, send it back without that account. SCHEDULING A DRAFT that goes to several platforms (action 'schedule' or 'now') turns it into one post per platform: the reply's `rows` lists every `postId` with its platforms, and the id you sent now names only its first platform — use the others' ids to move, cancel or retry them. A post that is being published right now cannot be edited; the answer says so — wait for it to finish. Like create_post, its reply's `warnings` name anything the post will not carry — pass them on. TIMES: scheduledDate/scheduledTime are the owner's wall clock (`ownerTimezone` from whoami); to keep a scheduled post's time, send back get_post's `scheduledLocal.date` and `scheduledLocal.time` — never the digits of its UTC `scheduledAt`. A date with an action other than 'schedule' is refused. The reply's `scheduledAt` (UTC) and `scheduledLocal` say when it now goes out.
upload_mediaPut an image or video into the library and get back an id for create_post. PREFER `url` — ALWAYS, including for a picture you just generated: we fetch it ourselves at full length, and it is the only thing that works for video or for anything more than a few kilobytes. We take whatever the link serves, so a CDN that will not name the type (application/octet-stream, as presigned S3, Drive and Dropbox links do) is fine. `base64` is a last resort for small files that exist at no address: it travels as your own output, a conversation truncates it, and the cut-off file is refused. Either way the API checks type, size and quota, then registers the file.
validate_postread-onlyCheck a post against every platform you plan to send it to, WITHOUT creating anything. Runs the same checks create_post runs, so the answer is what will really happen: platform rules ('X counts this as 1200/280 characters', 'YouTube needs a title', 'TikTok slideshows take JPEG or WebP photos'), whether each account is connected and signed in, AND the account's own limits — trial expiry, whether the plan includes X, and the monthly posting allowances. Pass the same `action`/`scheduledDate`/`scheduledTime` you will pass to create_post: a cap counts the month the post is DUE, so checking without them answers a different question — and with no action it checks what create_post would make of no action, a draft, which no limit applies to. scheduledDate/scheduledTime are the account owner's wall clock in their timezone (`ownerTimezone` from whoami), and a date with an action other than 'schedule' is refused, as create_post refuses it. Anything not tied to one platform comes back under the key "account". It takes create_post's own `platforms` (platform names or account ids — an id checks that one account), `options`, `overrides` and `mediaIds`, so hand it exactly what you will hand create_post. `ok` is false only when something would stop the post; an issue with severity 'warn' (an image that will be cropped, images past a platform's cap) is worth telling the user but does not. Free to call as often as you like; use it before create_post rather than discovering a problem after the post is spent.
whoamiread-onlyWhich iHatePosting login this connection acts for (email and account name), whether it connected by signing in or with an API key, and the account owner's timezone (`ownerTimezone`) — the zone every scheduledDate/scheduledTime you send is read in. Use this for any question about who the user is or which account is connected, and before working out a time like 'tomorrow at 9' or 'in two hours'. This is NOT a social media handle — the handles from list_accounts belong to the connected profiles, which may carry other people's names.

Schemas: list_tools.

Similar MCP servers

Official server.json
{
  "name": "com.ihateposting/mcp",
  "icons": [
    {
      "src": "https://ihateposting.com/icon.png",
      "sizes": [
        "512x512"
      ],
      "mimeType": "image/png"
    }
  ],
  "title": "iHatePosting",
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
  "remotes": [
    {
      "url": "https://ihateposting.com/mcp/oauth",
      "type": "streamable-http"
    },
    {
      "url": "https://ihateposting.com/api/mcp",
      "type": "streamable-http",
      "headers": [
        {
          "name": "Authorization",
          "value": "Bearer {api_key}",
          "isSecret": true,
          "variables": {
            "api_key": {
              "isSecret": true,
              "isRequired": true,
              "description": "iHatePosting API key (starts with pk_live_)"
            }
          },
          "isRequired": true,
          "description": "API-key alternative to signing in: your iHatePosting API key as a Bearer token. Create one in Settings > Developers."
        }
      ]
    }
  ],
  "version": "0.12.4",
  "packages": [
    {
      "version": "0.12.4",
      "transport": {
        "type": "stdio"
      },
      "identifier": "ihateposting-mcp",
      "runtimeHint": "npx",
      "registryType": "npm",
      "registryBaseUrl": "https://registry.npmjs.org",
      "environmentVariables": [
        {
          "name": "IHATEPOSTING_API_KEY",
          "format": "string",
          "isSecret": true,
          "isRequired": true,
          "description": "Your iHatePosting API key (starts with pk_live_). Create one in Settings > Developers."
        }
      ]
    }
  ],
  "repository": {
    "id": "1381336809",
    "url": "https://github.com/iHatePostingOrg/agent-skill",
    "source": "github"
  },
  "websiteUrl": "https://ihateposting.com",
  "description": "Draft, check and schedule posts to your social accounts from Claude, ChatGPT, Cursor or VS Code."
}