Documentation

Documentation / Integrations

MCP (AI assistants)

MCP (Model Context Protocol) lets AI assistants like Claude operate Power Author through a set of structured tools (keyword research, modifier authoring, combination editing, public hub setup and article generation) using a project-bound API key.

What MCP is

MCP is an open protocol for exposing tools and resources to AI assistants. Power Author ships an MCP server at /api/mcp; once you connect it to your assistant of choice, the assistant can call tools like list_combinations, add_keyword, update_combination, add_combination_modifiers, delete_combination_articles, and more — exactly the same operations available in the dashboard, just driven by the assistant.

Prerequisites

  • Generate an MCP API key in Workspace Settings → MCP & Tools. You can scope it to a single project (recommended for most workflows) or leave it workspace-wide and pass projectId on each tool call.
  • Add the server to your assistant. The connect card on the same settings page provides ready-to-paste configs for Cursor, Claude Code, and Claude Desktop.
  • Workspace-wide keys: call list_projects first to find the right projectId for the project you want to work in.

Combination public hub workflow

For the underlying concepts — what a hub page is, when to use one, the three layout options, required fields — see Combinations → Public Hub Pages. The MCP tools below are the operational surface.

Which layout to pick

  • grouped (default) — section per IMMEDIATE parent of each article's varying-tag. Best for shallow (1–2 level) role trees, or when you want every L2 visible as its own section.
  • grouped_by_top — section per TOP-LEVEL (L1) ancestor. Best for 3+ level trees (e.g. field → role → niche), so all articles in a field sit together under one section.
  • flat — one grid, no section headings. Best when there's no meaningful grouping or very few articles.

Operations

  • Enable hub: update_combination with { id, publishAsHub: true, slug: "cover-letter-examples", hubNavStyle: "grouped_by_top" }. hubNavStyle defaults to "grouped" when omitted.
  • Switch layout on a published hub: update_combination with { id, hubNavStyle: "grouped_by_top" }. This does NOT call unpublish — the new layout ships on the next Push hub to site or full publish run.
  • Author copy: upsert_combination_hub_copy with { combinationId, languageCode: "en", title, description, metaDescription }. At least one of the three text fields is required; omitted fields are left untouched on update.
  • Read back / verify: get_combination returns core combination columns (including hubNavStyle) plus a translations map keyed by language code. list_combinations returns the same hub fields across every combination in one call — useful for auditing layouts.
  • Disable hub: update_combination with { id, publishAsHub: false }. This triggers a destination unpublish call BEFORE the toggle is persisted; if the unpublish fails, the toggle change is not applied and the tool returns an error.

Modifier descriptions workflow

  • Find candidates: list_modifiers with { groupName: "role", level: 1 } returns all L1 role modifiers, now including their current description for the project's default (source) language.
  • Author one: update_modifier_description with { modifierId, description }. When languageCode is omitted the project's default language is used — matching the typical “set the English description” flow.
  • Author many: bulk_update_modifier_descriptions with { updates: [{ modifierId, description }, ...] }. Recommended for the “describe every L1 role” workflow. Returns per-row success or error.
  • Other locales come from the workspace Translate Taxonomy run; manual writes to non-source locales are allowed but may be overwritten by a later translation run.

Worked example — cover-letter hub grouped by field (L1)

Mirroring the example in Combinations → Public Hub Pages: a cover-letter-examples hub for a job-posting platform whose role tree has three levels (field → role → niche), so we want one section per field. End-to-end as MCP tool calls (the platform stays anonymous):

// 1. Find the combination you want to publish as a hub, and inspect
//    its current settings + copy before changing anything.
list_combinations({})            // returns hubNavStyle on every row
get_combination({ id: 12 })      // detail for the chosen combination

// 2. Enable the hub with the L1 layout in one call. The role tree under
//    this combination has 3 levels, so 'grouped_by_top' (one section per
//    field) is the right pick.
update_combination({
  id: 12,
  publishAsHub: true,
  slug: "cover-letter-examples",
  hubNavStyle: "grouped_by_top"
})

