@bettercms-ai/mcp 0.54.1 → 0.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2574,7 +2574,20 @@ var LISTING_TAIL = /* @__PURE__ */ new Set([
2574
2574
  "deploy_from_upload",
2575
2575
  "set_authoring_preference",
2576
2576
  "set_media_delivery",
2577
- "delete_form_submission"
2577
+ "delete_form_submission",
2578
+ // The ABM campaign lane: a marketer's dashboard surface, plus the worker tools a task runner
2579
+ // claims with. Ten more tools than the tail was written for, and a connector that truncates
2580
+ // would otherwise lose `get_deploy_status` and the GitHub lane to make room for them.
2581
+ "research_account",
2582
+ "claim_abm_task",
2583
+ "complete_abm_task",
2584
+ "fail_abm_task",
2585
+ "get_campaign_review",
2586
+ "regenerate_entry",
2587
+ "approve_campaign_entries",
2588
+ "publish_campaign",
2589
+ "get_campaign_share_link",
2590
+ "revoke_campaign_share_link"
2578
2591
  ]);
2579
2592
  function buildToolDefs(deps) {
2580
2593
  async function withClient(fn) {
@@ -3852,7 +3865,7 @@ ${res.warnings.join("\n")}` : summary, res.data);
3852
3865
  def(
3853
3866
  "generate_pages_from_dataset",
3854
3867
  "Generate many pages from a dataset",
3855
- "Turn a dataset into many pages at once (programmatic SEO from a keyword list, or ABM pages from an account list). Give a template `pageId`, a `mapping` (contentModelId, a slugTemplate like 'for-{{company}}', and per-field values that are either a column name or {ai:{prompt}}), and `rows`. ALWAYS call with dryRun:true first and show the sample \u2014 a real run parks for approval and must be released with approve_ai_job.",
3868
+ "Turn a dataset into many pages at once (programmatic SEO from a keyword list, or ABM pages from an account list). Give a template `pageId`, a `mapping` (contentModelId, a slugTemplate like 'for-{{company}}', and per-field values that are either a column name or {ai:{prompt}}), and `rows`. For an ABM campaign set `mapping.campaign.id` and the run fans out across every page that campaign owns, returning one job id per page in `jobIds`. ALWAYS call with dryRun:true first and show the sample \u2014 a real run parks for approval and must be released with approve_ai_job.",
3856
3869
  z.object({
3857
3870
  pageId: z.string().min(1),
3858
3871
  mapping: z.record(z.string(), z.unknown()),
@@ -3861,6 +3874,121 @@ ${res.warnings.join("\n")}` : summary, res.data);
3861
3874
  }).shape,
3862
3875
  async (c, a) => ok("Generation queued.", await data(c, "POST", `/management/bulk/generate`, { pageId: a.pageId, mapping: a.mapping, rows: a.rows, dryRun: a.dryRun }))
3863
3876
  ),
