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
projectIdon 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_projectsfirst to find the rightprojectIdfor 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_combinationwith{ id, publishAsHub: true, slug: "cover-letter-examples", hubNavStyle: "grouped_by_top" }.hubNavStyledefaults to"grouped"when omitted. - Switch layout on a published hub:
update_combinationwith{ 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_copywith{ 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_combinationreturns core combination columns (includinghubNavStyle) plus atranslationsmap keyed by language code.list_combinationsreturns the same hub fields across every combination in one call — useful for auditing layouts. - Disable hub:
update_combinationwith{ 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_modifierswith{ groupName: "role", level: 1 }returns all L1 role modifiers, now including their currentdescriptionfor the project's default (source) language. - Author one:
update_modifier_descriptionwith{ modifierId, description }. WhenlanguageCodeis omitted the project's default language is used — matching the typical “set the English description” flow. - Author many:
bulk_update_modifier_descriptionswith{ 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_runwithoutdryRun: falsereturns 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, orcombinationIdand/ormodifierIds(the article's varying modifier, withincludeDescendantsfor its children) plus an optionalstatusesfilter 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: trueonly 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 therunId. - 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_runslists 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 withlocaleandstepKeys, or passincludeOutput: falsefor 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. WithoutcombinationIdit 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 passenabled,aiModel(nullreturns the step to the project's default model) orstepType:"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 inget_article_outputsasrefresh:<stepKey>rows, which the next publish does not send. - Run it for real: pass
dryRun: falseandconfirmwith 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_refreshesshows 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: falseneedsbatch: { name, reason }(orbatchIdto 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 statusunpublishedinstead ofpipeline_completein the dashboard and inlist_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_cacheand 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
localesan unpublish removes every locale of the article. Withlocales: ["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 advertisescapabilities.perLocaleUnpublishonGET /api/career-advice/unpublish: a destination without it ignoreslocalesand takes down every locale. - Filters combine with AND.
modifierIdsmatches the article's VARYING modifier (exact match; passincludeDescendants: trueto cascade to L2/L3 children),combinationIdsrestricts 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: falseenqueues a publish run (modeunpublishorpublish) that the background worker processes in batches of 25 with automatic retry of transient failures. The response carries arunId. - Idempotent and safe to re-issue. Only
publishedarticles are selected for unpublish (onlypipeline_completeones for republish); articles already in an active run are listed underskipped. 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_runsshows them so a later session can see what was pruned and why.
Operations
- Preview a prune:
unpublish_articleswith{ modifierIds: [123], includeDescendants: true }(dry run). Returnsarticles,byCombination,byLocaleandliveUrls; changes nothing. - Run it: the same call with
dryRun: falseandbatch: { name, reason }. Returns{ runId, queued, skipped, failed, batch }. - Track progress:
get_publish_runwith{ runId }— status, item breakdown,done, the recordedsource, URLverificationcounts, and up to 200 failed items with their error. - Bring a role back:
republish_articleswith the same filter anddryRun: false. Articles return at the same URLs. Articles under archived modifiers are skipped unlessforce: true. - Send regenerated text live: a full pipeline run leaves a published article showing its old text.
republish_articleswithoutdated: trueanddryRun: falsesends the new version of the articles flaggedneedsRepublish, 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_batchesandget_prune_batchshow each batch's runs and, by combination and by modifier, what is still down versus republished. - Retire a role:
archive_modifierswith{ modifierIds: [123], reason: "…" }. Generation skips archived modifiers, so a new combination creates no articles for them; publishing refuses their articles unlessforce: true;list_modifiershides them unlessarchived: "any". Descendants are included by default. Archiving does not unpublish unless you passunpublish: true;unarchive_modifiersreverses 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_localereverses it andlist_project_localesshows 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_modifiersnow returnsarticleCounts: { published, unpublished }per modifier;list_articlesacceptsmodifierId,includeDescendantsandlocalefilters and returnsmodifierId,sentAt,localesandarchivedLocalesper article. - Deleting a modifier afterwards:
delete_modifieralso 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 rununpublish_articlesfirst and wait for that run to finish.
Field reference
publishAsHub— boolean toggle oncombinations; OFF by default.slug— public hub URL segment oncombinations(the path on the destination is/career-advice/{slug}on Specific Resume). Distinct fromslugTemplate, 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,metaDescriptionper locale oncombination_translations(keyed by(combinationId, languageId)).modifier_translations.descriptionper locale (keyed by(modifierId, languageId)) — the per-section intro paragraph rendered above each L1 bucket on grouped hub pages. There is nodescriptioncolumn onmodifiersitself.
Caveats & errors
- Toggling
publishAsHubfrom 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 andupdate_combinationreturns 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.