@bettercms-ai/mcp 0.9.0 → 0.11.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/SKILL.md +18 -0
- package/dist/index.js +462 -21
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/SKILL.md
CHANGED
|
@@ -58,6 +58,24 @@ Tools: `list_components`, `get_component`, `create_component`, `update_component
|
|
|
58
58
|
Authoring `blockJson` blind is error-prone — keep trees small, confirm with the user,
|
|
59
59
|
and read back with `get_component` to verify.
|
|
60
60
|
|
|
61
|
+
## AI content & SEO
|
|
62
|
+
Tools: `write_content`, `generate_seo_meta`. Guided prompts: `build_site`,
|
|
63
|
+
`generate_landing_pages`, `seo_optimize`. These use the workspace's own Anthropic key
|
|
64
|
+
(BYOK, unmetered) or platform AI credits, and return a *suggestion* — you apply it.
|
|
65
|
+
|
|
66
|
+
1. **Write copy** → `write_content { action, text, instructions?, targetLang?, context? }`.
|
|
67
|
+
`action`: `write` (draft from a brief), `rewrite` (improve existing copy), `translate`
|
|
68
|
+
(needs `targetLang`). Returns the text — apply it with `set_page_content`, `create_entry`,
|
|
69
|
+
or `update_entry`.
|
|
70
|
+
2. **SEO meta** → `generate_seo_meta { text, context? }` returns
|
|
71
|
+
`{ metaTitle, metaDescription, keywords, jsonLd? }`. Read the page/entry, pass its content
|
|
72
|
+
as `text`, then apply the suggestion via the page/entry update tools.
|
|
73
|
+
3. **Programmatic pages** → for a template + a dataset, loop rows: `write_content` per slot →
|
|
74
|
+
`create_entry` → `generate_seo_meta`. Confirm the first row or two first. For hundreds of
|
|
75
|
+
rows, the dashboard AI Page Builder bulk-imports a CSV.
|
|
76
|
+
|
|
77
|
+
Always show suggestions to the user before applying; never invent brand facts.
|
|
78
|
+
|
|
61
79
|
## Errors
|
|
62
80
|
- `401/403` → the MCP key needs (re)authorizing (re-run "Connect your AI").
|
|
63
81
|
- `409 slug_taken` → offer a different slug.
|
package/dist/index.js
CHANGED
|
@@ -438,6 +438,25 @@ function buildToolDefs(deps) {
|
|
|
438
438
|
metaTitle: z.string().optional().describe("SEO meta title"),
|
|
439
439
|
metaDescription: z.string().optional().describe("SEO meta description")
|
|
440
440
|
});
|
|
441
|
+
const writeContentInput = z.object({
|
|
442
|
+
action: z.enum(["write", "rewrite", "translate"]).describe("'write' = draft from a brief, 'rewrite' = improve existing copy, 'translate' = needs targetLang"),
|
|
443
|
+
text: z.string().min(1).describe("the source text (a brief for 'write', the copy to change otherwise)"),
|
|
444
|
+
instructions: z.string().optional().describe("optional extra guidance, e.g. 'make it punchier'"),
|
|
445
|
+
targetLang: z.string().optional().describe("required for 'translate', e.g. 'Spanish'"),
|
|
446
|
+
context: z.string().optional().describe("optional surrounding context, e.g. the page title")
|
|
447
|
+
});
|
|
448
|
+
const generateSeoMetaInput = z.object({
|
|
449
|
+
text: z.string().min(1).describe("the content to derive SEO metadata from"),
|
|
450
|
+
context: z.string().optional().describe("optional surrounding context, e.g. the page slug")
|
|
451
|
+
});
|
|
452
|
+
const createModelInput = z.object({
|
|
453
|
+
name: z.string().min(1).describe("human model name, e.g. 'Blog Post'"),
|
|
454
|
+
slug: slug.describe("url-safe unique slug, e.g. 'blog-post'"),
|
|
455
|
+
description: z.string().optional(),
|
|
456
|
+
fields: z.array(fieldObject).optional().describe(
|
|
457
|
+
"the model's typed schema fields. 'group'/'repeater' NEST their child fields (any depth) \u2014 don't flatten zones into top-level fields."
|
|
458
|
+
)
|
|
459
|
+
});
|
|
441
460
|
const addFieldInput = z.object({
|
|
442
461
|
modelId: z.string().min(1).describe("id of the content model to extend"),
|
|
443
462
|
...fieldShape
|
|
@@ -488,7 +507,7 @@ function buildToolDefs(deps) {
|
|
|
488
507
|
pageId: z.string().min(1).describe("id of the page to delete (from list_pages)")
|
|
489
508
|
});
|
|
490
509
|
const deleteEntryInput = z.object({
|
|
491
|
-
entryId: z.string().min(1).describe("id of the content entry to delete (from
|
|
510
|
+
entryId: z.string().min(1).describe("id of the content entry to delete (from list_content_entries)")
|
|
492
511
|
});
|
|
493
512
|
const deleteModelInput = z.object({
|
|
494
513
|
modelId: z.string().min(1).describe("id of the content model to delete (from list_content_models)")
|
|
@@ -578,6 +597,266 @@ function buildToolDefs(deps) {
|
|
|
578
597
|
const getComponentInput = z.object({
|
|
579
598
|
componentId: z.string().min(1).describe("component id (from list_components)")
|
|
580
599
|
});
|
|
600
|
+
function lifecycleTools() {
|
|
601
|
+
const def = (name, title, description, shape, run) => ({
|
|
602
|
+
name,
|
|
603
|
+
config: { title, description, inputSchema: shape },
|
|
604
|
+
handler: guard(async (args) => withClient((client) => run(client, args)))
|
|
605
|
+
});
|
|
606
|
+
const q = (obj) => {
|
|
607
|
+
const p = new URLSearchParams();
|
|
608
|
+
for (const [k, v] of Object.entries(obj)) if (v !== void 0 && v !== null) p.set(k, String(v));
|
|
609
|
+
const s2 = p.toString();
|
|
610
|
+
return s2 ? `?${s2}` : "";
|
|
611
|
+
};
|
|
612
|
+
const data = async (client, method, path, body) => (await client.fetchJSON(client.url(path), {
|
|
613
|
+
method,
|
|
614
|
+
...body !== void 0 ? { body: JSON.stringify(body), headers: { "content-type": "application/json" } } : {}
|
|
615
|
+
})).data;
|
|
616
|
+
const s = (v) => v;
|
|
617
|
+
const raw = async (client, path, base64, mimeType) => (await client.fetchJSON(client.url(path), {
|
|
618
|
+
method: "POST",
|
|
619
|
+
body: Buffer.from(base64, "base64"),
|
|
620
|
+
headers: { "content-type": mimeType ?? "application/octet-stream" }
|
|
621
|
+
})).data;
|
|
622
|
+
return [
|
|
623
|
+
def(
|
|
624
|
+
"list_media",
|
|
625
|
+
"List media assets",
|
|
626
|
+
"List the images/assets already in the connected project's Media Library (id, url, filename, mimeType, size, alt/caption). Reuse an existing asset instead of re-uploading. Filter with `search` or `type` ('image'|'video'|\u2026).",
|
|
627
|
+
z.object({ search: z.string().optional(), type: z.string().optional(), limit: z.number().optional(), page: z.number().optional() }).shape,
|
|
628
|
+
async (c, a) => ok("Media assets.", await data(c, "GET", `/management/media${q({ search: a.search, type: a.type, limit: a.limit, page: a.page })}`))
|
|
629
|
+
),
|
|
630
|
+
def(
|
|
631
|
+
"get_media",
|
|
632
|
+
"Get a media asset",
|
|
633
|
+
"Get one media asset by id \u2014 its CDN url, filename, MIME type, size, and alt/caption. Use the url as the value of an 'image' field.",
|
|
634
|
+
z.object({ assetId: z.string().min(1).describe("media asset id (from list_media / upload_asset)") }).shape,
|
|
635
|
+
async (c, a) => ok("Media asset.", await data(c, "GET", `/management/media/${s(a.assetId)}`))
|
|
636
|
+
),
|
|
637
|
+
def(
|
|
638
|
+
"delete_media",
|
|
639
|
+
"Delete a media asset",
|
|
640
|
+
"Delete a media asset from the Media Library (soft delete \u2014 reversible from the dashboard trash). Provide assetId.",
|
|
641
|
+
z.object({ assetId: z.string().min(1).describe("media asset id (from list_media)") }).shape,
|
|
642
|
+
async (c, a) => ok("Deleted media asset.", await data(c, "DELETE", `/management/media/${s(a.assetId)}`))
|
|
643
|
+
),
|
|
644
|
+
def(
|
|
645
|
+
"delete_form",
|
|
646
|
+
"Delete a form",
|
|
647
|
+
"Delete a form by id. Soft delete \u2014 the form stops rendering and accepting submissions, but leads already collected against it are preserved. Use it to clean up forms created by mistake.",
|
|
648
|
+
z.object({ formId: z.string().min(1).describe("form id (from list_forms)") }).shape,
|
|
649
|
+
async (c, a) => ok("Deleted form.", await data(c, "DELETE", `/management/forms/${s(a.formId)}`))
|
|
650
|
+
),
|
|
651
|
+
def(
|
|
652
|
+
"list_form_submissions",
|
|
653
|
+
"List a form's submissions (leads)",
|
|
654
|
+
"List the SUBMISSIONS (leads) a form has received \u2014 each with its submitted field values. Filter with status ('inbox'|'spam') and paginate with limit/page.",
|
|
655
|
+
z.object({ formId: z.string().min(1), status: z.enum(["inbox", "spam"]).optional(), limit: z.number().optional(), page: z.number().optional() }).shape,
|
|
656
|
+
async (c, a) => ok("Form submissions.", await data(c, "GET", `/management/forms/${s(a.formId)}/submissions${q({ status: a.status, limit: a.limit, page: a.page })}`))
|
|
657
|
+
),
|
|
658
|
+
def(
|
|
659
|
+
"delete_form_submission",
|
|
660
|
+
"Delete a form submission",
|
|
661
|
+
"Delete one form submission (lead) \u2014 e.g. to clear spam. Provide formId and submissionId (from list_form_submissions).",
|
|
662
|
+
z.object({ formId: z.string().min(1), submissionId: z.string().min(1) }).shape,
|
|
663
|
+
async (c, a) => ok("Deleted submission.", await data(c, "DELETE", `/management/forms/${s(a.formId)}/submissions/${s(a.submissionId)}`))
|
|
664
|
+
),
|
|
665
|
+
def(
|
|
666
|
+
"list_redirects",
|
|
667
|
+
"List redirects",
|
|
668
|
+
"List the URL redirects configured for the connected project (source path \u2192 destination, type).",
|
|
669
|
+
z.object({}).shape,
|
|
670
|
+
async (c) => ok("Redirects.", await data(c, "GET", `/management/redirects`))
|
|
671
|
+
),
|
|
672
|
+
def(
|
|
673
|
+
"update_redirect",
|
|
674
|
+
"Update a redirect",
|
|
675
|
+
"Update an existing redirect in place \u2014 change where it points, its HTTP status, or disable it without deleting. Prefer this over delete+create: it keeps the redirect's id and preserves the chain-collapse rewrites of other rules pointing at it. Only provided fields change. Loops 422, duplicate sources 409.",
|
|
676
|
+
z.object({ redirectId: z.string().min(1).describe("redirect id (from list_redirects)"), sourcePath: z.string().min(1).optional(), destination: z.string().min(1).optional(), redirectType: z.enum(["301", "302", "307", "308"]).optional(), isActive: z.boolean().optional().describe("set false to disable without deleting") }).shape,
|
|
677
|
+
async (c, a) => ok("Updated redirect.", await data(c, "PATCH", `/management/redirects/${s(a.redirectId)}`, { sourcePath: a.sourcePath, destination: a.destination, redirectType: a.redirectType, isActive: a.isActive }))
|
|
678
|
+
),
|
|
679
|
+
def(
|
|
680
|
+
"create_redirect",
|
|
681
|
+
"Create a redirect",
|
|
682
|
+
"Create a URL redirect \u2014 e.g. a 301 after renaming a page's slug. `sourcePath` is the path to redirect FROM ('/old-page'), `destination` the path/url TO ('/new-page'). Chains collapse; loops/dupes rejected. Defaults to 301.",
|
|
683
|
+
z.object({ sourcePath: z.string().min(1), destination: z.string().min(1), redirectType: z.enum(["301", "302", "307", "308"]).optional() }).shape,
|
|
684
|
+
async (c, a) => ok("Created redirect.", await data(c, "POST", `/management/redirects`, { sourcePath: a.sourcePath, destination: a.destination, redirectType: a.redirectType }))
|
|
685
|
+
),
|
|
686
|
+
def(
|
|
687
|
+
"delete_redirect",
|
|
688
|
+
"Delete a redirect",
|
|
689
|
+
"Delete a URL redirect by id (from list_redirects). Provide redirectId.",
|
|
690
|
+
z.object({ redirectId: z.string().min(1) }).shape,
|
|
691
|
+
async (c, a) => ok("Deleted redirect.", await data(c, "DELETE", `/management/redirects/${s(a.redirectId)}`))
|
|
692
|
+
),
|
|
693
|
+
def(
|
|
694
|
+
"get_seo",
|
|
695
|
+
"Get site SEO",
|
|
696
|
+
"Get the connected project's site-wide SEO settings \u2014 default meta (title/description/ogImage), JSON-LD siteSchema, and robots/sitemap/rss config.",
|
|
697
|
+
z.object({}).shape,
|
|
698
|
+
async (c) => ok("SEO settings.", await data(c, "GET", `/management/seo`))
|
|
699
|
+
),
|
|
700
|
+
def(
|
|
701
|
+
"update_seo",
|
|
702
|
+
"Update site SEO",
|
|
703
|
+
"Set site-wide SEO defaults. `seoDefaults` { metaTitle, metaDescription, ogImage, twitterHandle } applies to every page unless overridden. Optionally robotsConfig / sitemapConfig / rssConfig. Each field is REPLACED whole. Rebuilds the site.",
|
|
704
|
+
z.object({ seoDefaults: z.record(z.string(), z.unknown()).optional(), siteSchema: z.record(z.string(), z.unknown()).optional(), robotsConfig: z.record(z.string(), z.unknown()).optional(), sitemapConfig: z.record(z.string(), z.unknown()).optional(), rssConfig: z.record(z.string(), z.unknown()).optional() }).shape,
|
|
705
|
+
async (c, a) => ok("Updated SEO.", await data(c, "PATCH", `/management/seo`, a))
|
|
706
|
+
),
|
|
707
|
+
def(
|
|
708
|
+
"get_site_files",
|
|
709
|
+
"List site files",
|
|
710
|
+
"List the AI-crawler files installed on the connected project (llms.txt / llms-full.txt) \u2014 metadata only.",
|
|
711
|
+
z.object({}).shape,
|
|
712
|
+
async (c) => ok("Site files.", await data(c, "GET", `/management/site-files`))
|
|
713
|
+
),
|
|
714
|
+
def(
|
|
715
|
+
"set_site_file",
|
|
716
|
+
"Set a site file",
|
|
717
|
+
"Create or replace an AI-crawler file served at the site root \u2014 `kind` 'llms.txt' or 'llms-full.txt' \u2014 with `content` (plain text, max 5 MB). Rebuilds the site.",
|
|
718
|
+
z.object({ kind: z.enum(["llms.txt", "llms-full.txt"]), content: z.string() }).shape,
|
|
719
|
+
async (c, a) => ok("Saved site file.", await data(c, "PUT", `/management/site-files/${s(a.kind)}`, { content: a.content }))
|
|
720
|
+
),
|
|
721
|
+
def(
|
|
722
|
+
"delete_site_file",
|
|
723
|
+
"Delete a site file",
|
|
724
|
+
"Remove an AI-crawler file (llms.txt / llms-full.txt) from the connected project. Provide kind.",
|
|
725
|
+
z.object({ kind: z.enum(["llms.txt", "llms-full.txt"]) }).shape,
|
|
726
|
+
async (c, a) => ok("Deleted site file.", await data(c, "DELETE", `/management/site-files/${s(a.kind)}`))
|
|
727
|
+
),
|
|
728
|
+
def(
|
|
729
|
+
"promote_project",
|
|
730
|
+
"Promote staging \u2192 production",
|
|
731
|
+
"Promote the connected project's STAGED build to PRODUCTION \u2014 the one-click publish. Flips prod to the newest staged release (no rebuild) after the QA scan passes. Managed hosting only; needs a publish-enabled connection.",
|
|
732
|
+
z.object({}).shape,
|
|
733
|
+
async (c) => ok("Promoted to production.", await data(c, "POST", `/management/projects/promote`))
|
|
734
|
+
),
|
|
735
|
+
def(
|
|
736
|
+
"list_entry_versions",
|
|
737
|
+
"List a content entry's versions",
|
|
738
|
+
"List a content entry's version history (newest first) \u2014 each version's number, data snapshot, and when it was saved. Use it to find a past state to restore.",
|
|
739
|
+
z.object({ entryId: z.string().min(1) }).shape,
|
|
740
|
+
async (c, a) => ok("Entry versions.", await data(c, "GET", `/management/content/entries/${s(a.entryId)}/versions`))
|
|
741
|
+
),
|
|
742
|
+
def(
|
|
743
|
+
"restore_entry_version",
|
|
744
|
+
"Restore a content entry to a past version",
|
|
745
|
+
"Restore a content entry to a past version (undo). Copies that version's data back as the current DRAFT (non-destructive). Publish afterwards to take it live. Provide entryId and the version number (from list_entry_versions).",
|
|
746
|
+
z.object({ entryId: z.string().min(1), version: z.number().int().positive() }).shape,
|
|
747
|
+
async (c, a) => ok("Restored entry version.", await data(c, "POST", `/management/content/entries/${s(a.entryId)}/versions/${a.version}/restore`))
|
|
748
|
+
),
|
|
749
|
+
// ── Project / workspace (parity with remote /mcp) ──
|
|
750
|
+
def(
|
|
751
|
+
"get_project",
|
|
752
|
+
"Get the connected project",
|
|
753
|
+
"Get the connected project's info \u2014 id, name, slug, subdomain, and its live URL (https://<handle>.bettercms.site). Use it to tell the user where their site is published / link the result.",
|
|
754
|
+
z.object({}).shape,
|
|
755
|
+
async (c) => ok("Project.", await data(c, "GET", `/management/projects/current`))
|
|
756
|
+
),
|
|
757
|
+
def(
|
|
758
|
+
"list_projects",
|
|
759
|
+
"List projects",
|
|
760
|
+
"List the projects this key can see \u2014 id, name, slug, and live URL each. A project-scoped key sees only its own project; a workspace-level key sees every project in the workspace. Use it to find a project you created earlier, or to confirm which sites exist before acting.",
|
|
761
|
+
z.object({}).shape,
|
|
762
|
+
async (c) => ok("Projects.", await data(c, "GET", `/management/projects`))
|
|
763
|
+
),
|
|
764
|
+
def(
|
|
765
|
+
"update_project",
|
|
766
|
+
"Update the connected project",
|
|
767
|
+
"Update the connected project's settings \u2014 rename it, change its slug/description, SEO defaults, or visibility. Only the provided fields change.",
|
|
768
|
+
z.object({ name: z.string().optional(), slug: z.string().optional(), description: z.string().optional(), visibility: z.string().optional(), seoDefaults: z.record(z.string(), z.unknown()).optional() }).shape,
|
|
769
|
+
async (c, a) => ok("Updated project.", await data(c, "PATCH", `/management/projects/current`, a))
|
|
770
|
+
),
|
|
771
|
+
def(
|
|
772
|
+
"create_project",
|
|
773
|
+
"Create a new project",
|
|
774
|
+
"Create a NEW project (site) in the connected workspace. Blank by default; pass templateId to seed curated content, or framework ('next'|'astro') to record the starter. Returns the new project's id and slug. (Use clone_project instead to duplicate an existing project.)",
|
|
775
|
+
z.object({ name: z.string().min(1), slug: z.string().optional(), description: z.string().optional(), templateId: z.string().optional(), framework: z.enum(["next", "astro"]).optional() }).shape,
|
|
776
|
+
async (c, a) => ok("Created project.", await data(c, "POST", `/management/projects`, a))
|
|
777
|
+
),
|
|
778
|
+
def(
|
|
779
|
+
"clone_project",
|
|
780
|
+
"Clone a project",
|
|
781
|
+
"Clone (duplicate) a project as a reusable template into the connected workspace. Copies pages, content models + entries, components, media, forms, and SEO/custom-code settings; excludes submissions, analytics, domains, and secrets. Returns the new project's id and slug. Omit sourceProjectId to clone the connected project.",
|
|
782
|
+
z.object({ sourceProjectId: z.string().optional(), name: z.string().optional(), slug: z.string().optional() }).shape,
|
|
783
|
+
async (c, a) => ok("Cloned project.", await data(c, "POST", `/management/projects/clone`, a))
|
|
784
|
+
),
|
|
785
|
+
def(
|
|
786
|
+
"create_template",
|
|
787
|
+
"Save a project/page as a template",
|
|
788
|
+
"Save a project (or one page) as a REUSABLE template \u2014 a frozen snapshot of its content models, pages, entries, components, and forms (secrets/domains/analytics excluded). Later seed a new project from it with create_project { templateId }. Set visibility 'public' to list it in the cross-workspace gallery. Returns the new template's id.",
|
|
789
|
+
z.object({ sourceProjectId: z.string().min(1), name: z.string().min(1), scope: z.string().optional(), sourcePageId: z.string().optional(), description: z.string().optional(), visibility: z.string().optional() }).shape,
|
|
790
|
+
async (c, a) => ok("Created template.", await data(c, "POST", `/management/templates`, a))
|
|
791
|
+
),
|
|
792
|
+
def(
|
|
793
|
+
"list_templates",
|
|
794
|
+
"List saved templates",
|
|
795
|
+
"List your workspace's saved templates (id, name, scope, visibility). Use a template's id as create_project { templateId } to seed a new project from it.",
|
|
796
|
+
z.object({}).shape,
|
|
797
|
+
async (c) => ok("Templates.", await data(c, "GET", `/management/templates`))
|
|
798
|
+
),
|
|
799
|
+
// ── Content models (read/metadata; parity with remote /mcp) ──
|
|
800
|
+
def(
|
|
801
|
+
"list_content_models",
|
|
802
|
+
"List content models",
|
|
803
|
+
"List the content models (reusable schemas for dynamic collections like Blog/Products) in the connected project.",
|
|
804
|
+
z.object({}).shape,
|
|
805
|
+
async (c) => ok("Content models.", await data(c, "GET", `/management/content/models`))
|
|
806
|
+
),
|
|
807
|
+
def(
|
|
808
|
+
"get_content_model",
|
|
809
|
+
"Get a content model",
|
|
810
|
+
"Get one content model by id INCLUDING its full field schema (keys, types, nested group/repeater children). Read this before add_field so you know the existing keys.",
|
|
811
|
+
z.object({ modelId: z.string().min(1) }).shape,
|
|
812
|
+
async (c, a) => ok("Content model.", await data(c, "GET", `/management/content/models/${s(a.modelId)}`))
|
|
813
|
+
),
|
|
814
|
+
def(
|
|
815
|
+
"update_content_model",
|
|
816
|
+
"Update a content model's metadata",
|
|
817
|
+
"Rename a content model or edit its description/slug (metadata only \u2014 does NOT touch fields; use add_field to extend the schema). Provide modelId plus the fields to change.",
|
|
818
|
+
z.object({ modelId: z.string().min(1), name: z.string().optional(), slug: z.string().optional(), description: z.string().optional() }).shape,
|
|
819
|
+
async (c, a) => ok("Updated content model.", await data(c, "PATCH", `/management/content/models/${s(a.modelId)}`, { name: a.name, slug: a.slug, description: a.description }))
|
|
820
|
+
),
|
|
821
|
+
def(
|
|
822
|
+
"get_content_types",
|
|
823
|
+
"Get generated TypeScript types",
|
|
824
|
+
"Get the auto-generated TypeScript types for the connected project's content models/pages. Pull these to write correctly-typed code against the BetterCMS delivery SDK in the user's site.",
|
|
825
|
+
z.object({}).shape,
|
|
826
|
+
async (c) => ok("Content types.", await data(c, "GET", `/management/content/types`))
|
|
827
|
+
),
|
|
828
|
+
// ── Pages (metadata edit; parity with remote /mcp) ──
|
|
829
|
+
def(
|
|
830
|
+
"update_page",
|
|
831
|
+
"Edit a page's metadata",
|
|
832
|
+
"Edit a page's metadata \u2014 title, slug, SEO metaTitle/metaDescription, and publish status (draft|published). This is the slug/title/SEO editor (it does NOT change the field schema or content \u2014 use add_page_field / set_page_content for those). Renaming the slug keeps the page's content intact. Provide pageId plus the fields to change.",
|
|
833
|
+
z.object({ pageId: z.string().min(1), title: z.string().optional(), slug: z.string().optional(), metaTitle: z.string().optional(), metaDescription: z.string().optional(), status: z.enum(["draft", "published"]).optional() }).shape,
|
|
834
|
+
async (c, a) => ok("Updated page.", await data(c, "PATCH", `/management/pages/${s(a.pageId)}/meta`, { title: a.title, slug: a.slug, metaTitle: a.metaTitle, metaDescription: a.metaDescription, status: a.status }))
|
|
835
|
+
),
|
|
836
|
+
// ── Code + deploy (parity with remote /mcp; needs artifact:write) ──
|
|
837
|
+
def(
|
|
838
|
+
"pull_project_source",
|
|
839
|
+
"Pull the project's live source",
|
|
840
|
+
"Get the connected project's CURRENT live source/build so you can edit it locally. Returns a presigned tarball download url (1h) + the live commit sha \u2014 download it, extract, edit the files, then call deploy_project. If the project is connected to a GitHub repo, returns `github: {owner, repo}` so you can `git clone` that instead.",
|
|
841
|
+
z.object({}).shape,
|
|
842
|
+
async (c) => ok("Project source.", await data(c, "GET", `/management/projects/source`))
|
|
843
|
+
),
|
|
844
|
+
def(
|
|
845
|
+
"deploy_project",
|
|
846
|
+
"Deploy new source/build",
|
|
847
|
+
"Deploy new source/build for the connected project and make it live at its <handle>.bettercms.site. Pass a .tgz or .zip of the project as a base64 string in `data`: SOURCE (has package.json) is built server-side in an isolated sandbox; a prebuilt static site is served as-is. node_modules/.git are stripped automatically. Returns the release id + sha \u2014 then poll get_deploy_status until it is live.",
|
|
848
|
+
z.object({ data: z.string().min(1).describe("base64 .tgz/.zip of the project"), mimeType: z.string().optional() }).shape,
|
|
849
|
+
async (c, a) => ok("Deploy queued.", await raw(c, `/management/projects/deploy`, s(a.data), a.mimeType))
|
|
850
|
+
),
|
|
851
|
+
def(
|
|
852
|
+
"get_deploy_status",
|
|
853
|
+
"Get deploy/build status",
|
|
854
|
+
"Get the connected project's deploy/build status: state (idle|queued|building|failed), whether it's publishing, the live commit sha, when it went live, and any build error. Poll this after deploy_project until state is idle with your sha live.",
|
|
855
|
+
z.object({}).shape,
|
|
856
|
+
async (c) => ok("Deploy status.", await data(c, "GET", `/management/projects/deploy-status`))
|
|
857
|
+
)
|
|
858
|
+
];
|
|
859
|
+
}
|
|
581
860
|
const defs = [
|
|
582
861
|
{
|
|
583
862
|
name: "list_pages",
|
|
@@ -644,6 +923,28 @@ function buildToolDefs(deps) {
|
|
|
644
923
|
})
|
|
645
924
|
)
|
|
646
925
|
},
|
|
926
|
+
{
|
|
927
|
+
name: "create_content_model",
|
|
928
|
+
config: {
|
|
929
|
+
title: "Create a content model (reusable schema)",
|
|
930
|
+
description: "Create a content model \u2014 a reusable schema for a dynamic collection (Blog, Products, Testimonials). `fields` may NEST: type 'group' = one nested object of child fields; type 'repeater' = a repeatable array of child objects. Put child fields in each group/repeater's own `fields` (any depth).",
|
|
931
|
+
inputSchema: createModelInput.shape
|
|
932
|
+
},
|
|
933
|
+
handler: guard(
|
|
934
|
+
async (args) => withClient(async (client) => {
|
|
935
|
+
const model = await client.createModel({
|
|
936
|
+
name: args.name,
|
|
937
|
+
slug: args.slug,
|
|
938
|
+
...args.description !== void 0 ? { description: args.description } : {},
|
|
939
|
+
fields: toFields(args.fields)
|
|
940
|
+
});
|
|
941
|
+
return ok(
|
|
942
|
+
`Created content model '${model.name}' (${model.fields.length} field(s)).`,
|
|
943
|
+
model
|
|
944
|
+
);
|
|
945
|
+
})
|
|
946
|
+
)
|
|
947
|
+
},
|
|
647
948
|
{
|
|
648
949
|
name: "add_field",
|
|
649
950
|
config: {
|
|
@@ -685,23 +986,20 @@ function buildToolDefs(deps) {
|
|
|
685
986
|
)
|
|
686
987
|
},
|
|
687
988
|
{
|
|
688
|
-
name: "
|
|
989
|
+
name: "create_content_entry",
|
|
689
990
|
config: {
|
|
690
991
|
title: "Create a content entry",
|
|
691
|
-
description: "Create a content entry under a model.
|
|
992
|
+
description: "Create a content entry under a model. Pass its field VALUES in `data`, keyed by field key \u2014 INCLUDE ALL REQUIRED FIELDS (create validates them). New entries are drafts; pass status:'published' to take it live. Read get_content_model first for the field keys and which are required.",
|
|
692
993
|
inputSchema: createEntryInput.shape
|
|
693
994
|
},
|
|
694
995
|
handler: guard(
|
|
695
996
|
async (args) => withClient(async (client) => {
|
|
696
997
|
const created = await client.createEntry({
|
|
697
998
|
contentModelId: args.contentModelId,
|
|
698
|
-
...args.slug !== void 0 ? { slug: args.slug } : {}
|
|
999
|
+
...args.slug !== void 0 ? { slug: args.slug } : {},
|
|
1000
|
+
...args.data !== void 0 ? { data: args.data } : {}
|
|
699
1001
|
});
|
|
700
|
-
const
|
|
701
|
-
const entry = needsUpdate ? await client.updateEntry(created.id, {
|
|
702
|
-
...args.data !== void 0 ? { data: args.data } : {},
|
|
703
|
-
...args.status !== void 0 ? { status: args.status } : {}
|
|
704
|
-
}) : created;
|
|
1002
|
+
const entry = args.status !== void 0 && args.status !== "draft" ? await client.updateEntry(created.id, { status: args.status }) : created;
|
|
705
1003
|
return ok(
|
|
706
1004
|
`Created entry '${entry.slug}' (id ${entry.id}, status ${entry.status}).`,
|
|
707
1005
|
entry
|
|
@@ -713,7 +1011,7 @@ function buildToolDefs(deps) {
|
|
|
713
1011
|
name: "set_page_content",
|
|
714
1012
|
config: {
|
|
715
1013
|
title: "Set a page's field values (content)",
|
|
716
|
-
description: "Set a page's field VALUES \u2014 the actual content. For a SINGLETON page (Home, About, Site Settings) this creates or updates its one entry, so call it again to edit. `data` is keyed by field key: a nested 'array' (zone) value is an OBJECT { nonRepeatable: { childKey: value }, repeatable: [ { childKey: value } ] }; a primitive 'array' is a plain list; an 'image' value is an asset URL. Read the schema first with get_page. This is how you populate Home/About/Settings \u2014
|
|
1014
|
+
description: "Set a page's field VALUES \u2014 the actual content. For a SINGLETON page (Home, About, Site Settings) this creates or updates its one entry, so call it again to edit. `data` is keyed by field key: a nested 'array' (zone) value is an OBJECT { nonRepeatable: { childKey: value }, repeatable: [ { childKey: value } ] }; a primitive 'array' is a plain list; an 'image' value is an asset URL. Read the schema first with get_page. This is how you populate Home/About/Settings \u2014 create_content_entry is for dynamic collections only.",
|
|
717
1015
|
inputSchema: setPageContentInput.shape
|
|
718
1016
|
},
|
|
719
1017
|
handler: guard(
|
|
@@ -730,7 +1028,7 @@ function buildToolDefs(deps) {
|
|
|
730
1028
|
)
|
|
731
1029
|
},
|
|
732
1030
|
{
|
|
733
|
-
name: "
|
|
1031
|
+
name: "list_content_entries",
|
|
734
1032
|
config: {
|
|
735
1033
|
title: "List content entries (incl. drafts)",
|
|
736
1034
|
description: "List content entries \u2014 including drafts \u2014 filtered by model and/or page. Use it to SEE existing content before editing. For a singleton page, pass its pageId to get its single entry.",
|
|
@@ -748,7 +1046,7 @@ function buildToolDefs(deps) {
|
|
|
748
1046
|
)
|
|
749
1047
|
},
|
|
750
1048
|
{
|
|
751
|
-
name: "
|
|
1049
|
+
name: "get_content_entry",
|
|
752
1050
|
config: {
|
|
753
1051
|
title: "Get a content entry (with its values)",
|
|
754
1052
|
description: "Get one content entry by id INCLUDING its `data` (field values), even when draft.",
|
|
@@ -762,7 +1060,7 @@ function buildToolDefs(deps) {
|
|
|
762
1060
|
)
|
|
763
1061
|
},
|
|
764
1062
|
{
|
|
765
|
-
name: "
|
|
1063
|
+
name: "update_content_entry",
|
|
766
1064
|
config: {
|
|
767
1065
|
title: "Update a content entry's values",
|
|
768
1066
|
description: "Update a content entry's `data` (field values) and/or status by id. `data` is keyed by field key; a nested 'array' (zone) value is an object { nonRepeatable: {\u2026}, repeatable: [{\u2026}] }, a primitive 'array' is a plain list. Use this to edit an existing entry; for a singleton page prefer set_page_content.",
|
|
@@ -811,7 +1109,7 @@ function buildToolDefs(deps) {
|
|
|
811
1109
|
)
|
|
812
1110
|
},
|
|
813
1111
|
{
|
|
814
|
-
name: "
|
|
1112
|
+
name: "delete_content_entry",
|
|
815
1113
|
config: {
|
|
816
1114
|
title: "Delete a content entry",
|
|
817
1115
|
description: "DELETE a single content entry. Destructive but REVERSIBLE (soft-delete, restorable from the dashboard) and audit-logged. Use it to remove content created by mistake.",
|
|
@@ -959,7 +1257,41 @@ function buildToolDefs(deps) {
|
|
|
959
1257
|
return ok(`Updated component '${cmp.name}' (id ${cmp.id}).`, cmp);
|
|
960
1258
|
})
|
|
961
1259
|
)
|
|
962
|
-
}
|
|
1260
|
+
},
|
|
1261
|
+
// ── AI content + SEO actions (Option B) ────────────────────────────────────
|
|
1262
|
+
{
|
|
1263
|
+
name: "write_content",
|
|
1264
|
+
config: {
|
|
1265
|
+
title: "Write, rewrite, or translate content",
|
|
1266
|
+
description: "AI-write a piece of copy: action 'write' (draft from a brief), 'rewrite' (improve existing copy), or 'translate' (needs targetLang). Returns the suggested text \u2014 apply it with set_page_content or update_content_entry. Uses the workspace's own Anthropic key (BYOK, unmetered) or platform AI credits.",
|
|
1267
|
+
inputSchema: writeContentInput.shape
|
|
1268
|
+
},
|
|
1269
|
+
handler: guard(
|
|
1270
|
+
async (args) => withClient(async (client) => {
|
|
1271
|
+
const suggestion = await client.writeContent(args);
|
|
1272
|
+
return ok(`Generated ${args.action} suggestion (${suggestion.length} chars).`, { suggestion });
|
|
1273
|
+
})
|
|
1274
|
+
)
|
|
1275
|
+
},
|
|
1276
|
+
{
|
|
1277
|
+
name: "generate_seo_meta",
|
|
1278
|
+
config: {
|
|
1279
|
+
title: "Generate SEO metadata",
|
|
1280
|
+
description: "Generate SEO metadata (metaTitle, metaDescription, keywords, optional JSON-LD) for a piece of content. Pass the page/entry content as `text`. Returns a suggestion to apply via update_page or the entry SEO fields.",
|
|
1281
|
+
inputSchema: generateSeoMetaInput.shape
|
|
1282
|
+
},
|
|
1283
|
+
handler: guard(
|
|
1284
|
+
async (args) => withClient(async (client) => {
|
|
1285
|
+
const meta = await client.generateSeoMeta(args);
|
|
1286
|
+
return ok(`Generated SEO metadata: "${meta.metaTitle}".`, meta);
|
|
1287
|
+
})
|
|
1288
|
+
)
|
|
1289
|
+
},
|
|
1290
|
+
// ── Lifecycle tools (parity with the remote /mcp surface) ──────────────────
|
|
1291
|
+
// Media management, form submissions (leads), redirects, SEO, AEO site-files,
|
|
1292
|
+
// promote, and entry version history. These call the management endpoints straight
|
|
1293
|
+
// through the client's request plumbing — no bespoke SDK method per endpoint.
|
|
1294
|
+
...lifecycleTools()
|
|
963
1295
|
];
|
|
964
1296
|
return defs;
|
|
965
1297
|
}
|
|
@@ -1021,9 +1353,9 @@ above, including nested group/repeater), required?. Confirm, then call:
|
|
|
1021
1353
|
- a page \u2192 \`add_page_field\` { pageId, key, label, type, ... }
|
|
1022
1354
|
Both are additive: they reject a key that already exists and never retype/overwrite
|
|
1023
1355
|
an existing field (edit those in the dashboard).`;
|
|
1024
|
-
var ENTRY_FLOW = `### Create an entry \u2192 \`
|
|
1356
|
+
var ENTRY_FLOW = `### Create an entry \u2192 \`create_content_entry\` tool
|
|
1025
1357
|
Create a content entry under a model. Ask: which model (id), the field values (data),
|
|
1026
|
-
status (draft/published). Then call \`
|
|
1358
|
+
status (draft/published). Then call \`create_content_entry\` { contentModelId, data?, status?, slug? }.`;
|
|
1027
1359
|
var FORM_FLOW = `### Form authoring \u2192 \`create_form\` / \`update_form\` tools
|
|
1028
1360
|
Author a form (then the user embeds it with \`<BcmsForm form={getForm('Name')} />\` from
|
|
1029
1361
|
@bettercms-ai/next). Confirm the fields with the user BEFORE creating. Never guess fields.
|
|
@@ -1050,6 +1382,32 @@ confirm. Never guess the layout.
|
|
|
1050
1382
|
4. **Confirm the structure** (AskUserQuestion: show the block tree), then \`create_component\`
|
|
1051
1383
|
{ name, slug, category?, blockJson, props? } (returns the id), or \`update_component\`
|
|
1052
1384
|
{ componentId, ... } \u2014 note updating re-bakes every page that embeds it.`;
|
|
1385
|
+
var BUILD_SITE_FLOW = `### Build a whole site (schema \u2192 pages \u2192 AI copy) \u2192 composes the flows below
|
|
1386
|
+
End-to-end authoring from a repo or a brief. Confirm-first at every stage.
|
|
1387
|
+
1. **Schema** \u2014 run the whole-project schema design above: propose the page/zone/field tree,
|
|
1388
|
+
get the user's approval, then \`create_page\` per page.
|
|
1389
|
+
2. **Copy** \u2014 for each page/entry, draft the real text with \`write_content\` (action 'write',
|
|
1390
|
+
pass the section as the brief + the page title as \`context\`). Review with the user, then
|
|
1391
|
+
apply via \`set_page_content\` / \`create_content_entry\` / \`update_content_entry\`.
|
|
1392
|
+
3. **SEO** \u2014 run the SEO flow to fill metaTitle/metaDescription for each page.
|
|
1393
|
+
Never invent brand facts \u2014 ask the user for anything the repo/brief doesn't state.`;
|
|
1394
|
+
var LANDING_PAGES_FLOW = `### Generate landing pages (programmatic SEO / ABM) \u2192 \`write_content\` + \`create_content_entry\` + \`generate_seo_meta\`
|
|
1395
|
+
Spin up many pages sharing one template, each personalized per row (company, keyword, persona).
|
|
1396
|
+
1. **Template** \u2014 ensure a dynamic page or content model exists for the template
|
|
1397
|
+
(\`create_page\` pageType 'dynamic' / \`create_content_model\`); its fields are the per-page slots.
|
|
1398
|
+
2. **Dataset** \u2014 get the list of targets from the user (rows of variables, e.g. company + industry).
|
|
1399
|
+
3. **Per row (loop)** \u2014 draft each slot with \`write_content\` (the row's variables as the brief/
|
|
1400
|
+
\`context\`), pick a unique slug, then \`create_content_entry\` { contentModelId, data, slug, status }.
|
|
1401
|
+
Add SEO with \`generate_seo_meta\` on the drafted copy and store it on the entry.
|
|
1402
|
+
Confirm the first 1\u20132 rows with the user before generating the rest. For hundreds of rows,
|
|
1403
|
+
the dashboard AI Page Builder bulk-imports a CSV \u2014 mention it.`;
|
|
1404
|
+
var SEO_FLOW = `### Optimize SEO \u2192 \`generate_seo_meta\` + the page/entry update tools
|
|
1405
|
+
Fill or refresh SEO metadata across the site.
|
|
1406
|
+
1. **Target** \u2014 pick the pages/entries (\`list_pages\` / \`list_content_entries\`); confirm scope with the user.
|
|
1407
|
+
2. **Per target** \u2014 read its content (\`get_page\` / \`get_content_entry\`), call \`generate_seo_meta\` with that
|
|
1408
|
+
content as \`text\`, review the suggested metaTitle/metaDescription/keywords, then apply it via
|
|
1409
|
+
the page/entry update tools (or the dashboard SEO fields).
|
|
1410
|
+
Keep titles ~60 chars and descriptions ~155; don't overwrite good existing meta without asking.`;
|
|
1053
1411
|
function registerPrompts(server) {
|
|
1054
1412
|
server.registerPrompt(
|
|
1055
1413
|
"studio",
|
|
@@ -1070,7 +1428,7 @@ function registerPrompts(server) {
|
|
|
1070
1428
|
${request ? `The user's request: "${request}".
|
|
1071
1429
|
` : ""}
|
|
1072
1430
|
First, **preflight**: confirm the bettercms tools are loaded (\`create_page\`, \`add_field\`,
|
|
1073
|
-
\`
|
|
1431
|
+
\`create_content_entry\`). If \`create_page\` is missing and only \`create_model\` shows, the host has a
|
|
1074
1432
|
stale cached MCP \u2014 tell the user to run \`rm -rf ~/.npm/_npx\` and restart, then stop.
|
|
1075
1433
|
|
|
1076
1434
|
Then **route** to the matching sub-flow below based on the request and conversation
|
|
@@ -1095,6 +1453,12 @@ ${FORM_FLOW}
|
|
|
1095
1453
|
|
|
1096
1454
|
${COMPONENT_FLOW}
|
|
1097
1455
|
|
|
1456
|
+
${BUILD_SITE_FLOW}
|
|
1457
|
+
|
|
1458
|
+
${LANDING_PAGES_FLOW}
|
|
1459
|
+
|
|
1460
|
+
${SEO_FLOW}
|
|
1461
|
+
|
|
1098
1462
|
This assistant is extensible: when new BetterCMS tools are added, a new sub-flow appears
|
|
1099
1463
|
here \u2014 route to it the same way.`
|
|
1100
1464
|
}
|
|
@@ -1119,7 +1483,7 @@ here \u2014 route to it the same way.`
|
|
|
1119
1483
|
type: "text",
|
|
1120
1484
|
text: `Design the BetterCMS content schema for this repository.${request ? ` Focus: "${request}".` : ""}
|
|
1121
1485
|
|
|
1122
|
-
Preflight: if \`create_page\` isn't available (only create_model/add_field/
|
|
1486
|
+
Preflight: if \`create_page\` isn't available (only create_model/add_field/create_content_entry),
|
|
1123
1487
|
the host has a stale cached MCP \u2014 tell the user to \`rm -rf ~/.npm/_npx\` and restart, then stop.
|
|
1124
1488
|
|
|
1125
1489
|
${SCHEMA_PROPOSAL_FLOW}
|
|
@@ -1149,7 +1513,7 @@ landed in. On 409 slug_taken, offer an alternative slug; on 401/403, the MCP key
|
|
|
1149
1513
|
type: "text",
|
|
1150
1514
|
text: `Create a BetterCMS page via the \`create_page\` tool.${request ? ` The user wants: "${request}".` : ""}
|
|
1151
1515
|
|
|
1152
|
-
Preflight: if \`create_page\` isn't available (only create_model/add_field/
|
|
1516
|
+
Preflight: if \`create_page\` isn't available (only create_model/add_field/create_content_entry),
|
|
1153
1517
|
the host has a stale cached MCP \u2014 tell the user to \`rm -rf ~/.npm/_npx\` and restart, then stop.
|
|
1154
1518
|
|
|
1155
1519
|
${PAGE_FLOW}
|
|
@@ -1213,11 +1577,88 @@ On 409 slug_taken, offer an alternative slug; on 401/403, the MCP key needs (re)
|
|
|
1213
1577
|
]
|
|
1214
1578
|
})
|
|
1215
1579
|
);
|
|
1580
|
+
server.registerPrompt(
|
|
1581
|
+
"build_site",
|
|
1582
|
+
{
|
|
1583
|
+
title: "Build a site (guided, end-to-end)",
|
|
1584
|
+
description: "Author a whole BetterCMS site from a repo or a brief: design the schema, create the pages, draft the copy with AI (write_content), and fill SEO (generate_seo_meta).",
|
|
1585
|
+
argsSchema: {
|
|
1586
|
+
request: z2.string().optional().describe("what to build, e.g. 'a SaaS marketing site from this repo'")
|
|
1587
|
+
}
|
|
1588
|
+
},
|
|
1589
|
+
({ request }) => ({
|
|
1590
|
+
messages: [
|
|
1591
|
+
{
|
|
1592
|
+
role: "user",
|
|
1593
|
+
content: {
|
|
1594
|
+
type: "text",
|
|
1595
|
+
text: `Build a BetterCMS site end-to-end.${request ? ` The user wants: "${request}".` : ""}
|
|
1596
|
+
|
|
1597
|
+
${BUILD_SITE_FLOW}
|
|
1598
|
+
|
|
1599
|
+
${SEO_FLOW}
|
|
1600
|
+
|
|
1601
|
+
Confirm each stage with the user before writing. On 401/403, the MCP key needs (re)authorizing.`
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
]
|
|
1605
|
+
})
|
|
1606
|
+
);
|
|
1607
|
+
server.registerPrompt(
|
|
1608
|
+
"generate_landing_pages",
|
|
1609
|
+
{
|
|
1610
|
+
title: "Generate landing pages (programmatic SEO / ABM)",
|
|
1611
|
+
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.",
|
|
1612
|
+
argsSchema: {
|
|
1613
|
+
request: z2.string().optional().describe("the campaign, e.g. 'a page per target company for our ABM push'")
|
|
1614
|
+
}
|
|
1615
|
+
},
|
|
1616
|
+
({ request }) => ({
|
|
1617
|
+
messages: [
|
|
1618
|
+
{
|
|
1619
|
+
role: "user",
|
|
1620
|
+
content: {
|
|
1621
|
+
type: "text",
|
|
1622
|
+
text: `Generate programmatic landing pages.${request ? ` The user wants: "${request}".` : ""}
|
|
1623
|
+
|
|
1624
|
+
${LANDING_PAGES_FLOW}
|
|
1625
|
+
|
|
1626
|
+
On 401/403, the MCP key needs (re)authorizing.`
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
]
|
|
1630
|
+
})
|
|
1631
|
+
);
|
|
1632
|
+
server.registerPrompt(
|
|
1633
|
+
"seo_optimize",
|
|
1634
|
+
{
|
|
1635
|
+
title: "Optimize SEO (guided)",
|
|
1636
|
+
description: "Generate and apply SEO metadata (title, description, keywords, JSON-LD) across pages/entries with generate_seo_meta.",
|
|
1637
|
+
argsSchema: {
|
|
1638
|
+
request: z2.string().optional().describe("scope, e.g. 'all blog posts' or 'the home page'")
|
|
1639
|
+
}
|
|
1640
|
+
},
|
|
1641
|
+
({ request }) => ({
|
|
1642
|
+
messages: [
|
|
1643
|
+
{
|
|
1644
|
+
role: "user",
|
|
1645
|
+
content: {
|
|
1646
|
+
type: "text",
|
|
1647
|
+
text: `Optimize SEO metadata.${request ? ` The user wants: "${request}".` : ""}
|
|
1648
|
+
|
|
1649
|
+
${SEO_FLOW}
|
|
1650
|
+
|
|
1651
|
+
On 401/403, the MCP key needs (re)authorizing.`
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
]
|
|
1655
|
+
})
|
|
1656
|
+
);
|
|
1216
1657
|
}
|
|
1217
1658
|
|
|
1218
1659
|
// src/server.ts
|
|
1219
1660
|
var SERVER_NAME = "bettercms";
|
|
1220
|
-
var SERVER_VERSION = "
|
|
1661
|
+
var SERVER_VERSION = "1.3.0";
|
|
1221
1662
|
function buildServer(deps) {
|
|
1222
1663
|
const server = new McpServer(
|
|
1223
1664
|
{ name: SERVER_NAME, version: SERVER_VERSION },
|