MCP Tool Catalog
Every tool the BlackOps MCP server exposes to your AI client, grouped by what it touches. Connect once — see Connect the MCP — and your assistant can reach any of these.
200 tools, generated directly from the live server so this list never drifts. New connections see a compact default surface; everything else stays one lookup away via describe_tools and use_tool — see Tool surfaces.
Account & Context (70)
acknowledge_answered_questionsMark answers from get_answered_questions as collected, so a later run does not act on them a second time. Call this ONLY after you have actually done something with the answer — acting twice is cheap, losing an answer someone took the trouble to give is not. An answer you read but decided to leave for the owner should stay unacknowledged.
calibrate_voiceLearn the user's writing voice from how they rewrote AI output. The single highest-signal voice correction is the user restating AI-generated text in their own words. Pass original_text (what the assistant wrote) and rewritten_text (the user's version); the tool compares them and returns a summary, a voice anchor, and suggested_changes — additive brand-voice rules in the exact shape patch_brand_voice accepts. It is READ-ONLY: it never changes the voice. Workflow: when the user says the voice is off, or whenever you produce voice-sensitive content, offer to let them restate any part in their own words. When they do, call calibrate_voice, show them the suggested_changes, and apply them with patch_brand_voice (confirm: true) only after they approve. Calibration only proposes additions, never removes or rewrites existing rules.
create_commCreate a Comm intake channel and bind it to a Sortie with a fire policy. Example: a "journal" channel mapped to a daily-note routine on a 21:00 schedule, or an "ideas" channel mapped to a logging routine that fires on every push. Use list_sorties to find the sortie_id to link.
create_huntCreate a saved Hunt — a persistent reply-targeting definition for X. A Hunt pairs search keywords + optional author allow/deny filters with a semantic target_brief and reply_intent, and can bind brain_ids/post_ids for grounded reply drafts. Bound post BODIES are first-class anchor-fact sources: drafts quote the post verbatim, and a reply whose anchor fact came from a bound post may carry the canonical URL of that post (budget-capped ~1-in-20 per hunt; the link only ever rides a reply standing on facts from that same post). After creating, call get_reply_targets with the returned hunt id to pull ranked reply targets. score_weights tunes the queue builder's per-hunt composite scoring: {relevance, slop, author_fit} relative weights plus {threshold, slop_veto} 0-100 cutoffs; omit for defaults. Requires domain or site_id. If the user did not specify a site, call get_me to list their sites — use it automatically if only one, otherwise ask the user to choose. Do NOT guess.
create_review_sessionOpen a human review session for a draft and return a shareable review URL. Works for a blog post (content_type "post", the default — pass post_id), or a LinkedIn, Threads, or TikTok draft (content_type "linkedin"/"threads"/"tiktok" — pass content_id instead). The reviewer opens the link and sees the draft rendered the way the platform will display it — the real post template for a blog post; plain text with line breaks preserved for LinkedIn/Threads; the attached video playing inline plus privacy/post-mode/disclosure-toggle state for TikTok. They can edit the text inline, select any span and leave a comment anchored to it, comment on (and, except for a TikTok video, delete) non-text elements, and then submit everything as one batch. Threads and TikTok views also show the live character count against that channel's limit (500 / 2200, including any UTM-tagged link) and flag more than one hashtag on Threads; a TikTok draft with no video attached is visibly flagged as unpublishable. Use this when the user wants to review a draft visually instead of trading edit instructions in chat, and BEFORE publishing anything they asked to review — post_linkedin, post_threads, and post_tiktok already return a review link at `links.review_url` on the created draft, so you often do not need to call this at all. Hand the returned URL to the user, then call get_review_feedback to wait for their submission. The link carries its own access token, expires (24h by default), and re-opening review for the same draft returns the existing open session rather than a second conflicting link.
create_sortieRegister a Sortie — a named webhook + (optional) bearer-token target a Comm fires, or that fires itself on a native schedule. Use this to wire up a fire target from chat: point it at a Claude Code routine fire URL or any HTTP webhook, set the default_payload (drop a {{comms}} token where a Comm's drops should land), and the headers. To put it on a cadence directly — no Comm or drops required — set schedule_enabled: true with a cadence (daily/weekdays/weekly/interval/cron); it fires its stored default_payload as-is on that schedule. SECURITY: the bearer token is encrypted at rest and never echoed back; to keep the secret out of the conversation, OMIT `token` and add it later in Settings → Sorties (the Sortie is created "unarmed" until a token is set). Returns the new sortie id to pass to create_comm / patch_comm.
delete_huntPermanently delete a saved Hunt by id. This is irreversible and removes the Hunt along with its history — to retire a Hunt while KEEPING its history, use patch_hunt with status "archived" instead. Requires confirm: true; the call is rejected without it.
delete_notesPERMANENTLY delete a BlackOps note and its vault file. Requires confirm:true. PREFER patch_notes with status:"archived" — archiving is reversible, hides the note from every query, AND removes the vault file, so it is usually all you need; delete only when the record itself must be gone (e.g. a duplicate). Two guards return 409 and are overridable with force:true: (1) the note must already be archived, so deletion is two deliberate steps rather than one call; (2) a note attached to a brain is refused, because removing a brain source note degrades the compiled brain and the loss only surfaces on the next compile. Requires a note UUID — slug and title are rejected here because they can match more than one note. Resolve the id with get_note or list_notes first.
delete_tiktok_by_idDelete a TikTok draft from BlackOps. NOTE: the TikTok API cannot delete published videos — a live video must be removed in the TikTok app. This tool only removes the BlackOps record.
draft_x_articleDraft a native X Article from a published blog post — long-form native content on X instead of a link post. The Article carries a block-boundary excerpt (default ~2,500 chars) of the post with real headings, lists, formatting, and clickable links, closes with the canonical link back to the full post, and reuses the post hero as the cover image. The draft lands in the X composer for review; it is NEVER published by this tool — use publish_x_article as the separate explicit step. Requires a connected X account with an X Premium+ subscription (Articles eligibility); ineligible accounts get a clear error. Requires confirm=true. If the post already has an Article draft, the existing ids are returned unless force=true.
drop_commsDrop a capture into a Comm — a named intake channel ("journal", "ideas") that accumulates short notes through the day and fires a linked Sortie on its policy. Use this whenever the user says "add this to my <name> comms", "log to <name>", or wants to stash a thought/update/idea against a channel. An on_push channel fires its routine immediately; a scheduled one accumulates until its daily time. Call list_comms first if you do not know the channel slug.
fetchRetrieve the full text of one BlackOps note or blog post by its search id.
fire_commFire a Comm now — dispatch every undispatched drop in the channel through its linked Sortie and mark them sent. Use for a manual channel, or to flush a scheduled/on_push channel early.
fire_sortieFire a Sortie — a webhook + bearer-token pair registered in BlackOps (Settings → Sorties). The token stays encrypted server-side and never enters this conversation. Use this to trigger Claude Code Routines (e.g. a bug-fixer or PR-closer routine) or any HTTP webhook (Zapier, n8n, GitHub repo-dispatch, custom server). Returns the upstream response, surfacing claude_code_session_url when the fired routine provides one. Call list_sorties first if you do not know the exact name.
get_account_contextGet account context including defaults and publishing context for a domain.
get_account_settingsGet account-level settings: the default intake site used when notes are captured without a site (default_intake_site_id + resolved site). Account settings apply across ALL the user's sites — no domain/site_id needed.
get_ai_contextGet AI context pack for content generation. Recommended before creating tweets/posts.
get_analytics_memoryGet the full per-site analytics memory for a surface (ga4 or gsc): the latest reading and delta-since-last-look for each tool, the open recommendation ledger (with how many times each was surfaced and its status), and recent findings. A compact version of this is auto-injected into every GA4/GSC tool response; call this when you want the full history, e.g. before deciding what to recommend. Read-only.
get_answered_questionsCollect answers that came back from people you asked while you were not running — questions created with a `brain_id`, now answered, and not yet picked up by any agent. Call this at the START of a run when you have outstanding questions, or whenever resuming work on a brain: an answer can arrive hours after you asked, and nothing pushes it to you unless the site owner has separately wired a Sortie. Each result carries the question, the options offered, what was chosen, and the verified email that chose it. The answer is ALREADY logged to its brain — you are collecting it to act on, not to record. Reading does NOT clear the queue; call acknowledge_answered_questions once you have acted, so the same answer is still here if this run dies first.
get_asset_linkMint another signed share link for a private hosted asset, optionally one that expires. Existing links keep working — tokens are stateless, so revoke_asset is the only way to withdraw access.
get_brand_voiceGet canonical brand voice for the site. Pass domain or site_id to target a specific site; omit to use the auth default. Always pass the same domain/site_id used with patch_brand_voice so both tools operate on the same record.
get_compositionsList a site's HyperFrames compositions (live animations played by <hyperframes-player>). Returns slug + name + description. Each is served at /api/compositions/<slug>. Call before creating one to avoid duplicating, and to find the id to patch. Requires domain or site_id. If the user did not specify a site, call get_me to list their sites — use it automatically if only one, otherwise ask the user to choose. Do NOT guess.
get_compositions_by_idGet a single composition by ID, including the full html + mobile_html. Read this before patching so you tweak the real current markup.
get_email_domainGet the site's custom email sending domain state: the domain, its verification status (pending | verified | failed), the DNS records (SPF/DKIM) the customer must create, and whether the site's tier includes the feature (Pro+). While pending, this call re-checks Resend, so it doubles as a status poll after the user updates DNS. The newsletter flow (issue sends, test sends, signup verification) sends ONLY from a verified domain with newsletter_settings.from_email set on it — there is no platform fallback, so an unverified/unconfigured site cannot send newsletters or accept signups. Check this when a user reports newsletter sends failing. Requires domain or site_id. If the user did not specify a site, call get_me to list their sites — use it automatically if only one, otherwise ask the user to choose. Do NOT guess.
get_huntGet a single saved Hunt by id, including its keywords, author filters, intent, and bound brain/post context.
get_meGet current user profile, accessible sites, and account info. Call this first to discover which sites you can manage.
get_newsletter_statsAggregate newsletter list health for a site: active / pending / unsubscribed counts, total, and new signups in the last 30 days. This is deliberately the ONLY subscriber surface in MCP — individual email addresses are never returned or accepted through chat (privacy by design). Programmatic subscriber adds are server-to-server via POST /api/v2/subscribers with an API key, and list management lives in the admin UI. Requires domain or site_id. If the user did not specify a site, call get_me to list their sites — use it automatically if only one, otherwise ask the user to choose. Do NOT guess.
get_noteGet a single note by UUID, slug, or title. Use this to read the full content of individual notes, including brain source notes referenced in a brain manifest. VAULT SYNC STATUS: the response carries a sync block — status is "queued" (commit in flight), "synced" (landed; see file_path + commit_sha), "failed" (see last_error; the sweeper retries), or "none" (no vault target). This is how you confirm a deferred post_notes commit landed; post_notes itself can only report "queued".
get_note_taxonomyGet the active site's note taxonomy: tag vocabulary, folder routing rules, and default folder. Call this BEFORE post_notes / patch_notes so you can pick known tags (post_notes will return a warning for unknown ones) and predict which folder a note's tags + note_type will route the synced file into. Requires domain or site_id. If the user did not specify a site, call get_me to list their sites — use it automatically if only one, otherwise ask the user to choose. Do NOT guess.
get_questionRead the current state of a question created with post_question — its `status` ("pending" | "answered" | "expired"), and once answered, `answer`, `answered_by` (a verified email), `answered_via` ("authenticated_session" | "magic_link"), and `answered_at`. There is no push notification for an answer landing: call this at the start of your next run to check.
get_recent_notesGet recent notes/context for the site. Recommended before creating tweets/posts.
get_reply_queueRead the day's staged reply queue from the reply cadence engine: the ranked, pre-drafted X replies the daily queue-builder staged for the user, in work order, plus progress toward the daily goal, streak state, and the cadence config (queue time, timezone, daily target). Read-only — replies are sent by the user on x.com via the Chrome extension, never by the API. Pass date (YYYY-MM-DD, local to the cadence timezone) to inspect a past day. Requires domain or site_id. If the user did not specify a site, call get_me to list their sites — use it automatically if only one, otherwise ask the user to choose. Do NOT guess.
get_reply_targetsGet RANKED X reply targets, not a raw search dump: each carries a 0-100 value_score from reach, velocity, freshness and author-fit, with bot and auto-summary authors, pure-link and dead zero-engagement posts excluded and already-replied tweets deduped. Read-only — does not post. Pass a hunt_id (uses its saved keywords plus author filters) or a query/keywords directly. Requires domain or site_id. If the user did not specify a site, call get_me to list their sites — use it automatically if only one, otherwise ask the user to choose. Do NOT guess.
get_repo_targetsList configured repository targets for note file writes. Returns available paths where notes can be saved.
get_review_feedbackRead the feedback from a review session. Returns `status` plus, once submitted, a positional `diff` against the draft (only the blocks the reviewer actually changed), every anchored `comment`, any explicit element `deletions`, and `title`/`subtitle` as {before, after} when the reviewer rewrote the headline (null when they left it alone — do not write a field back that did not change). Status is "pending" while the reviewer is still working, "submitted" once they hit Send, and "expired" if they abandoned the link. Pass wait_seconds to long-poll for up to 45 seconds per call; on "pending" call again rather than assuming the review is done. IMPORTANT: inline edits are saved to the draft AS THE REVIEWER MAKES THEM, so `diff` is a record of what they changed, not work for you to do — `diffAlreadyApplied` is true and re-applying it would double-apply their edit. Read the diff to understand what changed, then act on the comments (which are the actionable part) and honour the deletions, then re-run the proofreader before publishing. Never publish a draft whose review session is still pending.
get_skillGet one skill compiled for a host: format "claude" (SKILL.md), "cursor" (.mdc rule), "agents-md" (AGENTS.md section), "grok" (plain prompt), or "raw" (stored markdown). Tool bindings are resolved for the target platform, so the returned procedure names the tools this host actually has. Follow the returned body as written. Check `warnings` — an unbound capability means a step has no tool behind it here.
get_tiktokList TikTok posts for a site. Filter by status (draft, scheduled, queued, posting, processing, published, failed). `processing` means TikTok has the video and is still transcoding — it is not stuck and must not be republished.
get_tiktok_accountsList connected TikTok accounts for a site. PREVIEW: TikTok is limited to platform admins right now — other accounts get a 403 (feature_locked).
get_tiktok_by_idGet a single TikTok post by ID. Use this to pick up the final tiktok_post_url after a publish returned status=processing.
get_tiktok_creator_infoLive TikTok creator settings: which privacy levels this account may use, whether comment/duet/stitch are locked off account-wide, and the max video duration. Call this BEFORE promising the user a public post — if `can_post_publicly` is false, the app has not been audited by TikTok and every direct post will come out private (SELF_ONLY) regardless of what is requested. Also doubles as a connection health check.
get_transcription_jobCheck a long-video transcription job created by transcribe_video. Returns { job_id, status, segments_total, segments_completed, segments_failed, progress_percent, duration_seconds, transcript_preview, transcript_chars, transcript_truncated, transcript_text, summary, key_insights, error }. Status is queued, processing, summarizing, completed, or failed. THE FULL TRANSCRIPT IS NOT RETURNED BY DEFAULT: a long video can run to hundreds of thousands of characters and will not fit in context. A completed job gives you the summary, the key insights, a short preview, and transcript_chars for the real length, with transcript_truncated set to true. Pass include_transcript=true only when you actually need the whole text, and expect it to be very large. Long videos are transcribed a segment at a time across background runs, so a job typically takes a few minutes per ten minutes of video: report the progress and check again later rather than polling in a loop. If a job completes with some segments failed, segments_failed is non-zero and error names how many are missing, so say so rather than presenting the transcript as complete.
get_twitter_activityRead the site's X activity log — tweets and replies the Chrome extension scraped off the owner's own profile (source="scanned"), including replies captured from /with_replies with their parent-tweet context. This is the ONLY record of X activity sent by hand rather than published through BlackOps, so it is the corpus to read for "what has he actually been posting/replying". Distinct from get_post_metrics, which reads the snapshot series and therefore only covers tweets the metrics collector pulled from the X API. Returns per row: tweet id and url, text, authored_at, source, latest engagement (likes/retweets/replies/views/quotes/bookmarks) with engagement_last_updated, voice_analyzed, and whether an embedding exists — never the vector itself. Sorted newest first, cursor-paginated. Caveat on authored_at: it is when BlackOps first recorded the tweet, not X's authoring time — the extension sends no timestamp, and imported rows fall back to the collector run time — so treat it as recency, not as a publication timestamp.
list_accountsList available BlackOps domains/accounts.
list_assetsList the hosted assets published for a site, with their URLs, visibility, size, and revision. Pass include_revoked to also see assets whose links have been taken down.
list_big_boardRender a Big Board: notes grouped into Phase columns. A Big Board is a saved tag query plus a Phase definition, not a separate table — the Ops are ordinary notes, so anything here is also reachable via get_note/patch_notes. Call with no board to list the available boards and their Phase vocabularies (bugs, features, content-seeds). Pass board (slug) to get its columns in order, each with total and ops sorted by position then updated_at. Account-scoped by default; pass domain/site_id only to narrow. An Op whose phase matches no column — including a note never backfilled, phase null — is returned in a LEADING "Unsorted" column rather than dropped, so nothing silently disappears from a queue. That column is present only when it has Ops in it; a fully classified board omits it. limit_per_phase (default 50, max 200) caps each column separately, so a busy column cannot starve the others.
list_commsList the Comm intake channels for your site — their slugs, linked sortie, fire mode (on_push/scheduled/manual), schedule, and how many drops are pending dispatch. Use before drop_comms or fire_comm to discover channel slugs.
list_huntsList saved Hunts for a site. Optional status filter (active | paused | archived). Requires domain or site_id. If the user did not specify a site, call get_me to list their sites — use it automatically if only one, otherwise ask the user to choose. Do NOT guess.
list_notesQuery notes with filters and pagination. Mirrors list_reservoir_items. ACCOUNT-SCOPED BY DEFAULT: omit domain/site_id to query across ALL the user's sites at once — notes are account-level intake, and this is the correct default (a site-scoped query silently hides every note filed under the user's other sites). Pass domain or site_id only to narrow to one site (affinity filter). Filters: tags (with tags_match=all|any, default all), note_type, status (default active), phase (a Big Board workflow-state column, free text), search (title + excerpt ILIKE). Pagination: limit (default 20, max 200), offset. Sort: created_at | updated_at | title | position (default updated_at desc). RECOVERY VIEW: pass unassigned_only: true to list notes with brain_id null (never picked up by any brain compile) from a configurable window (days, default 30) — composes with tags to narrow by topic. In recovery mode status is NOT defaulted to active: drafts and archived notes are included too, since a note that misrouted while still a draft is exactly what this view exists to surface. Pass status explicitly to narrow. Reassign a result to a brain via patch_notes with brain_id. For a quick "most recent" lookup keep using get_recent_notes; use list_notes when you need filtered or paginated queries.
list_skillsList the user's stored skills (slug + description). Call this at the START of a task: if a skill matches what the user asked for, fetch it with get_skill and follow it instead of improvising.
list_sortiesList the Sorties registered for your site — the named webhook/routine targets you can fire with fire_sortie. Returns names, fire URLs, status, last-fired time, and (when armed) the native schedule config plus next_run_at. Never returns the stored bearer token.
log_analytics_findingRecord what an analytics session found so the next session builds on it instead of repeating the same advice. Writes a structured finding, upserts recommendations into the per-site dedup ledger (identical advice is fingerprinted — re-passing one just bumps its counter, so check the injected open_recommendations first), applies any status updates you pass, and creates a dated, tagged note in BlackOps (tags: analytics-log, analytics:<domain>, surface:<ga4|gsc>). Call this at the END of an analytics conversation. WRITE TOOL. Requires Pro tier.
mark_reference_imageMark an existing media-library image as a hero reference (promote it into the site reference pool so the hero generator can use it). Any image in the library can be marked — pass asset_id (from upload_media or list_media) and a category. Use category "headshot" for a photo of a person so the generated hero face is anchored to it. Returns the reference id; pass it as a reference_image_id to post_posts_by_id_hero. Safe to call multiple times on the same asset — any category, title, or description you supply on a repeat call updates the existing reference in place (already_existed: true in the response).
patch_account_settingsUpdate account-level settings. Set default_intake_site_id (UUID, or null to clear) or default_intake_domain to choose which site receives notes captured without an explicit site — this makes post_notes work with no site argument for multi-site users. Intake is account-scoped; this setting only decides where the vault-synced file lands.
patch_brand_voiceUpdate brand voice rules conversationally. Use add, remove, or update to change individual items in tone, style_rules, content_bias, or cta_preferences. Never replaces the full record — only modifies the items you specify. Requires scope: 'global' writes to the account's voice and reaches every site the user owns, 'site' writes to the named domain only; there is no default, so decide from what the rule is about (how the operator writes vs. what this domain says) and say which you chose. Before calling, fetch get_brand_voice to show the user the current state — each returned item carries provenance ('global' or 'site') so you can see where it already lives — and confirm the intended changes. Requires domain or site_id. If the user did not specify a site, call get_me to list their sites — use it automatically if only one, otherwise ask the user to choose. Do NOT guess.
patch_commUpdate an existing Comm in place — change its fire policy (manual/scheduled/on_push), schedule time, linked Sortie, name/slug/description, or enable/disable it — WITHOUT recreating the channel or losing its accumulated drops. Use this to bind a channel to a Sortie ("link my journal channel to the daily-note sortie"), put it on a daily schedule ("fire my journal at 23:00"), pause it, or rename it. Pass only the fields you want to change. Call list_comms / list_sorties to discover slugs and sortie ids.
patch_compositions_by_idUpdate a composition (name, description, html, mobile_html, is_enabled). Applies immediately — NO deploy — to every component/post embedding it. This is the nimble path for "make it slower / hold longer / change the color": edit the html and patch. Slug rename is not supported; create a new composition for a new slug.
patch_huntUpdate a saved Hunt in place by id. Tune any field without recreating the Hunt: name, target_brief, reply_intent, keywords, author_allow, author_deny, brain_ids, post_ids, pitch_policy, score_weights, status, expires_at. score_weights tunes the queue builder's per-hunt composite scoring ({relevance, slop, author_fit} relative weights + {threshold, slop_veto} 0-100 cutoffs); pass null to reset to defaults. pitch_policy sets product-mention policy per hunt: never | natural (mention only within the ~1-in-20 link budget) | always-mention (name the product once in every reply; links stay budget-capped). pitch_product is the product name always-mention enforces; without it, mention enforcement is prompt-level only via reply_intent. Binding post_ids makes those posts anchor-fact sources for drafts, with a budget-capped link back to the anchoring post. Only the fields you pass are changed. Set status to "paused" to stop the Hunt matching new tweets and drafting replies without losing it, "archived" to retire it off active lists while keeping its tweets_matched/replies_drafted history, or "active" to resume. After editing keywords or author filters, call get_reply_targets with this hunt id to see the new set reflected immediately. Requires domain or site_id. If the user did not specify a site, call get_me to list their sites — use it automatically if only one, otherwise ask the user to choose. Do NOT guess.
patch_notesUpdate an existing BlackOps note. Supports partial updates: title, content_markdown, status, note_type, tags, excerpt, brain_id, phase, priority (High|Medium|Low), position. Requires domain or site_id. If the user did not specify a site, call get_me to list their sites — use it automatically if only one, otherwise ask the user to choose. Do NOT guess. Required: id (UUID, slug, or title). BRAIN REASSIGNMENT: pass brain_id to move this note into a brain's compile (moves the vault file to the brain's source_folder_path, deletes the stale file — including when the new brain lives on a different vault target — and recompiles both the old and new brain) — the primary use is fixing notes surfaced by list_notes with unassigned_only: true or by a post_notes UNASSIGNED_BRAIN_MATCH warning. Pass brain_id: null to detach the note from its current brain. TAGS: a tag outside the workspace vocabulary is still APPLIED to the note and stays queryable; the response carries an UNKNOWN_TAG warning naming it. Treat that warning as a prompt to ask the user whether the tag should exist, and call propose_note_tags only if they say yes — never propose on your own initiative, and never silently swap in a did-you-mean suggestion. OPT-IN REDACTION: if the workspace has enabled "Redact email addresses on vault sync" (Knowledge Settings), email addresses are redacted from the vault-synced file before commit (allowlisted addresses pass through) and the response carries a redaction block + warning; the BlackOps record always keeps the original content. With the setting off (default), content syncs exactly as written.
patch_op_phaseMove one Op to a Phase column on a Big Board, and optionally position it within that column. A single-row update: unlike patch_notes it does NOT run the vault sync inline, so it is the right call for reordering and the wrong one for editing content. The body's **Status:** line still ends up correct — it is rendered from the phase column on the note's next content sync. Pass phase (or null, or "Unsorted", to clear it). For ordering, pass before/after — the position rank strings of the neighbouring cards as the column should read after the move — and the server computes a rank between them; pass position directly only if you are supplying your own rank. Pass board to have the phase snapped to that board's canonical casing. A phase outside the board's columns still saves and comes back with a PHASE_NOT_IN_BOARD warning; the Op renders under "Unsorted" until corrected.
patch_sortieUpdate a Sortie — rename it, rotate its webhook/token, change its rate limit, enable/disable it, or arm/rearm its native schedule. Only the fields you pass are touched: e.g. patching just schedule_enabled: false pauses a schedule without losing its cadence config, and patching schedule_enabled: true resumes it. Call list_sorties first if you do not know the exact name or id.
patch_tiktok_by_idUpdate a TikTok draft (caption, privacy, interaction toggles, disclosure flags). Drafts only — TikTok has no edit API, so a live video's caption can only be changed in the TikTok app.
propose_note_tagsPropose one or more note tags for a workspace. Use this when post_notes returns an "Unknown tag" warning and the tag is one that SHOULD exist. This does NOT create the tag: it records a proposal and emails the workspace owner an approval link, and the tag stays unusable until they approve it. You cannot approve your own proposal. Safe to call repeatedly — a tag that already exists, is already pending, or was previously rejected is reported as such and creates no duplicate.
publish_assetPublish a self-contained HTML page, deck, or report and get back a live URL in the response. No build step, no repo, no deploy. Single file: pass html. Multi-file bundle: pass files[] with relative paths (index.html plus its CSS, JS, images, fonts). Relative paths inside the bundle resolve against the asset URL, so an artifact or slide deck works unmodified. visibility "public" serves to anyone with the link. visibility "private" returns share_url — a signed link that needs no login; the plain URL 404s without it, and 404 rather than 403 so the URL never confirms the asset exists. The asset serves from the SITE's own domain (or its blackopscenter.com subdomain), so it is scoped to one site. Inline <style> and <script> are kept exactly as written; every asset is served sandboxed, which means it cannot use localStorage or reach the parent site. Pass source_id to make republishing idempotent: the same value updates the asset in place at the same URL and bumps its revision, instead of creating a second one. Assets never expire — revoke_asset is what takes a link down. A social preview card is generated automatically, so the link previews with an image on X, LinkedIn, Slack and iMessage instead of a bare URL. og_prompt, og_title and og_subtitle steer it; include_title_text: false makes it a purely visual card; og: false skips it entirely. Private assets never get one. The response returns immediately with a fast template card already live at og_image_url — a content-aware version replaces it at the same URL moments later.
publish_x_articlePublish a previously drafted X Article (created with draft_x_article) so it goes live on X. X generates an announcement post for the Article; its post id and URL are returned and stored on the blog post record. This is the explicit human-decision step — nothing in BlackOps ever auto-publishes an Article. Requires confirm=true. Idempotent: an already-published Article returns the existing post id.
reindex_vaultRun an incremental whole-vault index pass for a vault target that has whole-vault indexing enabled. Walks the entire connected repo (minus the target's excluded folders) and refreshes the searchable note index so out-of-path notes become discoverable via search_notes. No-op for path-scoped targets (returns status: "disabled"). Indexing is incremental — unchanged files are skipped. Requires domain or site_id. If the user did not specify a site, call get_me to list their sites — use it automatically if only one, otherwise ask the user to choose. Do NOT guess. Required: id (vault target id from get_repo_targets).
revoke_assetTake a hosted asset offline. Its files are deleted and the URL returns 404 on the next request, including for anyone holding a private share link. This cannot be undone.
save_conversationSave the current AI conversation to BlackOps as a structured note. Renders mobile-readable markdown with role labels and turn separators. Auto-tags "conversation" and "emulator-source" so the note is loadable by `create_conversation_video` (the next step if the user wants to turn this conversation into a video and post it). IMPORTANT: do NOT pass paraphrased messages from your own context — extract the verbatim transcript first by running `bun scripts/extract-claude-session.ts --since-text "<first user message>"` from the project root and passing its output here.
searchSearch BlackOps notes and blog posts. Returns id, title, and url per match; pass an id to fetch for the full text.
share_noteShare a single note with an external recipient via email. The recipient receives the note's content and can reply to a unique reply-to address; their reply is appended to the note directly, as a new dated entry under a "## Replies" section — never a full rewrite of the note, and never a pending-edit proposal like share_brain uses. The recipient does not need a BlackOps account. Use this when the user says "share this note with X" or when composing a workflow (like a daily brief) that needs a reply-able note. Call get_note or list_notes first if you do not already have the note_id.
unshare_noteRevoke an active note share. Future replies to the share address are dropped instead of being appended to the note. Pass either share_id (preferred — returned from share_note) or recipient_email (revokes all active shares of this note to that recipient). Replies that already landed in the note's "## Replies" section are left in place — revoking stops new ones, it does not erase history.
Posts, Pages & Components (35)
attach_media_to_postAttach a media asset to a blog post. Placement options: hero (featured image), og_image (social sharing image), hero_video (video header), inline (append markdown embed to content).
delete_components_by_idDelete a component by ID. Pages that reference the deleted slug will render their fallback content (or an HTML comment if there was no fallback). Requires confirm: true.
delete_pages_by_idDelete a page by ID. Permanently removes the page from the site. Requires confirm: true.
delete_posts_by_idDelete a blog post by ID. Permanently removes the post from the site.
delete_verified_links_by_idRemove a URL from a site's verified-links allowlist. Requires confirm: true. Pages or components that reference the deleted URL will fail validation on the next write.
get_componentsList reusable components available for a site. ALWAYS call this BEFORE drafting a page's html_body — if a component already exists for something the page needs (pricing table, CTA, testimonials, etc.), reference it via <bo-component slug="..."></bo-component> instead of redrawing it. Components are inlined at render time, so updating a component automatically updates every page that references it. Returns slug + name + description for each component. Requires domain or site_id. If the user did not specify a site, call get_me to list their sites — use it automatically if only one, otherwise ask the user to choose. Do NOT guess.
get_components_by_idGet a single component by ID, including the full html_body and custom_css. Use this to read the current implementation before patching.
get_pagesList custom landing pages and section/markdown pages for a site. Filter by status (draft|published) and page_structure. Requires domain or site_id. If the user did not specify a site, call get_me to list their sites — use it automatically if only one, otherwise ask the user to choose. Do NOT guess.
get_pages_by_idGet a single page by ID, including the full html_body, sections, chrome flags, and SEO metadata.
get_postFetch a single blog post by slug or UUID, including full content, metadata, and SEO fields. One call — no need to list posts first to resolve a slug.
get_post_metricsRead per-post X performance collected by the daily metrics cron: an append-only snapshot series per tweet (impressions, profile clicks, link clicks, organic likes/replies/retweets) for the last N days, with the latest reading surfaced. Posts linked to a blog post carry a campaign_slug, and when include_ga4 is true (default) GA4 sessions are joined by utm_campaign onto the same row — so reach posts can be judged on profile clicks/follows and traffic posts on sessions. This is the data source the weekly review reads instead of manual entry. Note: X only serves these metrics for posts under 30 days old; the series is whatever was captured while each post's window was open.
get_postsList blog posts for a site. Filter by status (draft, published) with pagination support.
get_verified_linksList the URLs approved for a site. CALL THIS BEFORE drafting any html_body for pages or components. Matching is on origin + path with tracking params ignored, so you do NOT need a separate entry for each UTM variant — append utm_* freely to any approved URL. An entry may also be approved for a whole path prefix or a whole host, in which case any path under it is fine. A URL that is none of these no longer fails the write: the content saves, the link is queued for human approval, and PUBLISHING is blocked until it clears. Prefer a URL from this list; only reach for a new one when the content genuinely needs it. Requires domain or site_id. If the user did not specify a site, call get_me to list their sites — use it automatically if only one, otherwise ask the user to choose. Do NOT guess.
patch_components_by_idUpdate an existing component. Editable fields: name, description, html_body, custom_css, is_enabled. Updates apply immediately to every published page that references this component via <bo-component slug="...">. Slug rename is intentionally not supported — create a new component if you need a new slug. Same brand-pull rule as post_components: refresh get_brand_design when rewriting html_body.
patch_pages_by_idUpdate an existing page. Editable fields: title, html_body, content, sections, page_structure, is_enabled, show_header, show_footer, full_bleed, show_in_nav, nav_label, nav_order, seo_title, seo_description, seo_image_url, seo_keywords. When rewriting html_body, follow the same rule as post_pages: call get_brand_design + get_brand_voice for the page's site first so the new HTML stays on-brand, call get_components and get_verified_links to reuse existing assets and avoid hallucinated URLs, and let any user-specified directives override the brand defaults for those specific items. Slug rename is intentionally not supported here.
patch_posts_by_idEDIT ONLY — modifies a post in-place. NEVER call for reading; use post_posts to create. Published edits go live immediately. CONCURRENCY: always pass base_updated_at (from get_post.updated_at) — server returns 409 CONFLICT with current_updated_at + current_content on concurrent edit; re-read, merge, retry. Editable: title, subtitle, slug, content, description (PUBLIC meta — not an edit comment), status, tags, seo_title, seo_description, featured_image, featured, published_at, hero_prompt_edited, x_discussion_url. Custom styled HTML needs a richembed fenced block; bare block HTML is escaped. Do NOT include title in content body. Response has links.review_url for drafts — give that link to the user.
patch_verified_links_by_idUpdate a verified link entry. Editable fields: label, description, category, is_enabled. URL itself is immutable — delete and re-add to change the URL.
post_componentsCreate a reusable per-site component (e.g. a pricing table, hero variant, CTA block, testimonials grid) that pages can embed via <bo-component slug="..."></bo-component>. Before drafting html_body, call get_brand_design + get_brand_voice for the same site so the component is brand-accurate, check get_components to avoid duplicating an existing one, and call get_verified_links — every href/src/poster/action URL in the component must be verified. NEVER invent URLs (e.g. "/checkout?plan=pro", "https://stripe.com/buy/abc"). An unverified URL does not fail the write: the component saves with that link queued for human approval and rendered inert, and pages using it cannot publish until it clears. If you need a URL the user hasn't supplied, ask them; only call post_verified_links to add a URL when they've given it to you explicitly and confirmed. Slug must be lowercase kebab-case and unique per site (e.g. "price-table", "trial-cta", "testimonials-grid"). Description should explain when this component is appropriate so future LLM calls know to reuse it. Static HTML, custom CSS, <video>, and declarative CSS/SVG animation render live inline. ANIMATIONS — never hand-write GSAP or custom JavaScript here: in blog POSTS injected <script> does not execute (only declarative CSS and SVG/SMIL animate live). Pick the mechanism by the motion: (1) PREFER a declarative component when the motion is expressible in CSS/SVG — staggered reveals, transitions, ticking checkmarks, progress bars, loops, simple choreography (e.g. "animate this card", "make the files fly into the zip"). It renders live: crisp at any size, theme-matched, tiny, and editable in one place. (2) Render to a VIDEO (create_video → render_video, embed as <video autoplay muted loop playsinline> or via attach_media_to_post) only when the motion needs real JS-level sequencing/physics that CSS can't express, OR must play where components are gated — a free *.blackopscenter.com subdomain, email, or social. The user never names a tool: they describe what they want, you pick component-vs-video. WHERE COMPONENTS RENDER: on the site's own custom domain (and the blackopscenter.com owner site); on a free *.blackopscenter.com customer subdomain they show a placeholder until a custom domain is connected. Requires domain or site_id. If the user did not specify a site, call get_me to list their sites — use it automatically if only one, otherwise ask the user to choose. Do NOT guess.
post_compositionCreate a LIVE HyperFrames animation — a self-contained animated HTML document stored in the DB and served at /api/compositions/<slug> with NO deploy. This is how you ship a live in-page animation (not a video) when the motion exceeds plain CSS: an animated explainer, a UI walkthrough, a looping/animated hero, "animate this card", "make the files fly into the zip". Write a complete HTML doc to the composition contract: a root <div data-composition-id="<id>" data-width="W" data-height="H" data-start="0" data-duration="S"> wrapping the content; a PAUSED GSAP timeline (gsap.timeline({paused:true})) registered at window.__timelines["<id>"]; GSAP loaded from a CDN. To loop with a rest, END the timeline with an empty hold tween (tl.to({}, {duration:5})) and set data-duration to cover the whole timeline. Provide mobile_html (a portrait, larger-text variant) for mobile readability. THEN embed it from a reusable component's html_body (post_components): <hyperframes-player data-src-desktop="/api/compositions/<slug>" data-src-mobile="/api/compositions/<slug>?variant=mobile" data-breakpoint="640" data-aspect-desktop="W / H" data-aspect-mobile="Wm / Hm" muted style="width:100%;max-width:600px;aspect-ratio:W / H;display:block;margin:1.5rem auto"></hyperframes-player>, then reference that component in a post via <bo-component slug="...">. The platform handles autoplay, looping, and the desktop/mobile switch. The user never names a tool: they describe what they want, you build it. Tweaks ("slower", "hold longer", "change the orange") are a one-call patch_compositions_by_id — instantly live, no deploy. Use a VIDEO (create_video → render_video) instead only when you need a downloadable file for social/email. Compositions render where components do: custom-domain sites + the blackopscenter.com owner site. Requires domain or site_id. If the user did not specify a site, call get_me to list their sites — use it automatically if only one, otherwise ask the user to choose. Do NOT guess.
post_email_domainSet up or verify the site's custom email sending domain (Pro+). CREATE: pass sending_domain (e.g. "updates.example.com" — recommend a dedicated subdomain) to register it under the platform Resend account; the response includes the DNS records the user must create at their DNS provider — show them clearly. VERIFY: after the user adds the records, pass action: "verify" to trigger DNS verification (async on Resend's side — poll get_email_domain if still pending). One sending domain per site, and a domain can belong to only one site; a domain already present in the platform Resend account is adopted with its live status. After verification, remind the user to set an on-domain From Email under Newsletter Settings — sending stays disabled until it's set. Removal is intentionally admin-UI-only (Settings → Email Sending Domain). Requires domain or site_id. If the user did not specify a site, call get_me to list their sites — use it automatically if only one, otherwise ask the user to choose. Do NOT guess.
post_notesCreate a BlackOps note, or update one in place when source_id resolves to a known note. Requires content_markdown (or transcript/conversation). source_id is the provenance/idempotency key — pass it so repeat calls update the same note instead of forking. It takes a content hash, an agent-defined string, or a note UUID (updates that note directly, action: "note_updated"). Prefer patch_notes with id for updates to notes you already know. ALWAYS read the response warnings: an auto-suffixed slug ("my-note-2") with code SLUG_COLLISION is the only signal an intended update instead forked. Site is OPTIONAL — resolves explicit site → account default intake site → the user's only site, listing sites if that fails. Pass domain/site_id only when the note belongs to one site. BRAINS: when the user names a brain ("log this in the product brain"), pass that brain's UUID as brain_id — the server derives site, target and source folder from it. Never infer site_id from topic keywords or guess a target_id; ask first if ambiguous. The response's brain_alignment block confirms whether the compile will pick the note up. Opt in with suggest_brain: true to be told when tags/title/note_type match an unassigned brain (returns suggested_brain + UNASSIGNED_BRAIN_MATCH; omitted on ties; never fires on already-assigned notes). Default false skips inference. Fix assignments later with patch_notes, or list via list_notes unassigned_only: true. TAGS: an out-of-vocabulary tag still APPLIES and stays queryable; the response carries an UNKNOWN_TAG warning naming it — ask the user whether it should exist, and call propose_note_tags only if they say yes. VAULT SYNC is automatic but DEFERRED: the commit runs after the response, so sync is { status: "queued" }. The note IS saved — do NOT re-call. Dropped commits are retried by a sweeper; confirm one landed with get_note, never vault_sync_queued_at. Pass wait_for_sync: true only when you need commitSha/filePath inline. Folder routing follows the workspace taxonomy (preview with get_note_taxonomy); override_folder_path bypasses it, though a brain's source_folder_path still wins. Email redaction applies only if enabled in Knowledge Settings; the record always keeps the original. phase/priority/position are Big Board columns (High|Medium|Low for priority); phase is free text. Do NOT use post_notes_write — it skips the BlackOps record.
post_pagesCreate a custom landing page on the customer site at the given slug — root-level ("about") or prefixed ("vs/buffer", "for/agencies", "replace/notion", "alternative/<x>"). This tool's purpose is to publish a brand-accurate landing page for the target site, so before drafting html_body you MUST call get_brand_design AND get_brand_voice for the same site, AND call get_components to discover reusable components already authored for this site, AND call get_verified_links to discover the only URLs allowed in href/src/poster/action attributes. NEVER invent URLs. A URL that is not approved does not fail the write any more: the page saves with that link queued for human approval, renders it inert, and refuses to publish until it clears. Only malformed URLs (relative paths, unparseable hrefs) still fail with a 422. Allowed without verification: in-page anchors (#section), mailto:/tel: protocols, paths that match an existing published site page (use get_pages to check), and asset paths under /images/ /api/ /_next/ /brand/ /saas/ /fonts/. Everything else — every CTA URL, every checkout link, every external link — must come from get_verified_links. If the page needs a URL that isn't verified, ask the user; only call post_verified_links to add a URL when they've given you the URL explicitly and confirmed. Use the returned colors, fonts, gradients, scales, and voice to compose the HTML — the page should look and read like it belongs to that site. CRITICAL token usage: the brand_design response includes BOTH light-surface tokens (text_color, muted_color, border_color, background_color) AND dark-surface counterparts (text_color_dark, muted_color_dark, border_color_dark, background_color_dark). Whenever a section renders on a dark background (dark hero band, dark CTA, dark comparison table, full dark-mode page), use the *_dark tokens for text/muted/border so contrast stays readable. Never use the light muted_color on a dark background — secondary text disappears. REUSABLE COMPONENTS: when get_components returns a component matching what the page needs (pricing table, CTA, testimonials, etc.), embed it instead of redrawing the same block — write <bo-component slug="that-slug"></bo-component> at the spot in html_body where it should appear. Optional fallback content goes between the open and close tags. Components are inlined at render time, so updating a component updates every page that uses it. If no suitable component exists for a piece that will likely be reused (e.g. pricing), consider creating one with post_components first, then referencing it from the page. Anything the user explicitly specifies (a particular video, headline copy, color, layout, comparison points, custom CSS, etc.) overrides the brand defaults for that specific instruction; brand defaults fill in everything the user did not specify. Only call get_brain when the user explicitly asks to use a brain (e.g. "use the BlackOps brain to compare us to Squarespace") — otherwise do not pull brain context. Scripts, <video> with autoplay/loop, animations, and arbitrary CSS are all allowed inside html_body. Defaults to full_bleed: true with header + footer ON. SEO is mostly automatic: canonical URL, JSON-LD WebPage + BreadcrumbList, Open Graph and Twitter cards are emitted from the page record at render time. You should still supply seo_title (under 60 chars), seo_description (under 160 chars), seo_image_url (1200x630 OG image), and seo_keywords (5-12 search phrases) for best results. Pages start as drafts; pass status: "published" + confirm: true to publish in one call, or call post_pages_by_id_publish later. Requires domain or site_id. If the user did not specify a site, call get_me to list their sites — use it automatically if only one, otherwise ask the user to choose. Do NOT guess.
post_pages_by_id_publishPublish a draft page, making it live at its configured slug. Requires confirm: true.
post_postsCreate blog draft. Before calling, fetch get_me + get_brand_voice (+ get_ai_context/get_recent_notes). Requires domain or site_id. If the user did not specify a site, call get_me to list their sites — use it automatically if only one, otherwise ask the user to choose. Do NOT guess. Required fields: title, content. Optional: subtitle (deck/subheadline shown beneath the title — populate this whenever the post has a natural subheadline or clarifying phrase). IMPORTANT: content must be Markdown — do NOT pass a fully HTML-formatted document (e.g. <p>text</p><h2>heading</h2>); the CMS renders markdown directly and block-level HTML tags appear as literal text. Custom styled HTML (cards, tables, callouts with CSS) must use a richembed fenced block — rendered as live HTML. Bare block-level HTML is escaped as literal text. IMPORTANT: do NOT include the title in the content body — no leading "# Title" H1 and no first line repeating the title. The renderer displays the title from the title field above the content; including it in the body produces a duplicate. Start content with the intro paragraph or the first ## section heading. The response carries links.review_url for a draft. That is the link a person previews and edits the draft in, so give it to the user as the link to open. Do NOT hand back the admin editor URL or a raw preview URL instead.
post_posts_by_id_heroGenerate (or regenerate) a hero image for a blog post, or the post's OG social card — works the same before and after the post is created. Default target is "hero". target:"og" makes the Open Graph card (1200x630, sets og_image, leaves the hero alone) through the same prompt pipeline as the admin UI: a content-aware visual prompt folding in the site's image_style_prompt / image_color_palette / image_style_keywords, then social framing with the title as text (include_title_text:false for a purely visual card). The card renders the post's subtitle when it has one (override with subtitle, or include_subtitle:false) — nothing else is allowed on the image, because the generator otherwise invents its own tagline. Optional prompt (for target:"og" it is the VISUAL DIRECTION and still gets OG framing), reference_image_ids (curated reference IDs and/or media-library asset IDs from upload_media / list_media), and style (e.g. "western", "cinematic"). To anchor a face, mark the photo as a "headshot" via mark_reference_image first. With no prompt, one is generated from the post content. A hero with no reference_image_ids reuses the references from its first generation, else auto-matches from the prompt. An OG card takes NO references unless you name them — auto-matching used to put a real face on social cards nobody asked to be in. Returns the image inline plus its public URL. For a review flow, pass commit:false for a CANDIDATE that does not touch the post, then offer accept (accept_asset_id) or regenerate (iteration_prompt + source_image_url).
post_posts_by_id_publishPublish a draft blog post, making it live on the site. Requires confirm: true. Publishing a scheduled post publishes it immediately and clears the schedule.
post_posts_by_id_scheduleSchedule a draft blog post for future publishing. Provide a datetime via scheduledFor; the platform publishes the post automatically at that time (checked every 5 minutes). Unschedule by patching status back to "draft"; publish immediately with post_posts_by_id_publish. Requires confirm: true.
post_questionAsk a human a question when you hit a decision only they can make. This is a generic primitive — any agent or BlackOps customer can use it, not just the feature-builder. Defaults to non-blocking: record your `assumption` and keep going, treating the question as a flag for confirmation rather than a stop; only set `blocking: true` when you are genuinely waiting on the answer. Pass `recipient_email` ONLY when a human in THIS session actually gave you that address — never guess or infer one; if you have none, omit it and the question still queues for later discovery. Set `sensitivity: "high"` for anything where "proved they got the email" is not enough assurance (money, destructive actions, credentials) — those can only be answered by someone with an authenticated BlackOps session for the site, never by the email-verification path. Returns a `view_url` you can hand to a human directly (e.g. in a run-exit email) and an `id` to poll later with get_question. Pass `brain_id` when the question came out of a brain: the answer is then logged to that brain the moment it lands, and it queues for collection with get_answered_questions on your next run — so you never have to hold a session open waiting. Without a brain_id there is no queue and no notification: call get_question again later to see if it was answered.
post_questions_batchAsk a human SEVERAL questions at once — one email, one link, answered in one sitting. Use this instead of calling post_question repeatedly: three separate calls send three emails with three links, and someone doing you a favour answers the first and ignores the rest. Give each question `options` wherever the answer is a pick rather than an essay. Pass `recipient_email` ONLY when a human in THIS session actually gave you that address. Pass `brain_id` when the questions came out of a brain — every answer is logged there as it arrives, and you are woken once the batch is complete so the work can continue. Returns a `view_url` you can also hand over directly, and the `question_ids` to read later with get_question.
post_reservoir_ingestIngest content into a knowledge reservoir. Add URLs, articles, or text as reference material. Pass source to record where the content came from: manual, discovery, rss_feed, content_library, tweet, article, email, youtube, upload, or v2_api. Common words are mapped for you, so media and file become upload and x becomes tweet, which means a transcript from transcribe_video can be forwarded with its own source value unchanged. An unrecognised source is rejected rather than stored. Omit it and the item is recorded as v2_api. Requires domain or site_id. If the user did not specify a site, call get_me to list their sites — use it automatically if only one, otherwise ask the user to choose. Do NOT guess.
post_tiktokCreate a TikTok video post draft. PREVIEW: TikTok publishing is currently limited to platform admins — other accounts get a 403 (feature_locked); say so plainly rather than retrying. Requires domain or site_id. If the user did not specify a site, call get_me to list their sites — use it automatically if only one, otherwise ask the user to choose. Do NOT guess. Required: title (the caption, max 2200 chars including hashtags/mentions). A TikTok post is ALWAYS exactly one video — after this, call attach_video_to_tiktok with an asset_id from upload_media, then publish or schedule. There is no text-only or image TikTok post. PRIVACY: privacy_level defaults to SELF_ONLY (private). Until TikTok audits the app, that is the only level that reliably works — check get_tiktok_creator_info before telling the user a post will be public. POST MODE: post_mode='INBOX' uploads to the TikTok app's drafts for the user to finish by hand (no audit needed, but BlackOps cannot complete the post); the default 'DIRECT_POST' publishes to the profile. DISCLOSURE: set brand_content_toggle for paid partnerships, brand_organic_toggle for promoting the creator's own business, and is_aigc for AI-generated video — misreporting these is a TikTok policy violation. ATTRIBUTION: owned-domain links in the caption are auto-tagged with utm_source=tiktok&utm_medium=social at draft time. LINK TO BLOG POST: pass post_id (UUID, from get_posts) so the /admin/posts Distribution column links them automatically.
post_tiktok_by_id_postPublish a TikTok draft now. Uploads the video and waits up to ~2 minutes for TikTok to transcode. IMPORTANT — read the returned `status` before reporting back to the user: 'published' means live (surface tiktok_post_url); 'processing' means TikTok has the video and is still transcoding, the scheduler will confirm it within ~5 minutes, and calling this tool again would post the video TWICE — poll get_tiktok_by_id instead; 'sent_to_inbox' (post_mode=INBOX) means it is waiting in the TikTok app's drafts for the user to post by hand. Also check `privacy_downgraded_to` and `warnings`: if the app is unaudited, a post requested as public will have been published as SELF_ONLY (private), and the user needs to be told that plainly. Requires confirm: true.
post_tiktok_by_id_scheduleSchedule a TikTok draft for future publishing. Provide a datetime via scheduledFor (also accepts scheduled_for / schedule_at). The video must already be attached — scheduling an empty draft is rejected. The scheduler runs every 5 minutes. Requires confirm: true.
post_to_mediumCross-post a published blog post to Medium with canonical URL, tags, and SEO preserved. The Medium lead image defaults to the post OG image (falling back to the hero) — set featured_image to "hero" or an explicit URL to override. Requires a Medium integration token configured at /admin/settings?section=integrations. Requires confirm=true. Already-cross-posted posts return the existing Medium URL unless force=true.
post_verified_linksPROPOSE a URL for a site's verified-links allowlist. This does NOT approve it — the URL is queued as pending and a human approves it at /admin/verified-links. Use it only when the user has explicitly given you a URL; never invent one. The url field must be an absolute URL (https://...), an absolute path on the site (/foo), or a mailto:/tel: protocol link. Categories: page, cta, asset, social, docs, embed, other. Content referencing a pending URL saves fine but cannot be published until it is approved, so tell the user the link is waiting on them rather than reporting it as added. Requires domain or site_id. If the user did not specify a site, call get_me to list their sites — use it automatically if only one, otherwise ask the user to choose. Do NOT guess.
Social — X, Threads & LinkedIn (38)
attach_media_to_linkedin_postAttach media to a LinkedIn post draft. Reuse asset_id from upload_media.
attach_media_to_threadAttach a media asset (from upload_media) to a Threads draft. Threads requires media URLs to be publicly reachable HTTPS at publish time — the media library URL satisfies this. Container creation is deferred to publish.
attach_media_to_tweetAttach a media asset to a tweet draft. First upload the image with upload_media, then pass the returned asset_id here. tweet_index is 0-based (0 = first/only tweet, 1 = second tweet in a thread, etc.); omit to default to 0. Up to 4 images per tweet — limit enforced by the backend.
delete_linkedin_post_by_idDelete a LinkedIn post by ID. If published, also deletes from LinkedIn. Requires confirm: true.
delete_threads_by_idDelete a Threads draft from BlackOps. NOTE: the Threads API does not expose a delete endpoint — published Threads posts must be removed manually in the Threads app. This tool only removes the BlackOps record.
delete_tweets_by_idDelete a tweet or thread by ID. Removes the draft; does not delete from X if already posted.
detach_media_from_tweetRemove a media asset from a tweet draft. Pass tweet_id and tweet_index (0-based; omit to default to 0). Pass asset_id to remove only that specific image — useful for multi-image tweets (up to 4) where you want to swap one image without losing the others. Omit asset_id to clear ALL media from that tweet_index. Returns the updated media list.
generate_linkedin_carouselRender a branded PDF carousel and return a downloadable URL. NO LinkedIn account or draft post required. Use this when you want to post a carousel by hand (e.g. to a LinkedIn organization/company page the API cannot reach). Provide {topic, sourceMaterial?, slideCount?, theme?, tone?, allowImages?, imageGuidance?} to generate slides, OR {spec} to render a pre-built spec. Returns {public_url} (the PDF to download), asset_id, page_count, and the spec. Differs from post_linkedin_by_id_carousel, which requires a connected LinkedIn account and attaches the PDF to a draft post.
get_linkedin_accountsList connected LinkedIn accounts. Search by name or filter by site.
get_linkedin_post_by_idGet a single LinkedIn post by ID.
get_linkedin_postsList LinkedIn posts for a site. Filter by status (draft, scheduled, published).
get_threadsList Threads posts for a site. Filter by status (draft, scheduled, queued, posting, published, failed).
get_threads_accountsList connected Threads accounts for a site.
get_threads_by_idGet a single Threads post (or chain) by ID.
get_tweetsList tweets/threads for a site. Filter by status (draft, scheduled, posted) and X account.
get_tweets_by_idGet a single tweet or thread by ID, including text, status, and posting details.
get_x_accountsList connected X (Twitter) accounts. Search by username or filter by site. Each account record includes premium (boolean) and max_tweet_length (integer) — ALWAYS read these before drafting tweet content. Premium accounts allow up to 25,000 chars; non-premium are limited to 280. If max_tweet_length is null, treat the limit as unknown and ask the user rather than assuming 280.
patch_linkedin_post_by_idUpdate a LinkedIn post draft. Only drafts can be edited — LinkedIn API does not support editing published posts. To correct a published post, delete it and create a new one.
patch_threads_by_idUpdate a Threads draft. Drafts only — published Threads posts cannot be edited via the Threads API.
patch_tweets_by_idUpdate an existing tweet or thread. Edit text, thread content, or metadata.
post_linkedinCreate LinkedIn post draft. Before calling, fetch get_me + get_brand_voice. Requires domain or site_id. If the user did not specify a site, call get_me to list their sites — use it automatically if only one, otherwise ask the user to choose. Do NOT guess. Required: content (max 3000 chars). For article/link shares (post_type='article'), you MUST provide article_title and article_description since LinkedIn does not auto-scrape URLs for API-created posts. To add a thumbnail, first call upload_media + attach_media_to_linkedin_post. ATTRIBUTION: links to user-owned domains in content (and article_url) are auto-tagged with utm_source=linkedin, utm_medium=social, utm_campaign at draft time. LINK TO BLOG POST: when this post distributes a known blog post, pass its post_id (UUID, from get_posts) so the /admin/posts Distribution column links them automatically — no manual backfill.
post_linkedin_by_id_carouselGenerate and attach a PDF carousel to a LinkedIn post draft. Provide {topic, sourceMaterial?, slideCount?, theme?, tone?, allowImages?, imageGuidance?} to generate a spec, OR {spec} to render a pre-built spec. Renders PDF server-side, uploads to LinkedIn as a document, and attaches to the post. Post stays draft — call post_linkedin_by_id_publish to ship. IMAGES: Set allowImages=true to include images on slides. Each slide can have an image with source "library" (asset_id from media library), "generate" (AI-generated from a prompt), or "url" (external https URL). Use imageGuidance to steer image style. To add/change images after generation, use post_linkedin_by_id_carousel_refine with an instruction like "add an image to slide 2" then re-render by calling this tool with the returned spec.
post_linkedin_by_id_carousel_refineRefine an existing carousel spec with a natural-language instruction (e.g., "swap slide 3 for a stat", "make cover punchier", "add an image to slide 2", "more variety — too many lists"). Supports adding, changing, or removing images on any slide. Returns the updated spec. Does NOT re-render the PDF — call post_linkedin_by_id_carousel with the returned spec to render and attach.
post_linkedin_by_id_publishPublish a LinkedIn post immediately. Requires confirm: true.
post_linkedin_by_id_scheduleSchedule a LinkedIn post for future publishing. Provide a datetime via scheduledFor. Requires confirm: true.
post_linkedin_campaignCreate multi-post LinkedIn campaign with scheduled or draft posts. Requires domain or site_id. If the user did not specify a site, call get_me to list their sites — use it automatically if only one, otherwise ask the user to choose. Do NOT guess. Owned-domain links are auto-tagged with UTM params (linkedin/social, per-item campaign slug). Requires confirm: true. LINK TO BLOG POST: pass a campaign-level post_id (UUID, from get_posts) to link every item to a blog post, or set post_id per item to override — the /admin/posts Distribution column then populates automatically with no manual backfill.
post_threadsCreate a Threads post draft. Requires domain or site_id. If the user did not specify a site, call get_me to list their sites — use it automatically if only one, otherwise ask the user to choose. Do NOT guess. Provide either content (single post) or thread[] (a multi-post chain). Threads enforces a 500-character limit per post and at most 1 hashtag per post — these are validated at draft time. For chains, each element publishes sequentially via reply_to_id; there is no batch endpoint. ATTRIBUTION: links to user-owned domains are auto-tagged with utm_source=threads&utm_medium=social&utm_campaign=<slug> at draft time — this tagging is applied BEFORE the 500-char validation, so the tagged URL length counts toward the limit. Budget ~70 chars of headroom per owned-domain link (keep authored content under ~430 chars when including a link). If validation fails, the error message shows authored length + UTM overhead separately. LINK TO BLOG POST: when this post distributes a known blog post, pass its post_id (UUID, from get_posts) so the /admin/posts Distribution column links them automatically — no manual backfill.
post_threads_by_id_postPublish a Threads draft immediately. For VIDEO posts the call may block for ~30s while Meta processes the container; text and image posts publish quickly. For chains, walks each element sequentially via reply_to_id; on partial failure a retry resumes from the last successful element. On success returns `threads_post_id` and `threads_post_url` (the public permalink) — surface the URL to the user so they can verify the post. Requires confirm: true.
post_threads_by_id_scheduleSchedule a Threads draft for future publishing. Provide a datetime via scheduledFor (also accepts scheduled_for / schedule_at). The scheduler runs every 5 minutes. Requires confirm: true.
post_threads_campaignCreate a multi-post Threads campaign of drafts or scheduled posts. Requires domain or site_id. If the user did not specify a site, call get_me to list their sites — use it automatically if only one, otherwise ask the user to choose. Do NOT guess. Each net-new post in the campaign consumes one slot of the 250-posts/24h profile rate limit (chain children and replies do not). Sequential chain publishing means total wall time scales with chain length. Owned-domain links are auto-tagged with UTM params (threads/social, per-item campaign slug). Requires confirm: true. LINK TO BLOG POST: pass a campaign-level post_id (UUID, from get_posts) to link every item to a blog post, or set post_id per item to override — the /admin/posts Distribution column then populates automatically with no manual backfill.
post_threads_replyCreate a Threads reply draft (replies do NOT count toward the 250-posts/24h profile rate limit — only net-new posts do). Requires domain or site_id. If the user did not specify a site, call get_me to list their sites — use it automatically if only one, otherwise ask the user to choose. Do NOT guess. Required: content, reply_to_id (the Threads post id you're replying to). Owned-domain links in content are auto-tagged with utm_source=threads, utm_medium=social, utm_campaign. Use post_threads_by_id_post to publish.
post_tweetsCreate tweet draft. Before calling, fetch get_me + get_brand_voice (+ get_ai_context/get_recent_notes) AND call get_x_accounts to read the posting account's max_tweet_length — do not assume 280. X Premium / verified accounts have max_tweet_length up to 25,000; never reject or shorten your own draft based on a 280-char assumption. If get_x_accounts returns max_tweet_length: null, treat the limit as unknown and ask the user rather than defaulting to 280. Requires domain or site_id. If the user did not specify a site, call get_me to list their sites — use it automatically if only one, otherwise ask the user to choose. Do NOT guess. Required content: text or thread. To attach an image to the tweet, first upload with upload_media, then call attach_media_to_tweet with the returned asset_id and this tweet's id. ATTRIBUTION: links to user-owned domains will have UTM parameters (utm_source=twitter, utm_medium=social, utm_campaign=...) automatically appended at draft time so traffic shows up in GA4 instead of as Direct — you can also include them yourself if you prefer explicit campaign slugs. LINK TO BLOG POST: when this tweet distributes a known blog post, pass its post_id (UUID, from get_posts) so the /admin/posts Distribution column links the social post to the blog post automatically — no manual backfill.
post_tweets_by_id_postPublish a tweet or thread to X (Twitter) immediately. Requires confirm: true.
post_tweets_by_id_scheduleSchedule a tweet or thread for future posting on X. Provide a datetime via scheduledFor. If you are about to edit the tweet text as part of scheduling, call get_x_accounts first and respect the account's max_tweet_length — Premium accounts allow up to 25,000 chars per tweet, so do not shorten content based on a 280-char assumption.
post_tweets_campaignCreate a multi-tweet campaign with scheduled or draft tweets. Before drafting items, call get_x_accounts to read the posting account's max_tweet_length — do not default to 280. X Premium accounts allow up to 25,000 chars per tweet; size each item's text/thread entries against the account's actual limit, not an assumed one. Each item can be a single tweet ({ text }) or a thread ({ thread: [{ text }] }), with an optional scheduled_for datetime. Requires domain or site_id. If the user did not specify a site, call get_me to list their sites — use it automatically if only one, otherwise ask the user to choose. Do NOT guess. Owned-domain links are auto-tagged with UTM params (utm_source=twitter, utm_medium=social, utm_campaign per item) at draft time. LINK TO BLOG POST: pass a campaign-level post_id (UUID, from get_posts) to link every item to a blog post, or set post_id per item to override — the /admin/posts Distribution column then populates automatically with no manual backfill.
post_tweets_quoteQuote tweet an existing tweet on X. Provide tweet_id or tweet_url and your commentary text — the original tweet URL is appended so X renders a quoted-tweet card (works across all X API tiers). Owned-domain links in commentary are auto-tagged with utm_source=twitter, utm_medium=social, utm_campaign. Requires confirm: true.
post_tweets_replyReply to an existing tweet on X. Provide tweet_id or tweet_url and reply text. Owned-domain links in the reply text are auto-tagged with utm_source=twitter, utm_medium=social, utm_campaign. Requires confirm: true.
post_tweets_searchSearch recent tweets on X by keyword query. Returns matching public tweets.
Brains & Knowledge (7)
create_brainCreate a new brain (knowledge base) for a site. A brain compiles its source notes into a single navigable knowledge map. Two storage modes: (a) DB-backed — omit both vault fields and the brain's source notes live in BlackOps directly, no GitHub connection needed (the default for most users); (b) vault-backed — pass vault_target_id or vault_target_name (find names via get_repo_targets) to compile a folder of markdown files from a connected GitHub vault; source_folder_path is optional and auto-generates as "brains/{slug}". If the user has access to multiple sites, pass domain or site_id — for single-site users this is optional. After creation, call update_brain to add the founding note; compilation is triggered automatically (directly for DB-backed brains, via the GitHub webhook for vault-backed). Call get_brain afterwards to check compilation status.
get_brainGet the compiled knowledge map for a brain: a manifest of source notes with IDs and summaries — call get_note for a note's full content. Site-agnostic. Accepts a UUID, slug, name, or alias; no list_brains lookup needed first (call list_brains for a brain not named in your instructions). status: "pending" means it has never compiled — call update_brain to add the first note. AMBIGUITY: when a reference matches more than one brain the response is error "ambiguous_brain_ref" with a candidate list and NO brain. Present the candidates to the user and ask which they mean. Do not infer from context, and do not re-call this tool until they answer.
list_brainsList all brains accessible to the authenticated user. Returns each brain's id, name, description, aliases, status, note count, last compiled timestamp, and visibility. Match description and aliases to resolve natural-language references (e.g. "restaurant brain"). Brains are site-agnostic — no domain or site_id needed. Cross-brain retrieval: returns every brain in the account, so an answer about one can draw on another — say so when it does. Private brains are omitted here but stay readable via get_brain by name.
patch_brainUpdate a brain's metadata — name, description, aliases, or visibility — without adding new notes. Use this to make a brain discoverable by natural-language reference: set description to a plain-English summary of what the brain covers, and set aliases to the phrases a user might say (e.g. ["restaurant brain", "wine brain", "dining"]). Call list_brains first to get the brain_id. Set visibility "private" when the user asks to keep a brain out of cross-brain retrieval ("don't let it read my client brain", "keep that one separate"). A private brain stops appearing in list_brains but stays readable when named explicitly. Never set it on your own initiative — it changes what future sessions can find.
share_brainShare a brain with an external recipient via email. The recipient receives a clean summary of the brain and can reply to a unique reply-to address; their reply is parsed, attributed, and written back into the brain as an external-input note. The recipient does not need a BlackOps account. Use this when the user says "share this brain with X", "invite X to contribute", or "send a summary of the brain to X". Call list_brains first if you do not already have the brain_id. PII: if the workspace has enabled "Redact email addresses on vault sync" (Knowledge Settings), the recipient's address is redacted from everything the reply flow commits to the vault (reply notes, filenames, commit messages) — the recipient renders as their name plus a stable #tag. With the setting off (default), replies persist as received.
unshare_brainRevoke an active brain share. Future replies to the share address are dropped and trigger an auto-reply telling the sender the address is no longer recognized. Pass either share_id (preferred — returned from share_brain) or recipient_email (revokes all active shares of this brain to that recipient). The audit trail of prior inbound replies is preserved.
update_brainAdd a new note to a brain — no target_id or file path knowledge required. For DB-backed brains (no vault target) the note is stored in BlackOps directly and a recompile is dispatched immediately; for vault-backed brains the server resolves the vault target and source folder from the brain record and commits the file to GitHub. Use this whenever the user says "update the brain", "add this to the brain", or "log this in the brain". Call list_brains first to get the brain_id if you don't already have it. After writing, the note will be picked up on the next brain compile. Use post_notes_write (with brain_id) only when you need lower-level control over the filename or relative path. OPT-IN REDACTION (vault-backed only): if the workspace has enabled "Redact email addresses on vault sync" (Knowledge Settings), email addresses are redacted from the committed file (allowlisted addresses pass through). With the setting off (default), content commits exactly as written.
Capture & Import (7)
extract_brand_designRead a site or profile URL the user owns and propose visual brand design values. READ-ONLY — nothing is applied. Owned websites get real extraction of DECLARED styles (accent/background/text color, fonts, logo candidates, each with provenance); social profile URLs (LinkedIn, X, …) carry the platform's styles, so instead of extracting, the response includes curated palette and type-pairing shortlists to offer the user — never sample colors from a profile banner and never infer typography from a profile. Fields listed in `missing` were not declared by the page; offer the shortlists for exactly those fields. Flow: call this, show the user the proposal (suggested_brand_design + suggested_logo_url or shortlist options), get their confirmation or picks, then apply with patch_brand_design (confirm: true).
get_brand_designGet visual brand design settings for a site: accent color, background color, text color, font family, logo URL, favicon URL, and default voiceover voice ID. Use this to understand the site's visual identity when creating videos, carousels, or other branded assets.
get_capture_sessionGet a capture session by ID. Returns full session including meeting transcript, reactions transcript, fusion output, and the generated Note (if fusion is complete).
ingest_captureTurn a meeting transcript you already have into a structured Brain note — summary, decisions, action items split into yours vs other people's, open questions, and a ready-to-send follow-up email. Accepts any common transcript format (WebVTT, SRT, Zoom, Teams, Granola, Otter, Fathom, or plain pasted text), or the output of transcribe_video. No recorder or desktop app required. Pass `reactions` with your own side commentary and it gets fused against the meeting timeline, so the note reflects your read of the room and not just what was said. Pass `sanitize: true` to strip surnames, contact details, credentials, and financial identifiers BEFORE anything is stored. Returns immediately — poll get_capture_session for the finished note.
list_capture_sessionsList recorded meeting/capture sessions for a site. Returns session id, meeting metadata, fusion status, generated note id, and progress. Filter by status: pending | running | completed | failed.
patch_brand_designUpdate visual brand design settings conversationally: accent color, background color, text color, font family, logo URL, favicon URL, or voice IDs. Only the fields you provide are changed — all others remain unchanged. Before calling, show the user the intended changes and get confirm: true. Changes take effect immediately on the live site.
refuse_capture_sessionRe-run fusion on a capture session, optionally with a custom prompt to get a different output shape (e.g. only action items, or a status report). Returns immediately — poll get_capture_session to check progress.
Reservoirs (6)
delete_reservoir_itemPermanently delete a reservoir item. Requires confirm:true. If the item has been referenced in generated content (times_referenced > 0), the call returns 409 — pass force:true to override. Prefer patch_reservoir_item with status:'archived' for non-destructive removal. Requires domain or site_id. If the user did not specify a site, call get_me to list their sites — use it automatically if only one, otherwise ask the user to choose. Do NOT guess.
get_reservoirGet a single reservoir by id with full metadata, monitored keywords, and (by default) computed analytics: health score, content breakdown by source/type, recent activity, lineage. Pass include_analytics:false for a lighter response. Requires domain or site_id. If the user did not specify a site, call get_me to list their sites — use it automatically if only one, otherwise ask the user to choose. Do NOT guess.
get_reservoir_itemGet a single reservoir item with full content (including transcript if present). Requires domain or site_id. If the user did not specify a site, call get_me to list their sites — use it automatically if only one, otherwise ask the user to choose. Do NOT guess.
list_reservoir_itemsList items in a reservoir with filter/search/pagination. Default status: active. Valid sort values: added_at (default), updated_at, last_referenced_at, times_referenced, relevance_score, ai_reservoir_score, title. Note: created_at is aliased to added_at for back-compat. Requires domain or site_id. If the user did not specify a site, call get_me to list their sites — use it automatically if only one, otherwise ask the user to choose. Do NOT guess.
list_reservoirsList all knowledge reservoirs for a site with item counts, monitored keywords, and last-activity dates. Requires domain or site_id. If the user did not specify a site, call get_me to list their sites — use it automatically if only one, otherwise ask the user to choose. Do NOT guess.
patch_reservoir_itemUpdate fields on a reservoir item — useful for archiving (status: 'archived'), bookmarking, retagging, or correcting metadata. Non-destructive. Requires domain or site_id. If the user did not specify a site, call get_me to list their sites — use it automatically if only one, otherwise ask the user to choose. Do NOT guess.
Media (6)
delete_mediaDelete a media asset from the library and storage.
finalize_media_uploadFinalize a presigned URL media upload. Call this after uploading the file to the upload_url returned by upload_media. Validates the file exists and processes image dimensions/thumbnails.
get_mediaGet details for a single media asset including URL, dimensions, format, and metadata.
list_mediaBrowse the site media library. Filter by kind (mcp-upload, blog-upload, reference-upload, video-upload, etc.) or media_type (image, video). Use kind: "reference-upload" to list user-uploaded reference images. Returns paginated results.
patch_mediaUpdate an existing asset's alt_text, filename, and/or title. Use it to fix metadata after an upload — e.g. write alt text for bulk-upload siblings that came back empty, since you can see the images. Pass the asset id plus any fields to change; omitted fields are left unchanged.
upload_mediaUpload media to the site media library. Three modes: 1. URL mode — pass file_url + filename + content_type. The server fetches the URL and stores the bytes. Best when the asset is already hosted somewhere. 2. Interactive mode — pass nothing (or just domain/label). The server returns { url, token, expires_at }. Show the URL to the user; they open it in a browser, sign in, and upload one OR MORE images via the BlackOps quick-upload page, then click "Done". Use this for clipboard screenshots and local files the user pasted — it avoids round-tripping bytes through the model, and supports bulk (multiple images on one link). You may also pass filename, alt_text, and/or title here — they ride along on the handoff and are applied to the FIRST uploaded asset (additional images auto-generate their own). 3. Claim mode — pass token (from interactive mode). Returns all finalized assets once the user clicks "Done" (status="completed" with an assets[] array — one or many). Returns status="pending" while they are still uploading or have not clicked Done yet; poll again. Supports jpeg, png, webp, gif, mp4, webm, quicktime in URL mode. Quick-upload (interactive/claim) is image-only.
Analytics — GA4 & Search Console (17)
ga4_campaign_performanceUTM campaign performance for a site: sessions, users, key events, and conversion rate grouped by session campaign / source / medium, ranked by sessions. Lets you query traffic and conversions BY campaign rather than only by raw source. Untagged visits land under direct/none: X/Twitter does NOT strip UTM parameters (it passes them through t.co intact and only truncates the DISPLAYED url with an ellipsis), so a campaign missing from this report was almost certainly never tagged at post time rather than stripped in transit. Requires GA4 credentials configured in site settings.
ga4_conversionsGA4 conversions (key events) for a site: each conversion event with its occurrence count, converting sessions, converting users, and conversion rates against total sessions and users in the window. Answers "how many of our visitors actually signed up / started a trial / connected an integration". Requires GA4 credentials configured in site settings.
ga4_eventsCustom GA4 event counts for a site: event name, total occurrences, and unique users over a lookback window, ranked by count. Pass event_names to filter to specific events (e.g. sign_up, start_trial, connect_integration, first_publish). Use this to measure activation and engagement actions that session-level traffic tools cannot see. Requires GA4 credentials configured in site settings.
ga4_landing_page_conversionsLanding pages ranked by conversion rate for a site. Each landing page shows sessions, conversions, and conversion rate. Pass conversion_event to rank by a specific event (e.g. sign_up); omit it to rank by all key events. min_sessions filters out tiny-sample pages. Answers "which entry pages actually convert", which top-pages-by-pageviews cannot. Requires GA4 credentials configured in site settings.
ga4_site_comparisonSide-by-side traffic metrics for multiple sites in a single call. Pass the list of domains to compare. Uses Promise.allSettled so a misconfigured property does not fail the entire request.
ga4_site_searchSite search queries captured by GA4 (the view_search_results event): each search term with its count and unique users, ranked by frequency. Surfaces what a site's audience is actively looking for — content gaps and demand signals. Pass min_count to drop one-off long-tail queries. Requires GA4 credentials and site search instrumentation configured. Returns an empty term list when site search is not wired up on the property.
ga4_top_pagesTop pages on a site ranked by pageviews, with title, path, sessions, and average time on page. Requires GA4 credentials configured in site settings.
ga4_traffic_overviewGoogle Analytics 4 traffic overview for a site: sessions, users, pageviews, bounce rate, and average session duration over a lookback window. Requires GA4 credentials configured in site settings.
ga4_traffic_sourcesTraffic source/medium/channel breakdown for a site, ranked by sessions. Requires GA4 credentials configured in site settings.
ga4_trendDaily time-series for a single GA4 metric (sessions, users, pageviews, newUsers) over a lookback window. Requires GA4 credentials configured in site settings.
gsc_compare_periodsCompare Google Search Console performance across two date ranges with delta math built in. Returns winners, losers, and net change for the top-N queries or pages. Read-only. Requires GSC credentials configured in site settings and gsc_mcp_enabled.
gsc_index_statusGoogle Search Console indexing state for a list of URLs. Aggregates URL Inspection results into buckets (indexed, crawled-not-indexed, discovered-not-indexed, not-found, excluded). The most important GSC tool: it answers "is Google indexing my pages?". Read-only. Requires GSC credentials configured in site settings and gsc_mcp_enabled.
gsc_request_indexingRequest that Google crawl and consider indexing a list of URLs. Use to push high-value URLs to the front of the crawl queue after content updates, force re-evaluation of "Crawled, currently not indexed" pages, or trigger initial indexing on freshly published URLs. Up to 10 URLs per call. Backed by the Google Indexing API with a 200/day per-site quota. Every URL is HEAD/GET pre-validated before submission so 404s and typos do not burn quota; invalid URLs are returned as skipped_invalid with a reason. Pass skipValidation=true only for intentionally-unreachable URLs (auth, maintenance, brand new). Every response includes quota_remaining, quota_total, quota_reset_at. On exhaustion the tool returns a structured response — it does not throw or retry. The first call for a given site prepends a one-time TOS disclosure (Google's Indexing API is officially scoped to job postings and livestream events; works for general content in practice). WRITE TOOL. Requires GSC credentials configured in site settings and gsc_mcp_enabled.
gsc_search_analyticsGoogle Search Console performance: queries, pages, countries, devices broken down by impressions, clicks, CTR, and average position. Read-only. Requires GSC credentials configured in site settings and gsc_mcp_enabled.
gsc_sitemapsList sitemaps submitted to Google Search Console for a property: status, last-read date, errors, warnings, submitted vs. indexed page counts. Pass includeUrls=true to also fetch and return the actual URL list for each sitemap (or child sitemaps for sitemap-index files). Read-only. Requires GSC credentials configured in site settings and gsc_mcp_enabled.
gsc_submit_sitemapSubmit (or resubmit) a sitemap to Google Search Console. Use to push a fresh sitemap, force a refetch on a stale one, or notify Google after a sitemap structure change. Submitting an already-submitted sitemap is idempotent — Google treats it as a refetch request. WRITE TOOL. Requires GSC credentials configured in site settings and gsc_mcp_enabled.
gsc_url_inspectionPer-URL deep dive from Google Search Console: indexing state, last crawl, page-fetch state, robots.txt state, canonical, mobile usability, structured data verdict. Pass an array of URLs (1-50). For domain-wide indexing audits, prefer gsc_index_status. Read-only. Requires GSC credentials configured in site settings and gsc_mcp_enabled.
Video (14)
attach_video_to_tiktokAttach the video to a TikTok draft, using an asset_id from upload_media. Required before publishing — TikTok posts cannot be text-only. A TikTok post holds exactly one video, so this REPLACES any previously attached video. The asset must be a video (mp4, webm, or quicktime) with a known byte size, because TikTok requires the exact file length up front to plan the chunked upload. Bytes are transferred at publish time, not here.
batch_videosCreate multiple videos at once. Two modes: "variations" clones a base video with different content (for content series), "story-split" splits a long video into 15-second story segments. Platform pacing options: youtube, tiktok, instagram_stories, linkedin, twitter.
create_conversation_videoCHAT-ONLY scene builder. Produces a video with the parsed conversation as the entire content — no hook, no stat, no CTA. **Use this only when the user explicitly asks for a chat-only artifact** ("just the conversation," "raw chat video," "no bookends"). For shareable assets — proof stories, demos, case studies, marketing videos, social posts — use `create_video` with `from_note_id` instead; that orchestrator auto-assembles hook → chat → CTA so the chat is framed by an opener and a close. Returns the video id ready for `render_video`.
create_videoCreate a new video project. THIS IS THE ORCHESTRATOR — use it for any shareable asset (proof story, demo, case study, marketing video, social post). Two ways to use it: 1. **Full spec mode**: Provide `spec.scenes` with templateId + slots for every scene (use `list_templates` to see templates: hook, chat-demo, stat, cta, pipeline, comparison, impact, quote, feature-grid, code-block, browser-frame, phone-frame). 2. **Conversation mode**: Pass `from_note_id` (a saved BlackOps conversation note). Without explicit scenes this auto-assembles **hook (from note title) → chat-demo (parsed conversation) → cta (brand close)** — a complete shareable asset, not a chat scroll. With explicit scenes, replace any scene whose templateId is "chat-from-note" with the parsed conversation. Tune chat behavior via `conversation` (voiceover_text, voice_id, max_messages, chat_style, duration, input_placeholder); override copy via `hook` and `cta`. IMPORTANT: Each scene supports a voiceover object with "text" (narration) and optional "voiceId" (ElevenLabs voice ID). If the user does not specify whether they want voiceover, ASK before creating. After creating, call generate_voiceover to produce audio. Use `create_conversation_video` ONLY when the user explicitly asks for a chat-only artifact with no hook/CTA.
delete_videoDelete a video and all its exports and segments.
generate_voiceoverGenerate ElevenLabs voiceover audio for all scenes that have voiceover.text set. Automatically sizes scene durations to fit the narration. Call this after create_video or update_video when scenes include voiceover text. The audio will be mixed into the final MP4 when render_video is called. Voice fallback chain: per-scene voiceover.voiceId → video-level voiceId param → site custom_voice_id → site default_voice_id → Brian. Users can paste any ElevenLabs voice ID as their custom voice in Settings > Branding. Use list_voices to see curated voices, or use get_brand_design to check the site's configured voice.
get_videoGet a video by ID, including its exports (rendered files), segments (per-scene renders), and media_assets (auto-registered media library entries). When status === "rendered", the response includes media_assets[0].id — pass THAT asset_id directly to attach_media_to_tweet or attach_media_to_post (do NOT call list_media first, do NOT pass a raw URL). Rendered videos are automatically added to the media library; the asset_id is available here.
list_templatesList available video scene templates. Each template defines slots (content placeholders) that can be filled when creating scenes. Templates: hook (headline), chat-demo (chat UI), stat (metric counter), cta (call to action).
list_videosList all videos for a site. Returns id, title, status, format, duration, and progress.
list_voicesList available ElevenLabs voices for voiceover. Returns curated catalog with voice ID, name, gender, accent, and description. Use the voice ID in create_video (per-scene voiceover.voiceId) or generate_voiceover (voiceId param).
log_voice_correctionRecord that the user rewrote AI-generated text, so the daily voice digest can look for patterns across a whole day of edits. Pass original_text (what you wrote) and rewritten_text (their version) BOTH VERBATIM — keep their spacing, capitalisation and typos exactly, because the formatting is the signal and a tidied-up copy teaches nothing. Also pass surface (blog, x_reply, linkedin, threads, session) and mode (quick for off-the-cuff, drafted for considered writing); a rule learned from a quick reply is never applied to drafted writing. This only stores the correction — it changes no rules and asks the user nothing, so call it silently whenever they restate something in their own words. It is the batched counterpart to calibrate_voice: use calibrate_voice when you want suggested rules back right now, and this when you just want the edit on the record.
render_videoTrigger an async render job for a video. Returns immediately with a progress page URL — poll get_video to check status (generation_progress, generation_stage). Quality: draft (fast), standard, high. IMPORTANT: When status becomes "rendered", the MP4 is AUTOMATICALLY registered as a media library asset. The get_video response will include `media_assets[0].id` — pass THAT asset_id directly to attach_media_to_tweet or attach_media_to_post. Do NOT call list_media first, and do NOT pass a raw URL to attach_media_*. The full pipeline is: render_video → poll get_video until rendered → use media_assets[0].id → attach_media_to_tweet → post_tweets.
transcribe_videoTranscribe any video and return the transcript plus an AI summary synchronously. Pass url for an X post (x.com or twitter.com status link), a YouTube video (watch, youtu.be, embed, or shorts), or a direct link to an audio or video file; or pass media_id for something already in the site's media library. The source is detected automatically, so there is one tool for all of them. Returns { source, source_id, source_url, title, author, post_text, duration_seconds, transcript_text, language, summary, key_insights, extraction_method, cached }, with fields a given source cannot supply set to null. YouTube uses captions when they exist; everything else runs Whisper, splitting long audio into overlapping segments and stitching them, so there is no duration cap. Cached per source; pass refresh=true to bypass. Pass include_summary=false to skip the summary and return the transcript alone. TWO POSSIBLE OUTCOMES: a short video returns the transcript directly (transcript_text populated). A video too long to finish in one request returns an accepted job instead — job_id set, transcript_text null, plus segments_total — and is transcribed in the background a segment at a time. In that case tell the user it is running and how many segments it has, then check get_transcription_job later; do NOT report a job_id as though it were a transcript, and do not claim the video was transcribed until the job reports completed. Errors: UNSUPPORTED_SOURCE, MEDIA_NOT_FOUND, MEDIA_NOT_TRANSCRIBABLE, NO_VIDEO, NO_AUDIO, NO_CAPTIONS, VIDEO_TOO_LONG (the run will not fit the processing window, past roughly 45 minutes of audio), INSUFFICIENT_CREDITS. Call it and report the transcript; do not raise cost or credits with the user unless INSUFFICIENT_CREDITS actually comes back. Does not store the transcript in any reservoir — use post_reservoir_ingest separately if you want to persist it.
update_videoUpdate a video's title, spec (scenes, brand, format), or status. Provide only the fields to update.
Deprecated (2)
Still callable for one migration cycle, but slated for removal — prefer the replacement named in each description.
get_posts_by_idDeprecatedDEPRECATED — use get_post. Retained as an alias for one migration cycle. Fetch a single blog post by slug or UUID, including full content, metadata, and SEO fields.
post_notes_writeDeprecatedDEPRECATED — use post_notes instead. This tool writes a markdown file to a repo target WITHOUT creating a queryable BlackOps note record, which causes silent failures: the file exists but get_note returns 404, downstream tools that need a note_id break, and the artifact is invisible to list_notes / search / brain compile. post_notes already auto-syncs to the connected Obsidian vault (returns a sync.commitSha in the response), and is the correct entry point for note creation. Only call post_notes_write when the user has explicitly asked for a raw repo file write with no BlackOps record — which is rare and almost always a mistake. Required: markdown_content plus EITHER target_id OR brain_id. PATH CONTEXT: relative_path must be relative to the target's base_path — do NOT include the base path prefix. Example: if the target's base_path is /notes/cap/AI and the desired full path is /notes/cap/AI/personal-brand/file.md, set relative_path: personal-brand/file.md. Call get_repo_targets first to inspect each target's base_path before constructing paths. OPT-IN REDACTION: if the workspace has enabled "Redact email addresses on vault sync" (Knowledge Settings), email addresses are redacted from the vault-synced file before commit (allowlisted addresses pass through) and the response carries a redaction block + warning; the BlackOps record always keeps the original content. With the setting off (default), content syncs exactly as written.