// 3. Author the source-language hub copy (H1, intro, SEO meta).
upsert_combination_hub_copy({
  combinationId: 12,
  languageCode: "en",
  title: "Cover Letter Examples for 1,000+ Roles",
  description: "Browse cover letter examples for over a thousand job titles, organized by field. Each example shows two formats side by side, with notes on when to use each.",
  metaDescription: "Cover letter examples for 1,000+ job titles, organized by field. See traditional and modern formats side by side."
})

// 4. Author one description paragraph per L1 (field) modifier - these
//    render as the intro under each section heading on the hub.
list_modifiers({ groupName: "role", level: 1 })   // returns existing description for source language
bulk_update_modifier_descriptions({
  updates: [
    { modifierId: 101, description: "Healthcare roles spanning patient care, clinical support, and allied health." },
    { modifierId: 102, description: "Engineering roles across software, hardware, and infrastructure." },
    { modifierId: 103, description: "Education roles from early childhood through higher education." }
  ]
})

// 5. Verify before the next publish run.
get_combination({ id: 12 })

// --- Migration variant ---------------------------------------------------
// Switch an EXISTING 'grouped' hub (parent-walk) to 'grouped_by_top' (L1
// buckets). Safe in-place change - no unpublish call. The new layout ships
// on the next 'Push hub to site' or full publish run.

update_combination({ id: 12, hubNavStyle: "grouped_by_top" })

// --- Audit variant -------------------------------------------------------
// See every combination's hub settings in one call (publishAsHub, slug,
// hubNavStyle), then drill in for the ones that need attention.
list_combinations({})

Generating articles (pipeline runs)

An assistant can generate articles and read the result without the dashboard, which makes a review loop possible: generate two or three sample articles, read them, adjust the prompts, regenerate, and only then run every article. A pipeline run executes the article's pipeline (the combination's own steps when it has them, otherwise the project's) in the background worker, in the default locale and, through the translation steps, in every other active locale. Every step is a paid model call. Nothing is published.