3877
+ def(
3878
+ "research_account",
3879
+ "Research one account",
3880
+ "Read one company's public site into a cited brief the page generator can use: what they do, who they sell to, what they say is hard, with a source URL on every fact. `lite` is one fetch and costs nothing extra; `deep` searches the web, costs 3 credits and is capped per day. A brief less than 30 days old is returned from cache for free. Company-scoped by design: it never stores a person, an email or a job title.",
3881
+ z.object({
3882
+ campaignId: z.string().min(1),
3883
+ domain: z.string().min(1),
3884
+ name: z.string().optional(),
3885
+ mode: z.enum(["lite", "deep"]).optional(),
3886
+ force: z.boolean().optional()
3887
+ }).shape,
3888
+ async (c, a) => ok("Account brief.", await data(c, "POST", `/management/campaigns/research`, { campaignId: a.campaignId, domain: a.domain, name: a.name, mode: a.mode, force: a.force }))
3889
+ ),
3890
+ def(
3891
+ "claim_abm_task",
3892
+ "Claim an account to research",
3893
+ "Claim the next account of a campaign that is waiting for YOU to research it. Returns the account, a `taskId` and a `leaseToken` good for ten minutes; `null` means there is nothing left and your loop is done. Research the company however you like, then call complete_abm_task with the brief \u2014 or fail_abm_task if you cannot. Never store a person, an email or a job title: the brief is refused if you do.",
3894
+ z.object({ campaignId: z.string().min(1), agentId: z.string().optional() }).shape,
3895
+ async (c, a) => ok("Claimed task.", await data(c, "POST", `/management/campaigns/${s(a.campaignId)}/tasks/claim`, { agentId: a.agentId }))
3896
+ ),
3897
+ def(
3898
+ "complete_abm_task",
3899
+ "Hand back an account brief",
3900
+ "Hand back the brief for a task you claimed. `brief` is {name, domain, industry?, size?, painPoint?, summary?, facts:[{text, sourceUrl}], logo?, brandColors?} and NOTHING else \u2014 an unknown key is refused with brief_invalid and the path. Every fact needs the URL it came from. When the campaign's last task is done the page generation starts on its own.",
3901
+ z.object({
3902
+ campaignId: z.string().min(1),
3903
+ taskId: z.string().min(1),
3904
+ leaseToken: z.string().min(1),
3905
+ brief: z.record(z.string(), z.unknown())
3906
+ }).shape,
3907
+ async (c, a) => ok("Brief accepted.", await data(c, "POST", `/management/campaigns/${s(a.campaignId)}/tasks/${s(a.taskId)}/complete`, { leaseToken: a.leaseToken, brief: a.brief }))
3908
+ ),
3909
+ def(
3910
+ "fail_abm_task",
3911
+ "Give an account task back",
3912
+ "Give a claimed task back when you cannot research the account \u2014 the site is down, there is nothing public to read, the domain is wrong. The page is still written, from the dataset alone, and says so.",
3913
+ z.object({
3914
+ campaignId: z.string().min(1),
3915
+ taskId: z.string().min(1),
3916
+ leaseToken: z.string().min(1),
3917
+ reason: z.string().optional()
3918
+ }).shape,
3919
+ async (c, a) => ok("Task returned.", await data(c, "POST", `/management/campaigns/${s(a.campaignId)}/tasks/${s(a.taskId)}/fail`, { leaseToken: a.leaseToken, reason: a.reason }))
3920
+ ),
3921
+ def(
3922
+ "get_campaign_review",
3923
+ "Review a campaign's pages",
3924
+ "The campaign's generated pages, SORTED BY DOUBT \u2014 flagged first, then least confident. Each row carries its flags (`unsupported_claim:headline` and the like), the claims the model said it made, which fields it wrote, and a preview link for drafts. `filter:'flagged'` is the review queue; `sample:5` is the first look at a big run; `filter:'failed'` lists the rows that produced no page at all and why.",
3925
+ z.object({
3926
+ campaignId: z.string().min(1),
3927
+ filter: z.enum(["all", "flagged", "pending", "approved", "rejected", "low_confidence", "failed"]).optional(),
3928
+ sample: z.number().optional(),
3929
+ pageId: z.string().optional(),
3930
+ runId: z.string().optional(),
3931
+ cursor: z.string().optional(),
3932
+ limit: z.number().optional()
3933
+ }).shape,
3934
+ async (c, a) => ok("Campaign review.", await data(c, "GET", `/management/campaigns/${s(a.campaignId)}/review${q({ filter: a.filter, sample: a.sample, pageId: a.pageId, runId: a.runId, cursor: a.cursor, limit: a.limit })}`))
3935
+ ),
3936
+ def(
3937
+ "regenerate_entry",
3938
+ "Rewrite one account's page",
3939
+ "Rewrite ONE account's page, optionally with an instruction ('lead with the integration story') and optionally only certain fields. Costs one credit and writes a DRAFT \u2014 a published page shows unpublished changes until someone publishes it again. Re-runs from the original spreadsheet row, not from the last answer, so repeated regenerates do not drift.",
3940
+ z.object({
3941
+ campaignId: z.string().min(1),
3942
+ entryId: z.string().min(1),
3943
+ instruction: z.string().optional(),
3944
+ fields: z.array(z.string()).optional()
3945
+ }).shape,
3946
+ async (c, a) => ok("Regeneration queued.", await data(c, "POST", `/management/campaigns/${s(a.campaignId)}/entries/${s(a.entryId)}/regenerate`, { instruction: a.instruction, fields: a.fields }))
3947
+ ),
3948
+ def(
3949
+ "approve_campaign_entries",
3950
+ "Mark campaign pages reviewed",
3951
+ "Mark campaign pages reviewed. `entryIds:'all_unflagged'` clears every page with nothing wrong with it, which is most of them; a FLAGGED page must be named explicitly, because approving one is a person saying they checked the claim. Approving does not publish \u2014 publish_campaign does, and it refuses pages that are still flagged.",
3952
+ z.object({
3953
+ campaignId: z.string().min(1),
3954
+ entryIds: z.union([z.array(z.string()), z.literal("all_unflagged")]),
3955
+ status: z.enum(["approved", "rejected"]),
3956
+ note: z.string().optional()
3957
+ }).shape,
3958
+ async (c, a) => ok("Entries reviewed.", await data(c, "POST", `/management/campaigns/${s(a.campaignId)}/entries/review`, { entryIds: a.entryIds, status: a.status, note: a.note }))
3959
+ ),
3960
+ def(
3961
+ "publish_campaign",
3962
+ "Publish a campaign",
3963
+ "Put a campaign's pages on the internet, or schedule them. `confirm: true` is REQUIRED \u2014 this is the one action in the ABM lane that a human, not you, decides. It REFUSES with `entry_flagged` if any page still carries an unchecked claim (call get_campaign_review and approve them first) and with `blueprint_missing_disclaimer` if a campaign page has lost its 'not affiliated with' band. Pages somebody rejected are skipped and counted.",
3964
+ z.object({
3965
+ campaignId: z.string().min(1),
3966
+ scheduledAt: z.string().optional(),
3967
+ unpublishAt: z.string().optional(),
3968
+ confirm: z.boolean()
3969
+ }).shape,
3970
+ async (c, a) => ok("Campaign publish queued.", await data(c, "POST", `/management/campaigns/${s(a.campaignId)}/publish`, { scheduledAt: a.scheduledAt, unpublishAt: a.unpublishAt, confirm: a.confirm }))
3971
+ ),
3972
+ def(
3973
+ "get_campaign_share_link",
3974
+ "Make a share link",
3975
+ "Make a link a rep can send before the pages are published. `scope:'campaign'` opens the whole account index; `scope:'account'` with an `externalKey` (the account's domain) opens exactly that account's pages and 404s on anyone else's. At most 14 days, revocable, rate-limited and noindex. The link's `uses` is how a rep sees Not sent / Sent / Visited.",
3976
+ z.object({
3977
+ campaignId: z.string().min(1),
3978
+ scope: z.enum(["campaign", "account"]).optional(),
3979
+ externalKey: z.string().optional(),
3980
+ expiresIn: z.string().optional(),
3981
+ label: z.string().optional()
3982
+ }).shape,
3983
+ async (c, a) => ok("Share link.", await data(c, "POST", `/management/campaigns/${s(a.campaignId)}/share-links`, { scope: a.scope, externalKey: a.externalKey, expiresIn: a.expiresIn, label: a.label }))
3984
+ ),
3985
+ def(
3986
+ "revoke_campaign_share_link",
3987
+ "Withdraw a share link",
3988
+ "Withdraw ONE share link by its `jti`, leaving every other link to the same campaign working. The link then answers 404. Safe to call twice.",
3989
+ z.object({ campaignId: z.string().min(1), jti: z.string().min(1) }).shape,
3990
+ async (c, a) => ok("Share link revoked.", await data(c, "DELETE", `/management/campaigns/${s(a.campaignId)}/share-links/${s(a.jti)}`))
3991
+ ),
3864
3992
  def(
3865
3993
  "list_ai_jobs",
3866
3994
  "List bulk jobs",
@@ -3871,7 +3999,7 @@ ${res.warnings.join("\n")}` : summary, res.data);
3871
3999
  def(
3872
4000
  "get_ai_job",
3873
4001
  "Get a bulk job",
3874
- "One bulk job \u2014 status, rows processed, rows created, conflicts, and the approval plan if it is still parked.",
4002
+ "One bulk job \u2014 its `type` (generate, seo, rename, migrate, publish, unpublish, research), status, rows processed, rows created, conflicts, and the approval plan if it is still parked. `rowResults` is the per-row ledger: every row that FAILED or was skipped, with a reason code, which is the only place \u201C20 rows in, 18 pages out\u201D is explained. `summary` splits a rerun into added / updated / unchanged, and `campaignId` / `collectionSlug` say which campaign and collection the pages landed in.",
3875
4003
  z.object({ jobId: z.string().min(1) }).shape,
3876
4004
  async (c, a) => ok("Bulk job.", await data(c, "GET", `/management/bulk/jobs/${s(a.jobId)}`))
3877
4005
  ),
@@ -5928,16 +6056,31 @@ End-to-end authoring from a repo or a brief. Confirm-first at every stage.
5928
6056
  apply via \`set_page_content\` / \`create_content_entry\` / \`update_content_entry\`.
5929
6057
  3. **SEO** \u2014 run the SEO flow to fill metaTitle/metaDescription for each page.
5930
6058
  Never invent brand facts \u2014 ask the user for anything the repo/brief doesn't state.`;
5931
- var LANDING_PAGES_FLOW = `### Generate landing pages (programmatic SEO / ABM) \u2192 \`write_content\` + \`create_content_entry\` + \`generate_seo_meta\`
6059
+ var LANDING_PAGES_FLOW = `### Generate landing pages (programmatic SEO / ABM) \u2192 \`generate_pages_from_dataset\`
5932
6060
  Spin up many pages sharing one template, each personalized per row (company, keyword, persona).
5933
- 1. **Template** \u2014 ensure a dynamic page or content model exists for the template
5934
- (\`create_page\` pageType 'dynamic' / \`create_content_model\`); its fields are the per-page slots.
5935
- 2. **Dataset** \u2014 get the list of targets from the user (rows of variables, e.g. company + industry).
5936
- 3. **Per row (loop)** \u2014 draft each slot with \`write_content\` (the row's variables as the brief/
5937
- \`context\`), pick a unique slug, then \`create_content_entry\` { contentModelId, data, slug, status }.
5938
- Add SEO with \`generate_seo_meta\` on the drafted copy and store it on the entry.
5939
- Confirm the first 1\u20132 rows with the user before generating the rest. For hundreds of rows,
5940
- the dashboard AI Page Builder bulk-imports a CSV \u2014 mention it.`;
6061
+
6062
+ For an ABM campaign \u2014 a page per NAMED COMPANY \u2014 the whole flow is eight steps, in this order:
6063
+ 1. **Research** \u2014 \`research_account\` per account, or set \`mapping.campaign.research\` and let the
6064
+ run do it ('lite' reads the company's own site, 'deep' searches the web, 'agent' hands the work
6065
+ to YOU through \`claim_abm_task\` / \`complete_abm_task\`).
6066
+ 2. **Generate** \u2014 \`generate_pages_from_dataset\` with \`mapping.campaign = { id, research,
6067
+ externalKeyColumn: 'domain' }\`. It fans out across every page the campaign owns and parks for
6068
+ approval. Re-running the same export UPDATES each account's page rather than duplicating it.
6069
+ 3. **Approve the job** \u2014 \`approve_ai_job\`, only once the user has said yes.
6070
+ 4. **Review** \u2014 \`get_campaign_review\` with \`sample: 5\`, then \`filter: 'flagged'\`. A flag reads
6071
+ \`unsupported_claim:headline\`: a figure that is in none of the account's own facts, so the field
6072
+ fell back to the blueprint's copy. \`filter: 'failed'\` lists rows that produced no page at all.
6073
+ 5. **Regenerate** \u2014 \`regenerate_entry\` with an instruction, one row at a time.
6074
+ 6. **Approve entries** \u2014 \`approve_campaign_entries\`; \`'all_unflagged'\` clears the rest in one call.
6075
+ A FLAGGED page must be named explicitly: approving one says a person checked the claim.
6076
+ 7. **Publish** \u2014 \`publish_campaign\` with \`confirm: true\`. It refuses \`entry_flagged\` while any
6077
+ page carries an unresolved flag, and \`blueprint_missing_disclaimer\` if a page has lost its
6078
+ "not affiliated with" band. The user decides this step, never you.
6079
+ 8. **Share** \u2014 \`get_campaign_share_link\` for a link a rep can send before publishing.
6080
+
6081
+ For a plain programmatic-SEO run (a page per keyword, no named companies) the same tool works
6082
+ with no \`campaign\`, and \`write_content\` + \`create_content_entry\` per row is still fine for a
6083
+ handful. ALWAYS dry-run first and show the sample.`;
5941
6084
  var SEO_FLOW = `### Optimize SEO \u2192 \`generate_seo_meta\` + the page/entry update tools
5942
6085
  Fill or refresh SEO metadata across the site.
5943
6086
  1. **Target** \u2014 pick the pages/entries (\`list_pages\` / \`list_content_entries\`); confirm scope with the user.
@@ -6221,7 +6364,7 @@ Confirm each stage with the user before writing. On 401/403, the MCP key needs (
6221
6364
  "generate_landing_pages",
6222
6365
  {
6223
6366
  title: "Generate landing pages (programmatic SEO / ABM)",
6224
- description: "Spin up many personalized landing pages from one template + a dataset, drafting each page's copy with write_content and SEO with generate_seo_meta.",
6367
+ description: "Spin up many personalized landing pages from one template + a dataset. For ABM (a page per named company) this is the research -> generate -> review -> approve -> publish -> share order.",
6225
6368
  argsSchema: {
6226
6369
  request: z2.string().optional().describe("the campaign, e.g. 'a page per target company for our ABM push'")
6227
6370
  }