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, and public hub setup — 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, 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({})

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.
  • 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.