Pendpost
Local-first social MCP: an agent drafts and schedules posts; you approve before anything publishes.
Connector by pendpost · GitHub account verified by the MCP Registry
Install
Works with Claude Code, Codex, VS Code, Cursor, Windsurf, Claude Desktop. Read the repository before you give any tool access to your data.
npx agentmarket install connector/pendpost-pendpost
Asks which of your tools to add it to (Claude Code, Codex, Cursor, VS Code, Windsurf, Gemini CLI, Claude Desktop) and shows the change first. See https://agentmart.dev/cli.
Get the weekly diff
The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.
About
Use cases: social-media, marketing-content
Reviews
Write a reviewNo reviews yet. Installed it? Reviews come from verified installers only. How reviews work
Permissions
What this server can do and what it asks for, from its tool list, install config and code. Checked Oct 25, 2026.
- Tools
- 40 read · 57 write · 8 delete · 29 not yet classified
- Secrets
- None asked for
- Install scripts
- None found
- Runs as
- Node.js package
Tools by what they do 134
Read from the source code; the running server may list different tools.
Read 40 · Look things up without changing anything.
account_statusassets_listbrand_lintclient_listcloud_capabilitiescloud_clientscloud_statuscloud_subscriptionconfig_getconnect_discoverengage_answerengage_asks_list
Show 28 more tools
engage_probeengage_queue_listfetch_insightsgbp_attributes_getgbp_media_listghost_membersghost_newsletterslist_commentslist_engagerslist_inbound_eventslist_reviewsnostr_list_getnostr_relay_list_getpendpost_healthpinterest_boards_listpinterest_list_board_sectionsplan_getplan_listplatform_validatepresubmit_checkradar_followup_checkradar_listread_insightsreddit_list_flairsreviewer_listvalidate_mediaverify_postyoutube_playlists_list
Write 57 · Create, change, send or run things.
approve_postasset_uploadcampaign_createcampaign_set_activecampaign_set_internalclient_archiveclient_createclient_set_activeclient_updatecomment_inboxcomment_inbox_refreshcomment_resolve
Show 45 more tools
config_setdiscord_schedule_eventedit_publishedengage_cancelengage_confirmengage_confirm_handlegbp_attributes_setgbp_media_addghost_member_createghost_members_importghost_newsletter_createghost_newsletter_updatemark_postedmastodon_followmastodon_pinmastodon_update_profilemeta_lane_setnostr_update_profilependpost_record_blockpinterest_board_createpinterest_board_section_createpinterest_board_section_updatepinterest_board_updatepublish_due_runpublish_previewradar_agent_stopradar_draft_comparisonradar_ingestradar_mark_copy_postedradar_queue_replyreact_to_postreject_postrename_assetreply_to_commentreply_to_inbound_eventreschedulereviewer_createscheduler_setsend_zapset_covertelegram_update_profilex_update_profileyoutube_playlist_addyoutube_playlist_createyoutube_update_profile
Delete 8 · Delete, drop, revoke or wipe things.
autonomy_revokeclear_coverdelete_assetmoderate_commentradar_geo_resetreply_to_reviewreviewer_revokeunlink_engagers
Not yet classified 29 · We could not tell from the name and description.
activity_logagent_recheckclients_overviewengage_community_recheckengage_dismissengage_pauseengage_undoforget_engagergenerate_digesthealth_rechecklink_engagersnostr_list_set
Show 17 more tools
nostr_relay_list_setplan_create_postplan_delete_postplan_update_postradar_agent_comparisonradar_agent_scanradar_engage_reportradar_followup_reportradar_footprint_logradar_scanradar_triageradar_triage_bulkradar_tuneresume_lanetoken_refreshunforget_engagerunschedule
Network hosts it talks to 60
accounts.google.com· found in codeapi.pinterest.com· found in codeapi.telegram.org· found in codebalanced-giraffe-97.accounts.dev· found in codebsky.app· found in codebsky.social· found in codebusinessprofileperformance.googleapis.com· found in codecdn.discordapp.com· found in codeclaude.com· found in codeconsole.cloud.google.com· found in codedeinblog.ch· found in codedeineseite.ghost.io· found in codedevelopers.facebook.com· found in codedevelopers.google.com· found in codedevelopers.pinterest.com· found in codedevelopers.tiktok.com· found in codediscord.com· found in codedocs.pendpost.com· found in codeexample.invalid· found in codefacebook.com· found in codeghost.org· found in codegithub.com· found in codeglama.ai· found in codegoogleapis.com· found in codegraph-video.facebook.com· found in codegraph.facebook.com· found in codehn.algolia.com· found in codeinstagram.com· found in codejoinmastodon.org· found in codelocal.google.com· found in code
Show 30 more hosts
mastodon.social· found in codemock.blog· found in codemock.google· found in codemock.instance· found in codemock.site· found in codemock.social· found in codemybusiness.googleapis.com· found in codemybusinessaccountmanagement.googleapis.com· found in codemybusinessbusinessinformation.googleapis.com· found in codenews.ycombinator.com· found in codenjump.me· found in codenostr.com· found in codeoauth.reddit.com· found in codeoauth2.googleapis.com· found in codeopen.tiktokapis.com· found in codependpost-cloud-api.fly.dev· found in codependpost.com· found in codepinterest.com· found in codequora.com· found in coderailway.app· found in coderedd.it· found in codereddit.com· found in coderes.cloudinary.com· found in coderupload.facebook.com· found in codesupport.discord.com· found in codet.me· found in codetiktok.com· found in codewordpress.org· found in codeyourblog.com· found in codeyoutu.be· found in code
Files it touches
/opt/homebrew/bin/opt/homebrew/bin/claude/private/tmp/usr/sbin- Your home directory (~)
C:\\Windows- Any path the agent passes to its tools
Tools (134)
Read from the source code; the running server may differ. Descriptions are the server’s own words; hidden characters are shown as [U+…].
account_statusConnection health per platform (Meta/LinkedIn/YouTube): configured, authenticated, token expiry, Meta action-block state (block.tracked is false until a block was ever recorded), scheduler state. Token values are never returned. Read-only.
activity_logThe pendpost audit feed: every publish attempt, scheduler start/stop and circuit-breaker event, newest first ({ts, campaign, postId, platform, action, ok, errorCode, errorMessage, lateMin, actor}). Read-only.
agent_recheckProve the operator's configured agent CLI (posting.radar.agent.provider) can actually run Radar research: spawns it once with a trivial prompt and pendpost's own MCP config, and requires it to call pendpost_health. Returns { ok, agent: { state, ok, detail, checkedAt } }. `state` is "live" ONLY if the tool call actually landed on this daemon - the agent's own answer is never taken as proof, because a model can reply "OK" having called nothing. Anything else is "failed" carrying the agent's own first line as detail (e.g. "Not logged in - Please run /login"), or "unproven" when no provider or cre…
approve_postApprove a post for publishing (approval: approved). Required actor; the actor who created the post can NEVER approve it (no self-approval; only the actor "owner" is exempt as the approval authority). Per the standing rule, agents call this only on the owner's explicit instruction - approval always comes from the owner. Refused with code not_configured (and the platform named) when the post targets a platform the owner skipped at onboarding that is still not connected: such a post could never publish, and force does not override it. Fix: connect that platform in Setup, or remove it from the pos…
asset_uploadIngest a new media file into data/media (the asset library). Provide filePath (a repo-local file to copy in) OR base64 bytes, plus the target filename. Refuses to overwrite an existing file and only accepts .mp4/.mov/.jpg/.png. The HTTP upload route in the pendpost UI uses the same implementation with a streamed binary body.
assets_listList publishable renders in data/media with ffprobe specs, spec checks (9x16/4x5, h264, faststart), cover JPEG, which plan posts use each file, and the matching voiceover caption SRTs (captions[] with srtPath/srtUrl) - the canonical source when drafting social copy. Read-only.
autonomy_revokeRevoke autonomy that unwinds (ux-audit R7 / AU4): return EVERY not-yet-published post the auto-approve policy already approved (approvalBy policy:auto-approve, status != posted - both auto-approved drafts and auto-posted Radar replies) to review, clearing the approval back to a pending draft. Owner-only (autonomy is owner-authorized); de-escalation only - it can never approve or publish anything. Pairs with disabling the policy: the toggle stops FUTURE approvals, this returns the BACKLOG. Published posts are never touched.
brand_lintLint caption/copy text against the editable brand rules in rules.json: platform hygiene (per-platform caption length cap, broken/empty links, ALL-CAPS shouting, hashtag-count sanity) plus a humanizer that flags AI-writing tells (AI-vocabulary, em-dash overuse, rule-of-three padding, negative parallelism, filler/hedging, promotional puffery). errors block publish; warns are advisory. Optional platform tunes the length + hashtag caps. Run over every caption before proposing it. Read-only.
campaign_createCreate a new campaign: writes data/plans/<id>/post-plan.json (empty posts) and registers it active in the manifest.
campaign_set_activeActivate/deactivate a campaign in the manifest. Inactive campaigns are ignored by the scheduler tick (explicitly-targeted runs still reach them).
campaign_set_internalFlag a campaign internal (or not) in the manifest. Internal campaigns (e.g. validation/test plans) drop out of the operator views (Published/Planner/Approvals) by default while staying fully active and schedulable - so a live validation campaign can keep running without cluttering the operator UI. Display-only: no scheduler impact.
clear_coverRemove a post's cover override (deletes the override JPEG and the post.cover field; the render-sibling JPEG becomes the cover again).
client_archiveToggle a client between active and archived (reversible). Owner-gated: requires actor:"owner" and confirm:true. Refuses to archive the currently active client (switch first). ARCHIVE SAFETY: archiving computes the client's in-flight work first and returns it as inFlight { total, local, native, posts }. When the platform itself already holds scheduled objects (native > 0: FB scheduled post, YouTube publishAt, mastodon queue entry, WP future, Ghost scheduled - they keep firing after archive), the call is refused with needs_confirm until you ALSO pass unscheduleInFlight:true (cancel the platform …
client_createCreate a new client workspace (non-secret profile only: id slug, displayName, optional logo/accent/timezone). Owner-gated: requires actor:"owner" and confirm:true (fail-closed). Scaffolds data/clients/<id>/. Never touches credentials. Prefer per-call clientId on other tools over switching the active client.
client_listList the configured clients and the active one: { activeClientId, clients: [{ id, displayName, status, timezone?, accent?, logo?, schedulerRunning, actionBlocked }] }. schedulerRunning is the PER-CLIENT scheduler enabled flag (each client's own state.scheduler.enabled, default-ON, so it can differ per row - a client the operator stopped reads false while its siblings stay true); actionBlocked is the per-client Meta-368 breaker (booleans only - never the blockedUntil/reason/fbtrace). No secrets. Pass clientId on any other tool to scope that one call to a specific client without switching the ac…
Show 119 more tools
client_set_activeSwitch the GLOBAL active client. Owner-gated: requires actor:"owner" and confirm:true. PREFER passing per-call clientId on other tools instead - this mutates global default state and is the core "posted to the wrong client" risk. The target must exist and be active. Never touches credentials.
client_updateUpdate a client's non-secret profile (displayName/logo/accent/timezone); id is immutable. Owner-gated: requires actor:"owner" and confirm:true. Requires ifRev (echo the rev from client_list/GET /api/clients) for optimistic concurrency; a stale rev returns stale_write. Never touches credentials.
clients_overviewCross-client roll-up of pending/overdue work, scheduler state and the Meta-368 breaker, one row per registered client: { activeClientId, clients: [{ id, displayName, status, ready, schedulerRunning, pending, overdue, metaBlocked, nextDue, error }] }. pending counts due posts in the horizon (waiting-due + overdue), overdue counts the past-due subset, nextDue is the soonest due ISO timestamp (or null), metaBlocked is the per-client 368 breaker (booleans + counts only - never the blockedUntil/reason/fbtrace or any secret). A corrupt client subtree degrades to error (a manifest_error envelope) whi…
cloud_capabilitiesRead the lane-capability map the UI badges lanes with (cloud 24/7 / native / local-only), proxied from the cloud's PUBLIC unauthenticated /v1/capabilities. Needs NO workspace and NO api key (pre-purchase honesty), is cached, and degrades to the conservative baked-in fallback offline - it never fails. Twin of GET /api/cloud/capabilities. Read-only.
cloud_clientsRead every local client with its per-brand always-on flag plus the install-global cloud connection summary (the "cloud clients" view): { ok, connection, clients: [{ clientId, name, active, alwaysOn }] }. Pure local read; no secrets. Twin of GET /api/cloud/clients. Read-only.
cloud_statusRead the managed-cloud (pendpost-cloud) connection status for THIS install: { enabled, baseUrl, workspaceId, apiKey presence-only } plus `sync` - the always-on guarantee roll-up that drives the header dot. `sync.state` has FOUR values, do not collapse them to "broken vs not": green = every approved cloud-lane post is confirmed on the cloud; yellow = WAITING, self-healing within the 10 min grace (push_pending: an approved post not yet confirmed by the cloud, normal for <=1 tick after approving; or flag_divergence while pendpost re-asserts the always-on flag) - neutral, not a warning; amber = DE…
cloud_subscriptionRead the metered subscription view proxied from the cloud (the in-app meter): { alwaysOn, status, allowance, postsUsed, postsIncluded, billingMode, currentPeriodEnd, action, checkoutEligible }. The api key (server-side, never exposed) scopes it to the workspace; no Stripe ids, no secrets. Requires a connected workspace - returns a stable error (e.g. not_configured / no_api_key) when the cloud is not connected. Twin of GET /api/cloud/subscription. Read-only.
comment_inboxRead the aggregated UNANSWERED comments on YOUR OWN recently-published posts (the own-post comment inbox the monitor sweep maintains): { ok, enabled, lastSweep, intervalHours, windowDays, unanswered, posts:[{ campaign, postId, platform, lanes, caption, permalink, unanswered, comments:[{ lane, commentId, author, text, ts, permalink, foundAt }] }], sources:{ lane:{ ok }|{ ok:false, error, scope } } }. Distinct from list_comments (one post's live thread) and radar_list (external conversations). OFF (config posting.commentWatch.enabled=false) => enabled:false, empty. To reply, use reply_to_comment…
comment_inbox_refreshForce a check-now sweep of your own recently-published posts for NEW comments, then return the fresh inbox (same shape as comment_inbox). Bypasses the interval cadence clock; OFF (posting.commentWatch.enabled=false) stays inert. Reaches the platforms read-only (never posts). Twin of POST /api/comments/inbox/refresh.
comment_resolveMark one own-post comment handled so it leaves the inbox and never re-surfaces: { ok, key, reason, removed }. `key` is the inbox item key `lane:postId:commentId` (from comment_inbox posts[].comments[]). reason is 'replied' or 'dismissed' (audit only). This is a LOCAL state stamp, NOT a platform write - the actual reply goes through reply_to_comment. Idempotent. Twin of POST /api/comments/inbox/resolve.
config_getRead the pendpost configuration: non-secret per-platform identifiers (Meta page/IG/app id, LinkedIn org urn + api version, YouTube redirect uri), posting variables (defaultLink, utm, hashtagPresets, defaultTimezone), and per-secret presence/tail/expiry (never the token value). Returns a rev for optimistic concurrency. Read-only.
config_setEdit NON-SECRET config only: identifiers (written to .env via a whitelisted, hardened writer) and posting variables (config.json). Secrets are display-only and can NEVER be set here - rotate them via the engine CLI (node scripts/<engine>.mjs auth). Autonomy keys are OWNER-GATED: posting.autoApprove, the gate refinements posting.approvalExpiryHours / posting.slotSlipMinutes (both default null = off), and the Radar autonomy keys posting.radar.{autoReply,agent,xEnterprise} are refused with invalid_input unless actor is "owner" (every other posting.radar key stays agent-writable). Requires ifRev f…
connect_discoverRead who one connected lane authenticates as + which assets it can manage (x/youtube/discord/linkedin/wordpress/reddit/pinterest/gbp): { ok, platform, connected, assetKind, identity:{id,handle,name,avatarUrl?}, assets:[{ kind, id, name, current, meta? }], selected:{<identifierKey>:<value|null>}, needsScope?, scope?, error? }. assetKind is the lane noun (channel/board/location/page/section/guild). Read-only, open-world (reaches the platform). Pull-on-demand, never persisted. Picking an asset writes the identifier through the existing config_set write - discovery itself never writes. Twin of GET…
delete_assetDelete one media file from data/media (the asset library), including its paired .jpg cover sibling. Confirm-gated + in-use-protected: if any plan post references the file it refuses with needs_confirm naming the using post(s) (campaign/postId) and deletes nothing unless confirm:true is passed (with confirm:true the plan rows are left dangling by design, mirroring plan_delete_post force). Rejects path segments / leading dots / a disallowed extension (invalid_input) before touching the disk; a missing file is invalid_input.
discord_schedule_eventCreate a REAL Discord guild scheduled event from a post's dcEvent intent ({name, startTime, endTime?, location?, entityType?, channelId?}, authored via plan_update_post first): { ok, event:{ id } }. Requires a DISCORD_BOT_TOKEN with the MANAGE_EVENTS permission in the guild - a webhook alone cannot create events. This makes a REAL, immediate change to the live Discord server - only call it on the owner's explicit instruction. Requires confirm: true; a bare call returns needs_confirm. A post with no dcEvent intent returns invalid_input; a missing/scopeless bot token returns not_configured with …
edit_publishedPush the post's current content (title/description/tags on YouTube, caption/text on Telegram, content on Discord) to the ALREADY-PUBLISHED object on youtube/telegram/discord - a first-party edit-in-place, never a re-publish (the object id and permalink are preserved). Edit the content FIRST via plan_update_post, then call this to push it live. This makes a REAL, immediate change to the live post - only call it on the owner's explicit instruction. Requires confirm: true. A post with no published, edit-capable lane (still planned, or targeting only lanes with no edit verb) returns invalid_input.…
engage_answerAnswer ONE question from "Respond for me" (spec 50 row 8) so the reply can go out. Send the fact in one line, the way you would tell a colleague ("the reviewer link is in every tier, including Starter"); pendpost spawns the drafting agent with your answer as the AUTHORITATIVE fact, humanizes the result, re-runs every hard rule (brand lint, the stranger-link fence, the per-platform limits) and queues the reply on the normal pacer. Do NOT write the reply yourself here - the text field is the ANSWER, not the post. If the drafted reply turns out to touch something sensitive (pricing, roadmap, lega…
engage_asks_listRead what "Respond for me" needs a HUMAN for (spec 50 §7.7): { ok, mode, open, asks:[{ id, kind, status, lane, signalKey, actionId, question, draft, finalText, reasonLine, urgent, answer, createdAt, resolvedAt, signal:{ source, externalId, author, community, text, url, intentScore } | null }] }. Five kinds, each with exactly one resolution: `question` = the agent needs a fact only the owner has (pricing, roadmap, a real customer) and cannot write the reply without it - resolve with engage_answer; `confirm` = a reply is written but touches something sensitive and must be read before it goes out…
engage_cancelCancel ONE queued "Respond for me" action before it happens (spec 50 row 5): the row goes to status cancelled and nothing is posted, liked or followed. Only a row that has not been handed to an executor can be cancelled - queued, or posting_soon (inside the 15-minute grace window that reposts, messages, original posts and replies to large accounts sit in). A row that is already releasing, done or terminal is refused with invalid_input, which is the honest answer: cancelling something already posted is an UNDO, a different verb with a different cost. OWNER-ONLY. Refused while the mode is off. L…
engage_community_recheckForget what "Respond for me" believes about ONE community's automation rules and let it look again (spec 50 row 7e5): clears the cached rule for that lane+community and drops the `decision` on every signal there that was skipped BECAUSE of it, so the next triage run judges them from scratch. Use it when a subreddit / instance / group changed its rules, or when the cached "this community bans automated posts" looks wrong. Judgments about the THREAD (outrage bait, already answered, not relevant) are untouched - only the community rule is re-opened. Returns { ok, lane, community, cleared, reopene…
engage_confirmPost a reply that "Respond for me" held for a look (spec 50 row 8e): a `confirm` ask carries the finished text plus one line saying why it was held ("Checked before posting: this touches pricing"). Calling this queues that text, optionally edited via `text`, and SKIPS the 15-minute grace window - grace exists to call back a row nobody read, and you just read this one. The wording still runs through the humanizer, whether it is the agent's or yours. To NOT post it, use engage_dismiss instead; there is no third answer. OWNER-ONLY. Refused while the mode is off. Returns { ok, askId, status, queue…
engage_confirm_handleConfirm (or deny) that the account a platform check SAW is this client's account, before "Respond for me" is allowed to act as it (spec 50 row 2e2, D13). ok:true copies the seen handle into posting.radar.engage.lanes[lane].handle and marks the platform ready; ok:false marks it wrong_account and it stays unusable until the operator switches the account in Chrome and re-checks. There is deliberately NO free-text handle field anywhere: a handle is recognised from what the check reported, never typed, so a typo can never authorise posting as the wrong account. Refused when no handle has been seen …
engage_dismissSkip ONE ask from "Respond for me" (spec 50 row 8e2): the ask closes and the signal it belongs to records `skip` with the reason `owner`, so the feed row reads "Skipped by you" instead of inventing a machine reason for a human decision. Nothing is posted, and the thread is not re-decided by a later triage run - a person already decided it. Use it for a question you do not want to answer, a held reply you do not want to send, or a hand-off you are not going to post by hand. OWNER-ONLY: only a human can claim a human skipped it. Refused while the mode is off. Returns { ok, askId, status }. Local…
engage_pausePause or resume ALL of "Respond for me" for this client (spec 50 row 12). This is a RESUMABLE hold, deliberately NOT the same thing as turning the mode off: the mode, the platform switches and every queued row keep their state, nothing is released while it is on, and resuming re-paces the backlog from where it stood. Use it when you want everything to stop right now and to be able to undo that in one call. A platform that stopped itself after a rate limit is a different state ("cooling down"), cleared with resume_lane. OWNER-ONLY (it is a policy decision and it persists in config). Refused whi…
engage_probeCheck ONE platform now for "Respond for me" (spec 50 rows 2e / 2e2) and record what it found: { ok, lane, route, usable, reason, detail, lastProbeAt }. `route` is how this platform acts for this client - "api" (the lane engine holds a credential) or "browser" (it needs the operator's own logged-in Chrome). An API platform runs the SAME liveness probe health_recheck runs and sets reason ready (it authenticated) or no_credential (it did not). A browser platform sets not_logged_in with usable:false: only the engage child can see whether Chrome is logged in as the right account, and claiming "Read…
engage_queue_listRead the "Respond for me" (auto-engage, spec 50) action list for this client: { ok, mode, paused, actions:[{ id, signalKey, lane, kind, status, waitingOn, releaseAt, graceUntil, attempts, rung, result, askId, text, createdAt }] }. Each row is one thing the policy will do - or already did - to one Radar signal: a reply, a like/upvote, a follow, a repost, a message or an original post. `status` is queued | posting_soon (inside the 15-minute undo window) | releasing (with an executor now) | done | dry_run | skipped | cancelled | undone | failed; `waitingOn` says WHY a queued row is not running ye…
engage_undoTake back ONE action "Respond for me" already performed (spec 50 §7.9): delete the reply or original post it published, remove the like/upvote, unfollow, un-repost. Only a row whose status is `done` can be undone - a queued row is CANCELLED (engage_cancel), a dry_run row never touched a platform, and a failed row never landed. The reversal is attempted FIRST and the original row is marked `undone` only if it actually succeeded, so an "Undone" badge always means the thing is really gone; a failed reversal leaves the row exactly as it was and returns the platform's own reason. Where a platform g…
fetch_insightsFetch fresh platform metrics for published posts (spawns the engines' read-only insights commands across the FREE measured lanes: IG/FB Graph insights with defensive metric fallback, YouTube videos.list statistics, LinkedIn share statistics, GBP performance, Pinterest pin analytics, Telegram subscriber counts, Ghost email opens/sends/clicks, Nostr reaction/zap counts, Reddit score/comments/upvote-ratio, Mastodon favourites/boosts/replies). Read-only against the platforms, but it WRITES the fetched metrics into pendpost state (state.json) - which is why it is not flagged read-only; the schedule…
forget_engagerForget one person in the relationship memory (spec 49 S6): erases their local exchange history and replaces the record with a minimal keyed tombstone { lane, handleNorm, forgotten:true }, so a later comment by the same handle does NOT re-accrete. This is a privacy erase and cannot be undone (the erased history does not come back; un-forget only lets the person accrete again from zero). DESTRUCTIVE and confirm-gated: a bare call returns needs_confirm - pass confirm: true (the Studio confirms inline first). Local-only. Owner-driven regardless of the agent-read opt-in. Twin of POST /api/engagers/…
gbp_attributes_getRead the connected Google Business Profile location's attributes (hours/description are NOT attributes - a documented out-of-scope follow-up, see spec 19 §4): { ok, items:[{ id, valueType, values }], needsScope?, scope? }. id is the full attribute resource name (e.g. attributes/has_wifi) - pass it verbatim to gbp_attributes_set. A project not yet allowlisted for the Business Profile APIs returns needsScope with scope "business.manage"; a genuine read failure is ok:false (never a false-empty items:[]). Pull-on-demand, never persisted. Twin of GET /api/gbp/attributes. Read-only, open-world (reac…
gbp_attributes_setUpdate ONE attribute on the connected Google Business Profile location: { ok, id:<attributeName> }. attribute is the full resource name from gbp_attributes_get items[].id (e.g. attributes/has_wifi); value is sent as a boolean when it is literally "true"/"false", otherwise as-is (a numeric-looking string is never silently coerced to a number). PATCH is an upsert, so re-sending the same attribute+value is safe. A project not yet allowlisted returns not_configured with the scope to authorize. Twin of POST /api/gbp/attributes.
gbp_media_addAdd one photo/video to the connected Google Business Profile location's gallery: { ok, id:<mediaName>, googleUrl }. Pass EXACTLY ONE of sourceUrl (an absolute http(s) URL - a single API call) or filePath (a client-root-relative local path - the engine runs a two-step resumable upload: startUpload, then the raw bytes, then media.create). category is required (one of COVER|PROFILE|LOGO|EXTERIOR|INTERIOR|PRODUCT|AT_WORK|FOOD_AND_DRINK|MENU|COMMON_AREA|ROOMS|TEAMS|ADDITIONAL); format defaults to PHOTO. Unlike GBP local posts (which take media by public sourceUrl ONLY), the gallery accepts local by…
gbp_media_listList the connected Google Business Profile location's photo/video gallery: { ok, items:[{ id, format:"PHOTO"|"VIDEO", category, thumbnailUrl, googleUrl, createTime }], needsScope?, scope? }. Account-level media management, distinct from assets_list (the LOCAL data/media render library) - this gallery lives on Google. A project not yet allowlisted for the Business Profile APIs returns needsScope with scope "business.manage"; a genuine read failure is ok:false (never a false-empty items:[]). Pull-on-demand, never persisted. Twin of GET /api/gbp/media. Read-only, open-world (reaches Google).
generate_digestRender the performance digest (locale-aware markdown, rendered in the active client posting locale en or de-CH with locale-aware dates) from stored metrics + plan state: published posts of the last 7 days with per-platform metrics, all measured posts, queue/overdue/scheduler/account health, the next due posts. Honest about gaps ("no metrics yet"). Read-only.
ghost_member_createAdd one member to the connected Ghost site: { ok, id }. labels/newsletters accept either a comma-separated string or an array; each newsletters entry may be a slug or a 24-char Ghost id. labels double as the segment target spec 01's emailSegment ("label:<slug>") later narrows an email to. NOT idempotent - repeated calls each attempt a new member; a duplicate email returns invalid_input. A missing GHOST_ADMIN_API_KEY returns not_configured. Twin of POST /api/ghost/members.
ghost_membersRead the connected Ghost site's member list: { ok, counts:{total,free,paid,comped}, items:[{id,email,name,status,labels,newsletters}] }. limit/page paginate items (default 15/1); filter is a raw Ghost NQL filter (e.g. "label:vip") narrowing BOTH items and the counts breakdown. Segments = labels - Ghost has no first-class segment object, so a member's labels ARE the audience spec 01's emailSegment ("label:<slug>") targets. A missing GHOST_ADMIN_API_KEY or a genuine read failure resolves ok:false (never a false-empty items:[]). Read-only, open-world (reaches Ghost). Twin of GET /api/ghost/member…
ghost_members_importBulk-add members to the connected Ghost site from a CSV file or an inline row array: { ok, created, skipped, failed:[{email,error}] }. Pass EXACTLY ONE of file (a client-root-relative local CSV path, columns email,name,note,labels,newsletters) or rows (an array of {email,name?,note?,labels?,newsletters?} objects). RESILIENT: a duplicate email is tallied as skipped, any other rejected row lands in failed[] with its message - the batch NEVER aborts partway. file also accepts upload:true to take Ghost's native multipart CSV-upload fast path instead of the per-row loop. A missing GHOST_ADMIN_API_K…
ghost_newsletter_createCreate a newsletter on the connected Ghost site: { ok, id, slug }. NOT idempotent - repeated calls each create a new newsletter. subscribeOnSignup opts new site members into it automatically (Ghost defaults this to true when omitted). A missing GHOST_ADMIN_API_KEY returns not_configured; a rejected field returns invalid_input. Twin of POST /api/ghost/newsletters.
ghost_newsletter_updateArchive, activate, or rename a newsletter on the connected Ghost site: { ok, id, status }. id is from ghost_newsletters; status is active|archived (only an active newsletter can be emailed by spec 01). IDEMPOTENT - re-applying the same status is safe. A missing GHOST_ADMIN_API_KEY returns not_configured; an unknown id or rejected field returns invalid_input. Twin of POST /api/ghost/newsletters/update.
ghost_newslettersRead the connected Ghost site's newsletter roster: { ok, items:[{id,slug,name,status,subscribe_on_signup,members_count?}] }. status is "active" or "archived" - only an active newsletter can be emailed (spec 01's ghostEmail/newsletter fields). A missing GHOST_ADMIN_API_KEY or a genuine read failure resolves ok:false (never a false-empty items:[]). Read-only, open-world (reaches Ghost). Twin of GET /api/ghost/newsletters.
health_recheckRun a live liveness probe per platform and store the result in pendpost state (account_status.<platform>.live). Each probe is a single read-only call that proves the credential actually authenticates (LinkedIn token introspection, YouTube channels.list, Meta GET me) - it can never publish. The Meta probe is skipped while a 368 block is recorded. Returns { ok, health }. Use this to actively re-probe a lane's credential now; use pendpost_health for a cached go/no-go readiness roll-up without new probes.
link_engagersLink two people in the relationship memory as the same person (spec 49 S4c): stores a durable operator-confirmed association so their two lanes' exchanges present as one joined history. This is NOT a merge - both records stay separate on disk, byte-intact and independently forgettable; no exchange is moved or copied, and the link is fully reversible (unlink_engagers). There is deliberately NO merge verb. Additive and reversible, so NO confirm is needed. Idempotent (a pair is never double-stored). Local-only. Owner-driven. Twin of POST /api/engagers/link.
list_commentsRead the inbound comments on one POSTED post, normalized across lanes (meta/youtube/linkedin/wordpress/reddit/tiktok/telegram/mastodon/nostr/discord): { ok, items:[{ kind, commentId, author, text, ts, postId, permalink?, parentId? }], platform, postId, needsScope?, scope? }. Optional platform picks the lane when a post hit several; otherwise the first comment-capable lane. Pull-on-demand, never persisted, never a publish. Twin of GET /api/comments. Read-only, open-world (reaches the platform).
list_engagersRead the relationship memory for the active brand (spec 49): the accreted history of the humans who keep engaging. With lane AND handle, returns one person { ok, key, engager:{ lane, handle, exchangeCount, exchanges:[{ kind, ts, direction, excerpt, permalink?, rating? }], firstSeenTs, lastSeenTs, lastRating? } | forgotten-tombstone | null, suggestions:[cross-lane guesses], links:[confirmed same-person links] } - the SAME record the Studio history popover renders. With lane/handle omitted, returns the full brand list { ok, engagers:[{ key, ...record }], links } (storage is unbounded; limit boun…
list_inbound_eventsRead the normalized, idempotently-merged inbound-event feed (comment | mention | message | reaction): { ok, events:[{ eventId, type, platform, clientId, postId, externalPostId, author, text, reaction, parentId, permalink, ts }] }, newest first. Every pull merges the cloud's latest delta into a capped per-client store (deduped by `eventId`) and returns the WHOLE accumulated store, so the feed is stable across polls (never self-erases on an empty delta) and every consumer (Studio, this tool, the GET twin) sees the same events. Optional `since` is an OPAQUE cursor (as returned internally from a p…
list_reviewsRead the Google Business Profile reviews for the connected location, normalized: { ok, items:[{ kind:"review", commentId, author, text, ts, rating, reply, replyTs, platform:"gbp", postId:null }], averageRating, totalReviewCount, needsScope?, scope? }. Newest-first. Location-scoped (a review is about the business, not any post - no campaign/postId). New reviews are logged as review-received Activity entries for the inbox. A project not yet allowlisted for the Business Profile APIs returns needsScope with scope "business.manage"; a genuine read failure is ok:false (never a false-empty items:[]).…
mark_postedMark a planned post as posted because the owner published it natively OUTSIDE pendpost (e.g. in the Meta/LinkedIn app). Sets status:posted so it leaves the publish-due queue; NEVER triggers a real publish and never mints a platform id. Optionally record the externalUrl of the live post. On a MIXED multi-lane post pass `platform` to record ONE lane only - the post stays open (keeps owing its other lanes) until every lane is marked or fires, and it closes on its own once the last lane is in.
mastodon_followFollow or unfollow a Mastodon account: { ok, id, acct, following, platform:"mastodon" }. acct is "user", "@user", "user@remote.tld" or "@user@remote.tld" - resolved via a webfinger search. follow defaults to true (follow); pass follow:false to unfollow. An unresolvable acct returns invalid_input. A Mastodon token missing the write:follows scope returns not_configured with the scope to authorize. MCP-only - no GUI face (deliberate, honest deferral: follow/unfollow is graph state, not a per-post action). Twin of POST /api/mastodon/follow.
mastodon_pinPin or unpin a published Mastodon status to the profile: { ok, id, pinned, platform:"mastodon", alreadyPinned?, alreadyUnpinned? }. Pass EITHER statusId (an explicit status id) OR campaign+postId (a published Mastodon post carrying mastodonStatusId - the PostDetail "Pin to profile"/"Unpin" action uses this path). pinned defaults to true (pin); pass pinned:false to unpin. IDEMPOTENT - re-pinning/re-unpinning an already-in-that-state status is a safe no-op (alreadyPinned/alreadyUnpinned:true), never an error. A Mastodon token missing the write:accounts scope (minted before spec 31) returns not_c…
mastodon_update_profileEdit the connected Mastodon profile (display name, bio/note, website, avatar, header/banner) via accounts/update_credentials (scope write:accounts). Account-level, not a post. This makes a REAL, immediate change to the live account - only call it on the owner's explicit instruction. Requires confirm: true to apply. probe: true runs a read-only access-tier check and changes nothing. image/banner are LOCAL file paths the engine reads under the client root. The engine refuses unless the authenticated acct matches the client's MASTODON_HANDLE (it never edits the wrong account).
meta_lane_setSet the Meta (Facebook/Instagram) publishing lane: tune the anti-ban cadence cap and/or pause/resume the lane. Pass cadence:{maxPer24h,minGapMinutes} to set the cap (maxPer24h must be >=1 - the cap can never be disabled - minGapMinutes >=0, both integers); pass paused:true (with an optional reason) to STOP all Meta publishing or paused:false to resume. Cadence and pause/reason co-exist in one file, so a cadence-only call never unsets paused and vice-versa. Resuming the lane NEVER clears a recorded Meta-368 action block (clear that separately via pendpost_record_block once Meta confirms the lif…
moderate_commentModerate one inbound comment on a posted post (the twin of list_comments): hide/unhide/delete/approve/hold/spam/remove via the lane's moderation REST, returning { ok, id, platform, postId, commentId, action }. Each lane supports a SUBSET (meta: hide/unhide/delete; youtube: hold/approve/spam/delete; wordpress: approve/hold/spam/delete; linkedin/telegram/discord: delete; reddit: remove/approve/spam and is human-gated; tiktok/mastodon/nostr: none); an action the lane cannot do returns unsupported_action (never a false success), and a missing tier returns not_configured with the scope to authorize…
nostr_list_getRead a NIP-51 list for the connected Nostr identity: { ok, platform:"nostr", kind, id, items }. kind is 10000 (mute list - MUTED PUBKEYS, tagged `p` - muting a user, the canonical NIP-51 mute op), 10001 (pin list - PINNED EVENT ids, tagged `e`), or 30000 (follow-set - MEMBER PUBKEYS, tagged `p`, plus a `d` tag carrying the stable list identifier "pendpost"). items are the raw NIP-01 tag arrays from the latest matching event, filtered/verified so a kind-30000 read can never return a different client's follow-set under the same identity. A missing Nostr key/relay configuration, an unsupported ki…
nostr_list_setSet a NIP-51 list for the connected Nostr identity (a REPLACEABLE/PARAMETERIZED-REPLACEABLE event - this call REPLACES the whole list, it does not merge): { ok, id, kind, count, platform:"nostr" }. kind is 10000 (mute list - items are MUTED PUBKEYS, tagged `p` by default - muting a user is the canonical NIP-51 mute op, NOT muting an event), 10001 (pin list - items are PINNED EVENT ids, tagged `e`), or 30000 (follow-set - items are MEMBER PUBKEYS, tagged `p`, carried under a stable pendpost-managed `d` identifier so a re-publish edits the SAME list in place). A missing Nostr key/relay configura…
nostr_relay_list_getRead the connected Nostr identity's NIP-65 relay list (kind 10002): { ok, platform:"nostr", kind:10002, id, items:[["r",url,marker?], ...] }. Each item is the raw `r` tag - a 2-element tag ([r,url]) means both read+write, a 3-element tag ([r,url,"read"|"write"]) is direction-restricted. A missing Nostr key/relay configuration or a genuine read failure resolves ok:false (never a false-empty items:[]). MCP-only - no GUI face. Twin of GET /api/nostr/relay-list. Read-only, open-world (reaches the configured relays).
nostr_relay_list_setSet the connected Nostr identity's NIP-65 relay list (kind 10002, a REPLACEABLE event - this call REPLACES the whole list, it does not merge): { ok, id, count, platform:"nostr" }. relays is a non-empty array of either a bare wss:// URL string (both read+write) or an { url, read?, write? } object (set exactly one of read/write true to mark a direction-restricted relay; both/neither means unrestricted). A missing Nostr key/relay configuration returns not_configured; a malformed URL returns invalid_input. MCP-only - no GUI face. Twin of POST /api/nostr/relay-list.
nostr_update_profileEdit the connected Nostr kind-0 profile metadata (name, about, picture, nip05, website) by GET-merging the current profile off a relay and publishing a freshly-signed kind-0 event (NIP-01) to every configured relay - ok when at least one relay accepts it. Account-level, not a post. This makes a REAL, immediate change to the live profile - only call it on the owner's explicit instruction. Requires confirm: true to apply. probe: true reports the local identity (no relay call, no change). No OAuth scope: the sealed NOSTR_PRIVATE_KEY IS the identity, so this can never edit a sibling client's profi…
pendpost_healthOne-call readiness check (SS-10): { ready, blockers[], schedulerRunning, nextDue[] } - global blockers (manifest, credentials, Meta block, scheduler off) plus per-post blockers for the next N due posts. Use this for an overall go/no-go readiness roll-up; use health_recheck to actively re-probe a single lane's credential. Read-only.
pendpost_record_blockRecord a Meta action block (error 368) so pendpost and the scheduler skip the Meta lane. A 368 carries no machine-readable clear time, so the block stays active until it is EXPLICITLY cleared: pass blockedUntil: null (source: "owner") to record "block cleared" once you have confirmed out of band that Meta lifted it. blockedUntil on a new block is only a recorded-at anchor, not an auto-expiry. Use immediately when any Meta publish fails with error code 368 - and never retry the publish.
pinterest_board_createCreate a Pinterest board: { ok, id, name }. privacy is one of PUBLIC|PROTECTED|SECRET (Pinterest defaults to PUBLIC when omitted). NOT idempotent - repeated calls each create a new board. A missing boards:write scope (a token minted before spec 29) returns not_configured with the scope to authorize - a reconnect, not App-Review. A name clash or other rejected field returns invalid_input. Twin of POST /api/pinterest/boards.
pinterest_board_section_createCreate a section on a Pinterest board: { ok, boardId, id, name }. boardId is from pinterest_boards_list. NOT idempotent - repeated calls each create a new section. A missing boards:write scope returns not_configured with the scope to authorize; a name clash returns invalid_input. Twin of POST /api/pinterest/boards/:boardId/sections.
pinterest_board_section_updateRename a Pinterest board section: { ok, boardId, id }. boardId/sectionId are from pinterest_boards_list/pinterest_list_board_sections. IDEMPOTENT - re-sending the same name is safe. A missing boards:write scope returns not_configured with the scope to authorize. Twin of PATCH /api/pinterest/boards/:boardId/sections/:sectionId.
pinterest_board_updateUpdate a Pinterest board: { ok, id }. boardId is from pinterest_boards_list; pass at least one of name/description/privacy (PUBLIC|PROTECTED|SECRET). IDEMPOTENT - re-sending the same fields is safe. A missing boards:write scope returns not_configured with the scope to authorize; a rejected field returns invalid_input. Twin of PATCH /api/pinterest/boards/:boardId.
pinterest_boards_listList this Pinterest account's boards: { ok, platform:"pinterest", boards:[{id,name,privacy,pinCount}], current:<PINTEREST_BOARD_ID|null> }. Shares the same read connect_discover uses for pinterest, plus privacy/pinCount. A read failure resolves ok:false with an error (not_configured | needs_scope | engine_failure) - a failed read is NEVER a false-empty { ok:true, boards:[] }. Read-only, open-world (reaches Pinterest). Twin of GET /api/pinterest/boards.
pinterest_list_board_sectionsList a Pinterest board's sections: { ok, platform:"pinterest", boardId, items:[{id,name}] }. boardId is optional (defaults to the connected PINTEREST_BOARD_ID). A missing board/credential, an ungranted scope (media:write predates a token minted before spec 17 does NOT block this read - boards:read already covers it), or a read failure resolves ok:false with an error (not_configured | needs_scope | engine_failure) - a failed read is NEVER a false-empty { ok:true, items:[] }; publishing to the board root still works with no section picked. Read-only, open-world (reaches Pinterest). Twin of GET /…
plan_create_postCreate a new post in a campaign - ALWAYS as a draft (approval can only be flipped by approve_post from a different actor). Required: campaign, actor, post.id, post.type (reel|story|video|text|youtube-short|youtube-longform|poll|carousel|image|nostr-longform), post.platforms. Optional: caption, firstComment, title, link (article URL for type=text LinkedIn posts), image (absolute http(s) Cloudinary hero URL - LinkedIn renders it as the article-card thumbnail for type=text posts), scheduledAt (ISO), file, path, executionMode, description (also the LinkedIn article-card description line), tags (co…
plan_delete_postDelete a post row from its plan, in one motion: any natively scheduled platform object the row still owns (FB scheduled post, YouTube publishAt video, Mastodon queue entry, WordPress future post, Ghost scheduled post, Discord guild event) is cancelled platform-side FIRST, then the row is removed - no separate unschedule ceremony. A native cancel failure returns engine_failure with the row intact. Refuses posts with publish evidence (posted / platform ids) unless force: true - an already published copy always stays live on the platforms.
plan_getGet the full normalized posts of one campaign (captions, schedule, platforms, per-platform publish ids, media availability, approval, derived state). Optionally a single post via postId. Read-only.
plan_listList all social campaigns (plan files) with per-state post counts, the next due post, schedulerRunning and manifestError. Read-only.
plan_update_postUpdate owner-editable fields of a post (caption, firstComment, title, scheduledAt, platforms, type, file, path, executionMode, link, image, description, tags, blogSlug, audience, altText, newsletter, emailSegment, emailOnly, metaTitle, metaDescription, wpCategories, featureImageAlt, publishAsDraft, liAuthor, tgCta, dcEmbed, ttInteraction, spoilerText, xReplySettings - NEVER approval/cover/publish-result fields). altText is the image alt-text threaded to x/wordpress/pinterest/instagram media calls on publish (on IG: the feed IMAGE container only). newsletter/emailSegment/emailOnly refine Ghost'…
platform_validatePer-platform readiness of one post: media present, caption length caps, credentials/auth, Meta action block, YouTube future-publishAt, approval state. Returns { platform: { ready, problems[] } }. Use this for full per-platform publish readiness; use validate_media for just the local media probe. Read-only.
presubmit_checkCheck a post's reddit/tiktok platform-specific submission rules before publish: subreddit flair/title/restricted-type/submission-type rules (reddit, checked against THIS post's resolved redditSubreddit) and creator caption/privacy limits (tiktok, via creator_info). Returns { ok, postId, platforms: { <reddit|tiktok>: { ready, problems:[{code,text}], warnings:[{code,text}] } } } - only reddit/tiktok posts produce an entry; every other platform is absent. Reddit (spec 36): a required-flair sub with no picked flair is now a BLOCKING problem (flairRequired), and each post is checked against its own…
publish_due_runRun one publish-due sweep NOW (spawns the real engines for due, approved posts; optionally scoped to one campaign/post). This publishes REAL content - only call it on the owner's explicit instruction, and never while a Meta 368 block is active. Requires confirm: true.
publish_previewRead-only publish preview / dry-run (C3): for each due post in the horizon, reports which posts would fire, on which lanes, in which mode (mock|live), and with what blockers - { ok, ready, schedulerRunning, posts:[{campaign, postId, scheduledAt, platforms:[{platform, lane, mode, ready, blockers[]}]}] }. facebook + instagram both resolve to the meta lane (mode is resolveMode of the LANE, matching the engines/ModeBadge). It DESCRIBES readiness (approval!=approved, missing media, a recorded Meta-368 block) but NEVER publishes, NEVER spawns an engine, and NEVER writes - a 368 surfaces as a blocker…
radar_agent_comparisonHave the operator's own agent write the comparison page one Radar backlog entry is asking for. pendpost spawns it with the entry's buyer phrases as the brief and it calls radar_draft_comparison itself; the page lands as a DRAFT for a human to edit. Read the entries from radar_list geo.comparisonBacklog[]. Requires a connected agent AND a connected long-form lane (wordpress or ghost) - a comparison page publishes on the operator's own site, so with none connected it returns not_configured. It SPENDS the operator's subscription. Returns { ok, enabled, drafted, detail }. Twin of POST /api/radar/c…
radar_agent_scanRun a REAL research job on the operator's own agent CLI: pendpost spawns it with the saved query as a brief and its own MCP config, and the agent researches and calls radar_ingest itself. Returns { ok, enabled, job } where job is { id, queryId, scope, providerId, startedAt, finishedAt, state: running|done|failed, accepted, dropped, deduped, exitCode, reason, tail }. The counts are pendpost's OWN tally of what landed, never the agent's self-report. Requires posting.radar.agent.provider (owner-only) AND a proven-live agent - run agent_recheck first; there is NO fallback scan, because a scan that…
radar_agent_stopStop the running Radar agent research job for this client. The child is killed, the job ends state:"failed" with reason:"stopped", and anything it ALREADY ingested stays in the feed (those were real findings; deleting them because the search was stopped would be its own kind of lie). Returns { ok, stopped, job }. jobId is optional - at most one job runs per client - but when given it must match the running job, so a stale button can never kill a newer one. Stopping an already-finished job is a no-op ({ stopped:false }), never an error. Twin of POST /api/radar/agent-stop.
radar_draft_comparisonDraft the comparison page one Radar backlog entry is asking for (e.g. "pendpost vs Buffer"), as a DRAFT post in the named campaign. Read the entries from radar_list geo.comparisonBacklog[] - each carries { key, title, buyerPhrases[], examples[] }: the phrases are what real buyers actually typed, so write the page that answers THOSE, not a feature grid. backlogKey must match a current entry (pendpost decides what there is to write about, from signals it saw; you cannot invent a topic and file it as a Radar finding). YOU write the prose - pendpost never calls a model. It needs a CONNECTED long-f…
radar_engage_reportReport your "Respond for me" TRIAGE decisions for the Radar (beta) signals your brief listed (spec 50): one decision per signal, kind act | skip | ask. It records decisions and queues actions LOCALLY - it never posts, likes, follows or messages anything itself, and it can never approve or publish. pendpost re-checks EVERY rule server-side whatever you report, and refuses a row with a code rather than quietly changing it: the signalKey must be one from your brief (target_fenced), an act needs the project's minimum score (below_threshold), its platform must be enabled (lane_disabled) and each ki…
radar_followup_checkCheck now whether the original authors of the threads you replied into have replied BACK to your posted Radar replies: { ok, enabled, checked, replied, sources, agentFollowup? }. For every posted reply not yet answered, a READ-only re-read of your comment's thread on reddit/mastodon/bluesky; on a hit it stamps an "author replied" badge on that signal (and the daily digest counts it). x/youtube/nostr replies (no readable API for pendpost) are covered by the CONNECTED AGENT: when such targets are due this forced check ALSO spawns the followup-scope agent job (budget-EXEMPT - a manual check is yo…
radar_followup_reportReport ONE follow-up check result for a target your follow-up brief listed: did the thread's ORIGINAL author answer OUR posted reply on this x/youtube/nostr thread? Call it exactly once per target from your brief. replied:false records "checked, no answer yet" (lastCheckedTs only). replied:true is an EVIDENCE-GATED claim and every field is verified server-side against what pendpost itself recorded: author must be the thread author from the brief (normalized match), permalink must be a real absolute URL on the lane's own host proving the reply (x: the author's /<handle>/status/<id> tweet URL; y…
radar_footprint_logLog one LLM-footprint result for the Radar (beta) GEO layer: you (the agent) run a buying question against your OWN model access, then report whether pendpost was mentioned. Appends { question, mentioned, competitorsMentioned, excerpt, assistant, ts } to the footprint trend (state.radar.geo.footprint) - pendpost NEVER calls a model itself (zero-dep), it only stores your reported result. Over time this tracks the mention-rate the panel/digest shows. Returns { ok, question, mentioned, footprintRate:{ checks, mentioned, rate }, count }. Read the trend + the comparison-page backlog via radar_list …
radar_geo_resetReset (clear) this client's Radar GEO / AI-visibility state: the agent-logged footprint results, the derived comparison-page backlog, and the dismissed-backlog ledger. Use this when a project's KI-Sichtbarkeit state was polluted with another brand's competitors or buying questions - a config edit cannot fix it, because this is Radar STATE, not config. OWNER-ONLY (it drops agent-logged history). Idempotent and local: it makes no outbound request and posts nothing; comparisonBacklog recomputes on the next scan. Returns { ok, cleared: { footprint, comparisonBacklog, dismissedBacklog } } with the …
radar_ingestSubmit conversations you found (with your own web search / browse) as scored Radar (beta) signals - the credential-free scan path. First read the saved queries via config_get (posting.radar.queries); then, for the queryId you are scanning, search the named sources AND the open web for RECENT conversations where someone asks for, recommends, or compares tools like this product, and submit the candidates here. pendpost dedupes by source+externalId, persists the ranked feed, and stamps lastScan. Score each signal YOURSELF (`score` 0-100 + a one-line `reason`): you read the thread, and the fallbac…
radar_listRead the cached Radar (beta) signal feed + GEO summary: { ok, enabled, view, items:[Signal], lastScan, sources, geo:{ comparisonBacklog, footprint, footprintRate, shareOfVoice, buyingQuestions }, capabilities, nextScan, lastProduced }. nextScan says when the next UNATTENDED runs fire, derived from the same gate the scheduler ticks: { timezone, dailyAt, agent:{ armed, at, lastAt, budget, spent }, keyword:{ armed, at, lastAt }, followup:{ at, lastAt, agentDue } } (agent = the daily agent research incl. its honest budget/spent; keyword = the free daily keyword sweep; followup = the author-reply r…
radar_mark_copy_postedRecord that a COPY-DRAFT Radar (beta) signal was posted BY HAND. The copy-draft lanes (hackernews, nostr, and non-Enterprise x) and reddit KARMA post-ideas have no reply API, so the operator copies the drafted text and posts it themselves - there is no minted id and no replied evidence to prove it went out. This stores a durable { postedUrl?, ts } marker on the signal (keyed by source+externalId, surviving a re-scan), which is what lets the feed count the copy draft as answered instead of forever "drafted". Identify the signal by source (items[].source) + externalId (items[].externalId); optio…
radar_queue_replyQueue an approval-gated reply to a Radar (beta) signal's EXTERNAL thread (reddit/mastodon/bluesky/youtube), or save a COPY-PASTE suggestion on the signal itself for the sources with no usable reply API (hackernews, nostr, and - since X's Feb 2026 tier restriction - x, unless the owner declared posting.radar.xEnterprise, which flips x into the reply lane). youtube posts a top-level comment on the video, always human-gated. For a reply-capable source this creates a PENDING reply-post carrying radarReplyTo = the signal's { url, source, externalId } and caption = your drafted text, in the named ca…
radar_scanRun the project's saved Radar queries now and return the ranked, deduped signal feed. This is the DEMOTED plain keyword-match path (spec 41): radar_agent_scan (real agent research) is the PRIMARY scan - prefer it when a proven-live agent is configured; this one only string-matches the saved queries and never drafts. Returns: { ok, enabled, items:[{ source, externalId, url, author, text, matchedQuery, community, ts, intentScore, intentTags[], suggestedAction }], sources:{<source>:{ ok }|{ ok:false, error, scope? }}, lastScan, scanned }. Sources: reddit/hackernews/bluesky/mastodon. Each hit is s…
radar_triageTriage one cached Radar (beta) signal OR one GEO comparison-backlog entry. For a SIGNAL: dismiss / watch / clear - dismiss removes it from the feed and records it so a re-scan never re-surfaces it (US6); watch pins it to the top and exempts it from the retention prune (US7); clear undoes both. Identify the signal by source (any items[].source: reddit/hackernews/bluesky/mastodon plus the agent-ingested x/youtube/web) + externalId (from radar_scan/radar_list items[]). For a GEO BACKLOG ENTRY (a "page worth writing" the operator declines): pass backlogKey (from radar_list geo.comparisonBacklog[].…
radar_triage_bulkDismiss a LIST of cached Radar (beta) signals in ONE call - e.g. to clear out a low-quality query's whole output at once instead of one radar_triage per row. Pass ids as an array of { source, externalId } (each source is any items[].source: reddit/hackernews/bluesky/mastodon plus the agent-ingested x/youtube/web; each externalId from radar_scan/radar_list items[]) and action "dismiss". Dismiss semantics are identical to radar_triage: each signal is removed from the feed and recorded so a re-scan never re-surfaces it (US6). TWO safety guards: a WATCHED signal is NEVER dismissed (it is skipped a…
radar_tuneTune the saved Radar searches from what the owner marked not relevant. action "run" starts one tune job now on the operator's own agent (no tools, one spawn, at most one job per client; the daily sweep also runs it once a day when a search has 5 new "not relevant" marks or a week of results): it reads each search's config, its not-relevant and more-like-this examples and its precision stats { shown, less, done, drafted, replied }, and answers with a diff. NARROWING changes are applied on their own through config (the full queries array, revision-checked): added excludeKeywords, muted authors o…
react_to_postReact to one inbound comment or mention on a posted post (the reaction twin of list_comments): like/favourite/boost/emoji via the lane's reaction REST, returning { ok, id, platform, postId, commentId, reaction, removed }. Each lane supports a SUBSET (linkedin: like/praise/empathy/appreciation/interest/entertainment; mastodon: favourite/boost; nostr: like/emoji; telegram/discord: emoji; meta/youtube/wordpress/reddit/tiktok: none - reddit voting is ToS-prohibited); a reaction the lane cannot do returns unsupported_reaction (never a false success), and a missing tier returns not_configured with t…
read_insightsRead the STORED post metrics + history WITHOUT re-fetching (the twin of GET /api/insights). Returns { ok, lastFetch, items:[{ campaign, postId, platform, metrics, fetchedAt, history, postType, caption, mode }], metricLabels, mode, account, summary }. summary is the performance-memory ranking (R8): { hasEnough, measured, minMeasured, byLane, byType, byHour }, each a list of { key, avg, total, posts } sorted by AVERAGE engagement (interaction-count signals only - never exposure or a 0-1 rate) so effectiveness, not post volume, leads. hasEnough is false below minMeasured measured posts (read it a…
reddit_list_flairsList a subreddit's link-flair templates: { ok, platform:"reddit", subreddit, items:[{id,text,editable,cssClass}] }. subreddit is optional (defaults to the connected REDDIT_SUBREDDIT). A missing credential/subreddit, an ungranted flair scope, or a read failure resolves ok:false with an error (not_configured | needs_scope | engine_failure) - a failed read is NEVER a false-empty { ok:true, items:[] }; publishing still works without a flair. Read-only, open-world (reaches Reddit). Twin of GET /api/reddit/flairs.
reject_postReject a post (approval: rejected) with an optional note explaining what to fix. Same actor rules as approve_post.
rename_assetRename one media file within data/media, renaming its paired .jpg cover sibling to match. sanitizeAssetName runs on BOTH names (rejects path segments / leading dots / bad charset / a disallowed extension); the extension may NOT change. Never overwrites: an existing toName is invalid_input. Confirm-gated + in-use-protected: renaming a file referenced by a plan post breaks that post's media reference, so it refuses with needs_confirm naming the using post(s) unless confirm:true (the plan rows are not auto-rewritten). A missing source is invalid_input.
reply_to_commentReply to one inbound comment on a posted post (the paired WRITE of list_comments): posts the reply through the lane's reply verb and returns { ok, id, platform, postId, commentId }. Operator-triggered - Reddit especially is human-gated (never auto-reply). This posts PUBLIC text to the live thread immediately (no approval fence), so it is confirm-gated for agents: actor 'owner' (the Studio Comments panel) replies without confirm; ANY other actor must pass confirm: true or the call is refused with needs_confirm. A missing comment tier returns not_configured with the scope to authorize. Not a sch…
reply_to_inbound_eventAnswer one inbound X Activity event from the inbox (the paired WRITE of list_inbound_events): mention/reply -> a tweet reply via X's mention-exemption; message -> a direct message to the sender; reaction/follow -> not repliable (returns code not_repliable, no thread to answer). Finds the event by eventId in the local per-client inbox store and returns { ok, id, platform, eventId }. This posts PUBLIC text / a DM immediately (no approval fence), so it is confirm-gated for agents: actor 'owner' (the Studio inbox) sends without confirm; ANY other actor must pass confirm: true or the call is refuse…
reply_to_reviewReply to one Google Business review (the paired WRITE of list_reviews): upserts the owner reply via PUT, or REMOVES it when text is empty/omitted, returning { ok, id, platform:"gbp", reviewId }. reviewId is the full resource name from list_reviews items[].commentId. Operator-triggered, required actor. NOT a scheduled publish and never sets approval. This posts a PUBLIC owner reply immediately, so it is confirm-gated for agents: actor 'owner' (the Studio Reviews panel) replies without confirm; ANY other actor must pass confirm: true or the call is refused with needs_confirm. A project not yet a…
rescheduleMove a post to a new scheduledAt (ISO datetime). Waiting-due posts just change their due time; NATIVELY-scheduled posts have their platform object(s) deleted and re-queue for the new time (confirm: true required). Use this to move a post's time; use unschedule to park it entirely.
resume_laneResume a publishing lane that an account-level circuit breaker halted (the first case: X HTTP 402 - API credits depleted, which pauses the WHOLE x lane so retries do not burn quota). Clears the lane block, releases the posts that lane's credits failures parked, then re-fires those posts immediately as a credit recheck and reports the outcome: { cleared, released, published, stillDepleted, pending }. Because X exposes no credits-balance API, the re-fire IS the recheck - if credits are back the posts publish (published>0, stillDepleted:false); if they are still out the lane re-arms (stillDeplete…
reviewer_createInvite a reviewer to a client's review link: mints a 128-bit token and returns { ok, reviewer, token, actorString }. The token (the whole link identity) is returned EXACTLY ONCE and is NEVER retrievable again - only its 4-char tail is displayable afterwards; if it is lost, revoke and invite again. Owner-gated: requires actor:"owner" (minting a bearer capability is owner-only, the posting.autoApprove precedent). expiresAt is OPTIONAL and nullable (default null = the link lives until explicitly revoked, owner decision O3); pass an ISO timestamp to opt into an expiry. A duplicate ACTIVE name for …
reviewer_listList a client's review-link reviewers (the client-review-link identities): [{ id, name, tokenTail, createdAt, createdBy, revokedAt, expiresAt, revoked, expired, active }]. Targets the per-call clientId (else the active client). Responses carry ONLY the 4-char token tail - the full link is shown exactly once at mint (reviewer_create) and is never retrievable again. Read-only.
reviewer_revokeRevoke a client's reviewer by id: the token stops verifying immediately and the review link dies (the intended lifecycle end, matrix row 19). Owner-gated: requires actor:"owner". Idempotent - revoking an already-revoked reviewer is a no-op success. Returns { ok, reviewer } with revoked:true. To restore access, invite again (reviewer_create) - a revoked token can never be un-revoked. Never touches credentials.
scheduler_setStart or stop the in-process publish scheduler (60s tick over active campaigns; only approved + fully-scheduled posts publish). The setting persists across restarts.
send_zapSend a Lightning zap (NIP-57 value-for-value, the twin of the sats-earned insight) to a PUBLISHED nostr note: { ok, id:<preimage>, platform:"nostr", postId, sats }. Spends REAL sats from the connected Nostr Wallet Connect (NWC) wallet - this is an irreversible money movement, so it requires confirm: true and only on the owner's explicit instruction. The recipient is the note's author (resolved via their Lightning address); amount is in whole sats; comment is optional. A missing NWC wallet returns not_configured with scope "nwc" (connect a Lightning wallet); a wallet reject/timeout returns engi…
set_coverSet a cover override for one post. Pass exactly ONE source: frameSec (extract that second of the post's own video), filePath (a repo-local JPEG/PNG/WebP), base64 (image bytes, max 4 MB), or coverUrl (an absolute https image URL the CALLING APP hosts). coverUrl is the Instagram grid-safe path: IG applies it as cover_url on the reel container, so a calling-app-designed title card that centres its text survives the profile-grid centre-crop (pendpost stays dependency-free and renders no text; the engine falls back to a frame if IG rejects the URL). frameSec/filePath/base64 materialize data/plans/<…
telegram_update_profileEdit the MANAGED Telegram channel's title, description, and/or photo (setChatTitle / setChatDescription / setChatPhoto) - NOT the bot's own BotFather profile. Account-level, not a post. This makes a REAL, immediate change to the live channel - only call it on the owner's explicit instruction. Requires confirm: true to apply. probe: true runs a read-only access-tier check (confirms the bot administers the channel) and changes nothing. image is a LOCAL file path. The engine refuses unless the bot is an administrator (with "Change info" rights) of the configured TELEGRAM_CHANNEL_ID (it never edit…
token_refreshRefresh a platform token programmatically. linkedin and x are refreshable (each wraps its engine's refresh); Meta uses a long-lived page token and YouTube refreshes per call. On refresh-token expiry the hint carries the interactive re-auth command.
unforget_engagerUn-forget one person in the relationship memory (spec 49 S6u): clears their forget-tombstone so future exchanges accrete again from zero. It does NOT resurrect the erased history (forget really erased it) - only new exchanges accrete. A no-op on a person who is not forgotten (it never destroys a live record). Restorative and non-destructive, so NO confirm is needed. Local-only. Owner-driven. Twin of POST /api/engagers/unforget.
unlink_engagersUn-link two people in the relationship memory (spec 49 S4u): removes the operator-confirmed same-person association so each chip reverts to its own single-lane history. LOSSLESS - nothing was ever merged, so both records are byte-identical to before the link; un-linking cannot lose or duplicate anything. Confirm-gated because it removes an operator decision: a bare call returns needs_confirm - pass confirm: true. Local-only. Owner-driven. Twin of POST /api/engagers/unlink.
unscheduleTake a post off the schedule (executionMode: parked, so the scheduler ignores it). If the post is NATIVELY scheduled (FB scheduled post / YouTube publishAt video / Mastodon scheduled status / WordPress future post / Ghost scheduled post), every such platform object is DELETED via the engines - that needs confirm: true. Use this to park a post; use reschedule to move it to a new time, or reject_post to revoke approval.
validate_mediaProbe one post's local media file (ffprobe): resolution/codec/faststart/duration spec checks for 9:16 story / 4:5 feed. Use this for just the local file probe; use platform_validate for full per-platform publish readiness, or publish_preview for a dry-run across due posts. Read-only.
verify_postRead a handed-off post back from its platforms to confirm it is actually live (turns the guessed fired-assumed state into verified-live or verify-failed). Read-only against the platforms - spawns each engine's read-only verify subcommand and records the result in a non-destructive post.verify block (no publish, no minted id, no status change); this local annotation is why it is not flagged read-only. Meta is read even while its lane is paused (a read is not a blocked action). Use this to confirm a handed-off post is live; use mark_posted instead to record a post you published manually outside …
x_update_profileEdit the connected X (Twitter) profile (name, bio <=160, url, location, profile image, 1500x500 banner) via the v1.1 account/* endpoints (OAuth 1.0a). Account-level, not a post. This makes a REAL, immediate change to the live account - only call it on the owner's explicit instruction. Requires confirm: true to apply. probe: true runs a read-only access-tier check and changes nothing. image/banner are LOCAL file paths the engine reads under the client root. The engine refuses unless the authenticated handle matches the client's X_HANDLE (it never edits the wrong account).
youtube_playlist_addAdd a published YouTube video to a playlist: { ok, id, playlistId, videoId, duplicate? }. Pass campaign+postId to resolve a scheduled post's published ytVideoId, or an ad-hoc videoId directly with no post context. Re-adding an already-present video reports duplicate:true rather than inserting a second item. A missing youtube/youtube.force-ssl write scope returns not_configured with the scope to authorize - a reconnect, not App-Review. Twin of POST /api/youtube/playlists/:playlistId/items.
youtube_playlist_createCreate a YouTube playlist: { ok, id, title }. privacy is one of public|unlisted|private, defaulting to "private". NOT idempotent - repeated calls each create a new playlist. A missing youtube/youtube.force-ssl write scope (a token minted with only youtube.upload) returns not_configured with the scope to authorize - a reconnect, not App-Review. Twin of POST /api/youtube/playlists.
youtube_playlists_listList this YouTube channel's playlists: { ok, platform:"youtube", playlists:[{id,title,privacy,itemCount}], needsScope?, scope? }. Pull-on-demand, never persisted, no write side effect. Read-only, open-world (reaches YouTube). Twin of GET /api/youtube/playlists.
youtube_update_profileEdit the YouTube channel's branding (description, keywords, country, defaultLanguage) and/or localizations via a GET-merge-PUT over channels?part=brandingSettings,localizations (scope youtube - readonly cannot write). Account-level, not a post; the channel DISPLAY NAME (snippet.title) is NOT writable via this API. This makes a REAL, immediate change to the live channel - only call it on the owner's explicit instruction. Requires confirm: true to apply. probe: true runs a read-only access-tier check and changes nothing. The engine refuses unless the authenticated channel id matches the client's…
Signals
- Stars
- 14
- Forks
- 5
- Open issues
- 2
- Last commit
- Oct 14, 2026 (15 days ago)
- Last release
- Oct 14, 2026 (15 days ago)
From GitHub (fetched Oct 24, 2026).
Details
- Registry name
io.github.pendpost/pendpost- Version
- 2.7.0
- Auth
- No auth
- Packages
- npm pendpost
- License
- MIT
- Repository
- github.com/pendpost/pendpost
- Website
- pendpost.com
- First seen
- Oct 24, 2026
- Last checked
- Oct 24, 2026
Cite this page
Pendpost MCP server. Agent Mart. https://agentmart.dev/connectors/pendpost-pendpost (last updated 2026-10-24)