How it works

  • Dry run by default. start_pipeline_run without dryRun: false returns the articles it would run (by status and combination), the concurrency, and what a full run resets. It changes nothing.
  • Choose articles by id or by filter. Pass articleIds, or combinationId and/or modifierIds (the article's varying modifier, with includeDescendants for its children) plus an optional statuses filter such as ["draft"]. Ids from another project are refused.
  • Full run or fill in. By default a run regenerates from scratch: the articles' outputs are deleted before it starts. Unpublished articles show as generating; published ones stay live with their current version and are marked outdated until you republish them. skipCompleted: true only runs the steps that have no completed output.
  • Same rules as the dashboard. Concurrency is the project's saved setting, and an article that is already in a queued or running run is refused, with the blocking run ids.

Operations

  • Preview: start_pipeline_run({ combinationId: 7, modifierIds: [101, 102, 103] }).
  • Start: the same call with dryRun: false. Returns the runId.
  • Follow it: get_pipeline_run({ runId }) returns the run's status, done, elapsed time and, per article, its status, current step and errors. list_pipeline_runs lists recent runs, including those started in the dashboard.
  • Read the result: get_article_outputs({ articleId }) returns each step's output per locale with its status, error, model and cost. Narrow it with locale and stepKeys, or pass includeOutput: false for status and cost only.
  • Stop it: cancel_pipeline_run({ runId }) works like Cancel in the dashboard. Completed outputs are kept.

Reading and editing prompts

Each step of a pipeline is a prompt template. Prompts are live data: on your real workspace, saving one changes every later run of that combination, so an edit is a dry run until you say otherwise.

  • Read: get_pipeline_steps({ combinationId: 7 }) returns the steps that generate the combination's articles in run order: key, whether it runs, the model it uses (its own or the project's default), and the prompt template. Placeholders such as {{body}} are filled at run time from earlier steps, modifiers and snippets. Without combinationId it returns the project's pipeline, the template new combinations copy.
  • Preview a change: update_pipeline_step({ combinationId: 7, stepKey: "generate_body", promptTemplate }) returns the fields that change, a line diff of the prompt and any warnings, such as a placeholder that goes empty when you disable a step. It changes nothing.
  • Save it: the same call with dryRun: false. You can also pass enabled, aiModel (null returns the step to the project's default model) or stepType: "artifact" turns the step into sample resume PDFs, drafted on your website from the candidates and target postings written by earlier steps, and "job_figures" turns it into a fetch of the article's job figures from your website, which later prompts read through the step's output key. Articles already generated keep their text until you regenerate them.
  • Combinations with their own pipeline only. Every combination created in the dashboard has one. The project's pipeline is edited in the dashboard, and an edit is refused while a pipeline run is generating the combination's articles.

Refreshing articles from a month's figures

A combination with a Job figures step can have its published articles rewritten from a new month's figures. Only the body, meta description and excerpt change: never the title, slug or URL.

  • Try it first: start_monthly_refresh({ combinationId: 7 }) is a dry run. It rewrites and checks every article and publishes nothing.
  • Compare: get_monthly_refresh({ refreshId: 12, articleId: 345 }) returns one article's before and after. The same text shows in get_article_outputs as refresh:<stepKey> rows, which the next publish does not send.
  • Run it for real: pass dryRun: false and confirm with the combination's exact name. Articles that pass go out together, those that fail keep last month's version, and if too many fail nothing is published.
  • Automatic refreshes are off by default. An admin turns them on in the combination's Monthly refresh tab, and list_monthly_refreshes shows the setting.

Pruning the live catalogue by role (unpublish / republish)

When a site publishes one article per (combination × varying modifier) — e.g. five article types × a thousand roles — the useful unit for shrinking the catalogue is the role, not the keyword. delete_keyword removes one article type across every role and delete_modifier is permanent, so neither fits. The tools below change publish state only: nothing is deleted, generated content and modifier links survive, and the same URLs can be brought back later.

How it works

  • Every real unpublish belongs to a prune batch. dryRun: false needs batch: { name, reason } (or batchId to add to an existing batch). Each article the run takes down is stamped with the batch, the time and a copy of the reason, and reads as the derived status unpublished instead of pipeline_complete in the dashboard and in list_articles. The next successful publish clears the stamp.
  • The destination's answer decides. The worker still fetches each URL after an unpublish, but only records the result: a page that still answers 200 from the destination's one-hour page cache is stale_cache and is checked again 65 minutes later. Only a page that is still live then (mismatch) turns the item into a failure.
  • Per-locale is opt-in and gated. Without locales an unpublish removes every locale of the article. With locales: ["ko"] only those locales come down, the article stays published, and later publishes leave the hidden locales out. Power Author refuses per-locale calls unless the destination advertises capabilities.perLocaleUnpublish on GET /api/career-advice/unpublish: a destination without it ignores locales and takes down every locale.
  • Filters combine with AND. modifierIds matches the article's VARYING modifier (exact match; pass includeDescendants: true to cascade to L2/L3 children), combinationIds restricts to article types. At least one of the two is required.
  • Dry run by default. Both tools return counts only (articles by combination, by locale, total live URLs) unless you pass dryRun: false.
  • Real runs are queued, not inline. A role-level prune is thousands of articles and tens of thousands of URLs, far more than one MCP request can process. dryRun: false enqueues a publish run (mode unpublish or publish) that the background worker processes in batches of 25 with automatic retry of transient failures. The response carries a runId.
  • Idempotent and safe to re-issue. Only published articles are selected for unpublish (only pipeline_complete ones for republish); articles already in an active run are listed under skipped. Re-running the same call after a partial run only touches what is still live.
  • Every run is recorded. MCP-created runs store who (user or API key), when, the filter, and the dry-run summary on publish_runs.source; list_publish_runs shows them so a later session can see what was pruned and why.

Operations

  • Preview a prune: unpublish_articles with { modifierIds: [123], includeDescendants: true } (dry run). Returns articles, byCombination, byLocale and liveUrls; changes nothing.
  • Run it: the same call with dryRun: false and batch: { name, reason }. Returns { runId, queued, skipped, failed, batch }.
  • Track progress: get_publish_run with { runId } — status, item breakdown, done, the recorded source, URL verification counts, and up to 200 failed items with their error.
  • Bring a role back: republish_articles with the same filter and dryRun: false. Articles return at the same URLs. Articles under archived modifiers are skipped unless force: true.
  • Send regenerated text live: a full pipeline run leaves a published article showing its old text. republish_articles with outdated: true and dryRun: false sends the new version of the articles flagged needsRepublish, at the same URLs.
  • Bring a batch back: republish_articles({ batchId: 12, dryRun: false }) restores exactly what batch 12 took down and is still down. list_prune_batches and get_prune_batch show each batch's runs and, by combination and by modifier, what is still down versus republished.
  • Retire a role: archive_modifiers with { modifierIds: [123], reason: "…" }. Generation skips archived modifiers, so a new combination creates no articles for them; publishing refuses their articles unless force: true; list_modifiers hides them unless archived: "any". Descendants are included by default. Archiving does not unpublish unless you pass unpublish: true; unarchive_modifiers reverses it.
  • Retire a locale: archive_project_locale({ locale: "ko", reason: "…", dryRun: false }) stops generation, translation and publishing of the locale and keeps its translations; unarchive_project_locale reverses it and list_project_locales shows each locale's state and counts. Archiving does not unpublish: unpublish_articles({ combinationIds: [7], locales: ["ko"], batch: { … }, dryRun: false }) takes existing pages down where the destination supports it (get_locale_publish_settings). Only an archived locale's translations can be deleted: delete_project_locale_translations({ locale: "ko", dryRun: false, confirm: "ko" }), after a dry run that counts the rows in every table. Each change is recorded as a prune batch.
  • Verify from either side: list_modifiers now returns articleCounts: { published, unpublished } per modifier; list_articles accepts modifierId, includeDescendants and locale filters and returns modifierId, sentAt, locales and archivedLocales per article.
  • Deleting a modifier afterwards: delete_modifier also defaults to a dry run now, and its preview says how many affected articles are live. It never unpublishes — published articles stay on the site with their modifier link removed — so run unpublish_articles first and wait for that run to finish.

Field reference

  • publishAsHub — boolean toggle on combinations; OFF by default.
  • slug — public hub URL segment on combinations (the path on the destination is /career-advice/{slug} on Specific Resume). Distinct from slugTemplate, which is the per-article URL slug template (e.g. {{role}}-resume-examples).
  • hubNavStyle — one of "grouped" (default; section per IMMEDIATE parent of each article's varying-tag), "grouped_by_top" (section per TOP-LEVEL/L1 ancestor — recommended for 3+ level role trees), or "flat" (single grid, no sections). See the decision aid above and the dedicated Public Hub Pages page.
  • title, description, metaDescription per locale on combination_translations (keyed by (combinationId, languageId)).
  • modifier_translations.description per locale (keyed by (modifierId, languageId)) — the per-section intro paragraph rendered above each L1 bucket on grouped hub pages. There is no description column on modifiers itself.

Caveats & errors

  • Toggling publishAsHub from true to false fires an unpublish call to the destination site BEFORE persisting. If that call fails (network, 5xx, etc.), the toggle change is rolled back and update_combination returns an error so PA's view of the combination matches what the destination is actually serving.
  • REST and MCP share the same upsert helpers under the hood, so dashboard edits and MCP edits cannot drift apart.
  • Source-language descriptions are typically the only ones authored by hand — let the workspace Translate Taxonomy run handle the rest.