@cmo-so/mcp 0.12.1 → 0.12.3
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/cmo-mcp.mjs +416 -2
- package/package.json +1 -1
package/dist/cmo-mcp.mjs
CHANGED
|
@@ -7199,7 +7199,7 @@ var require_dist = __commonJS({
|
|
|
7199
7199
|
// package.json
|
|
7200
7200
|
var package_default = {
|
|
7201
7201
|
name: "@cmo-so/cli",
|
|
7202
|
-
version: "0.12.
|
|
7202
|
+
version: "0.12.3",
|
|
7203
7203
|
description: "CMO.so on the command line: your websites, ideas, blog posts, LinkedIn queue and results, plus a local MCP server for Claude Desktop, Cursor and other MCP clients.",
|
|
7204
7204
|
type: "module",
|
|
7205
7205
|
license: "MIT",
|
|
@@ -7722,6 +7722,8 @@ function domainApi(api2, domain) {
|
|
|
7722
7722
|
wpBuilder: () => api2.request(`${base}/wp/builder`),
|
|
7723
7723
|
connectWpBuilder: (input) => post(`${base}/wp/builder`, input),
|
|
7724
7724
|
disconnectWpBuilder: () => api2.request(`${base}/wp/builder`, { method: "DELETE" }),
|
|
7725
|
+
/** Put one picture in the site's media library: a public https link, or a local file as base64 (≤ 10 MB). */
|
|
7726
|
+
wpMedia: (input) => post(`${base}/wp/media`, input),
|
|
7725
7727
|
/** One WordPress REST request, signed by TOPY with the saved login (allowlisted routes only). */
|
|
7726
7728
|
wpRequest: (input) => post(`${base}/wp/request`, input),
|
|
7727
7729
|
/** A new website API key (aicmo_…) for the user's own code, shown once; for the CLI, which writes it to .env. */
|
|
@@ -14806,6 +14808,23 @@ function apiSite(api2, domain) {
|
|
|
14806
14808
|
return {
|
|
14807
14809
|
url: `https://${domain}`,
|
|
14808
14810
|
fetchPage: publicFetch,
|
|
14811
|
+
async uploadMedia(input) {
|
|
14812
|
+
let data_base64;
|
|
14813
|
+
if (input.data) {
|
|
14814
|
+
let bin = "";
|
|
14815
|
+
for (let i = 0; i < input.data.length; i += 32768)
|
|
14816
|
+
bin += String.fromCharCode(...input.data.subarray(i, i + 32768));
|
|
14817
|
+
data_base64 = btoa(bin);
|
|
14818
|
+
}
|
|
14819
|
+
return d.wpMedia({
|
|
14820
|
+
url: input.url,
|
|
14821
|
+
data_base64,
|
|
14822
|
+
filename: input.filename,
|
|
14823
|
+
alt: input.alt,
|
|
14824
|
+
title: input.title,
|
|
14825
|
+
caption: input.caption
|
|
14826
|
+
});
|
|
14827
|
+
},
|
|
14809
14828
|
async request(req) {
|
|
14810
14829
|
let r;
|
|
14811
14830
|
try {
|
|
@@ -16750,6 +16769,393 @@ var sectionTools = [
|
|
|
16750
16769
|
}
|
|
16751
16770
|
})
|
|
16752
16771
|
];
|
|
16772
|
+
var CHAT_FILE = {
|
|
16773
|
+
type: "object",
|
|
16774
|
+
description: "The picture from this chat (ChatGPT passes it here).",
|
|
16775
|
+
properties: {
|
|
16776
|
+
download_url: { type: "string" },
|
|
16777
|
+
file_id: { type: "string" },
|
|
16778
|
+
mime_type: { type: "string" },
|
|
16779
|
+
file_name: { type: "string" }
|
|
16780
|
+
},
|
|
16781
|
+
required: ["download_url", "file_id"]
|
|
16782
|
+
};
|
|
16783
|
+
var mediaTools = [
|
|
16784
|
+
tool({
|
|
16785
|
+
name: "wp_media_upload",
|
|
16786
|
+
description: "Put a picture in the WordPress site's media library: a picture from this chat (file) or a public https link to an image (url). JPEG, PNG, WebP, GIF or AVIF, up to 15 MB; checked as a real image. Give alt text that says what the picture shows. Returns its id and source_url; then use the url in a section (wp_section_update edits.images, or slots image) or as a page's featured_media. Doesn't change any page by itself.",
|
|
16787
|
+
parameters: {
|
|
16788
|
+
type: "object",
|
|
16789
|
+
properties: {
|
|
16790
|
+
file: CHAT_FILE,
|
|
16791
|
+
url: str2("Public https link to the image file."),
|
|
16792
|
+
filename: str2("File name to save it as (default from the link), e.g. maggie-hero.webp."),
|
|
16793
|
+
alt: str2("Alt text: what the picture shows, in one short sentence."),
|
|
16794
|
+
title: str2("Media title (optional)."),
|
|
16795
|
+
caption: str2("Caption (optional).")
|
|
16796
|
+
}
|
|
16797
|
+
},
|
|
16798
|
+
group: "site",
|
|
16799
|
+
need: WRITE,
|
|
16800
|
+
risk: "W2",
|
|
16801
|
+
gates: [...GATES],
|
|
16802
|
+
fileParams: ["file"],
|
|
16803
|
+
confirm: (a, lang) => {
|
|
16804
|
+
const name = a.file?.file_name ?? (typeof a.url === "string" ? a.url.split("/").pop() : "");
|
|
16805
|
+
return say(
|
|
16806
|
+
lang,
|
|
16807
|
+
`Upload the picture ${q2(name, "")} to your website's media library.`,
|
|
16808
|
+
`\u628A\u5716\u7247 ${q2(name, "")} \u4E0A\u50B3\u5230\u7DB2\u7AD9\u7684\u5A92\u9AD4\u5EAB\u3002`
|
|
16809
|
+
);
|
|
16810
|
+
},
|
|
16811
|
+
run: async (ctx, a) => {
|
|
16812
|
+
const site = siteOf(ctx);
|
|
16813
|
+
if (!site.uploadMedia) throw new Error("This connection can't upload pictures.");
|
|
16814
|
+
const chat = a.file;
|
|
16815
|
+
const url = String(chat?.download_url ?? a.url ?? "").trim();
|
|
16816
|
+
if (!url) throw new Error("Give the picture: a file from this chat, or a public https link (url).");
|
|
16817
|
+
const out = await site.uploadMedia({
|
|
16818
|
+
url,
|
|
16819
|
+
filename: a.filename || chat?.file_name || void 0,
|
|
16820
|
+
alt: a.alt || void 0,
|
|
16821
|
+
title: a.title || void 0,
|
|
16822
|
+
caption: a.caption || void 0
|
|
16823
|
+
});
|
|
16824
|
+
return {
|
|
16825
|
+
uploaded: true,
|
|
16826
|
+
...out,
|
|
16827
|
+
next: "Use source_url in wp_section_update (edits.images from \u2192 to, or slots image), or set it as the page's featured_media with wp_page_update."
|
|
16828
|
+
};
|
|
16829
|
+
}
|
|
16830
|
+
}),
|
|
16831
|
+
tool({
|
|
16832
|
+
name: "wp_media_list",
|
|
16833
|
+
description: "Pictures already in the WordPress media library (newest first), with id, link, alt text and size; search by words in the title or file name.",
|
|
16834
|
+
parameters: {
|
|
16835
|
+
type: "object",
|
|
16836
|
+
properties: {
|
|
16837
|
+
search: str2("Words in the title or file name."),
|
|
16838
|
+
limit: int2("How many (default 30, at most 100).")
|
|
16839
|
+
}
|
|
16840
|
+
},
|
|
16841
|
+
group: "site",
|
|
16842
|
+
need: READ,
|
|
16843
|
+
risk: "R0",
|
|
16844
|
+
gates: ["WP"],
|
|
16845
|
+
run: async (ctx, a) => {
|
|
16846
|
+
const rows = await call(siteOf(ctx), {
|
|
16847
|
+
path: "/wp/v2/media",
|
|
16848
|
+
query: {
|
|
16849
|
+
search: a.search || void 0,
|
|
16850
|
+
per_page: Math.min(100, Math.max(1, Number(a.limit) || 30)),
|
|
16851
|
+
media_type: "image",
|
|
16852
|
+
_fields: "id,source_url,alt_text,mime_type,date,title,media_details"
|
|
16853
|
+
}
|
|
16854
|
+
});
|
|
16855
|
+
return {
|
|
16856
|
+
count: rows.length,
|
|
16857
|
+
items: rows.map((m) => ({
|
|
16858
|
+
id: m.id,
|
|
16859
|
+
url: m.source_url,
|
|
16860
|
+
alt: m.alt_text,
|
|
16861
|
+
type: m.mime_type,
|
|
16862
|
+
size: m.media_details?.width ? `${m.media_details.width}\xD7${m.media_details.height}` : null,
|
|
16863
|
+
date: m.date
|
|
16864
|
+
}))
|
|
16865
|
+
};
|
|
16866
|
+
}
|
|
16867
|
+
})
|
|
16868
|
+
];
|
|
16869
|
+
var childInfo = (site, slug) => call(site, { path: "/maggie-core/v1/child-theme", query: { slug } });
|
|
16870
|
+
async function childSlug(site, slug) {
|
|
16871
|
+
if (typeof slug === "string" && slug) return slug;
|
|
16872
|
+
const info = await childInfo(site);
|
|
16873
|
+
const active = info.children.find((c) => c.active);
|
|
16874
|
+
if (!active)
|
|
16875
|
+
throw new Error("No Maggie child theme is active here: name one (slug), or create one with wp_child_theme_create.");
|
|
16876
|
+
return active.slug;
|
|
16877
|
+
}
|
|
16878
|
+
async function homeShape(site) {
|
|
16879
|
+
if (!site.fetchPage || !site.url) return null;
|
|
16880
|
+
const r = await site.fetchPage(`${site.url}/?maggie-check=${Date.now()}`).catch(() => null);
|
|
16881
|
+
if (!r) return null;
|
|
16882
|
+
const text3 = r.html.replace(/<script[\s\S]*?<\/script>|<style[\s\S]*?<\/style>/g, " ").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
16883
|
+
const fonts = [
|
|
16884
|
+
...new Set([...r.html.matchAll(/font-family:\s*([^;"}]+)/g)].map((m) => m[1].trim()))
|
|
16885
|
+
].sort();
|
|
16886
|
+
const colors = [...r.html.matchAll(/--wp--preset--color--([a-z0-9-]+):\s*([^;]+);/g)].map((m) => `${m[1]}=${m[2].trim()}`).sort();
|
|
16887
|
+
return { status: r.status, words: new Set(text3.toLowerCase().split(" ")), fonts, colors };
|
|
16888
|
+
}
|
|
16889
|
+
function compareShapes(a, b) {
|
|
16890
|
+
if (!a || !b) return { checked: false };
|
|
16891
|
+
const shared = [...a.words].filter((w) => b.words.has(w)).length;
|
|
16892
|
+
const text3 = Math.round(shared / Math.max(1, Math.max(a.words.size, b.words.size)) * 100);
|
|
16893
|
+
const fontsSame = a.fonts.join("|") === b.fonts.join("|");
|
|
16894
|
+
const colorsSame = a.colors.join("|") === b.colors.join("|");
|
|
16895
|
+
const problems = [
|
|
16896
|
+
b.status >= 400 ? `the homepage answers HTTP ${b.status}` : "",
|
|
16897
|
+
text3 < 90 ? `only ${text3}% of the homepage's words are the same` : "",
|
|
16898
|
+
fontsSame ? "" : "the fonts changed",
|
|
16899
|
+
colorsSame ? "" : "the brand colours changed"
|
|
16900
|
+
].filter(Boolean);
|
|
16901
|
+
return {
|
|
16902
|
+
checked: true,
|
|
16903
|
+
same_text_percent: text3,
|
|
16904
|
+
fonts_same: fontsSame,
|
|
16905
|
+
colors_same: colorsSame,
|
|
16906
|
+
ok: !problems.length,
|
|
16907
|
+
problems
|
|
16908
|
+
};
|
|
16909
|
+
}
|
|
16910
|
+
var PATTERN_SCHEMA = obj("A pattern for the editor's inserter.", {
|
|
16911
|
+
slug: str2("File name: lowercase, dashes (e.g. spring-offer)."),
|
|
16912
|
+
title: str2("Name shown in the inserter."),
|
|
16913
|
+
content: str2("Block markup (e.g. a section from wp_section_library written out, or copied from a page)."),
|
|
16914
|
+
description: str2(""),
|
|
16915
|
+
categories: arr("Pattern categories (default maggie).", { type: "string" })
|
|
16916
|
+
});
|
|
16917
|
+
var childTools = [
|
|
16918
|
+
tool({
|
|
16919
|
+
name: "wp_child_theme_status",
|
|
16920
|
+
description: "The site's Maggie child themes (Maggie Core is the parent): which is active, their files and backups, and whether files can be written on this host. With slug: that child's text files in full (theme.json, style.css, parts, patterns) to read before changing them.",
|
|
16921
|
+
parameters: { type: "object", properties: { slug: str2("A child theme to read in full, e.g. maggie-moonlake.") } },
|
|
16922
|
+
group: "site",
|
|
16923
|
+
need: READ,
|
|
16924
|
+
risk: "R0",
|
|
16925
|
+
gates: ["WP"],
|
|
16926
|
+
run: async (ctx, a) => {
|
|
16927
|
+
const info = await childInfo(siteOf(ctx), a.slug || void 0);
|
|
16928
|
+
const files = info.files ? Object.fromEntries(
|
|
16929
|
+
Object.entries(info.files).map(([k, v]) => [k, v.length > 2e4 ? `${v.slice(0, 2e4)}\u2026` : v])
|
|
16930
|
+
) : void 0;
|
|
16931
|
+
return {
|
|
16932
|
+
...info,
|
|
16933
|
+
children: info.children.map((c) => ({
|
|
16934
|
+
...c,
|
|
16935
|
+
files: Object.keys(c.files).length,
|
|
16936
|
+
file_list: Object.keys(c.files)
|
|
16937
|
+
})),
|
|
16938
|
+
...files ? { files } : {}
|
|
16939
|
+
};
|
|
16940
|
+
}
|
|
16941
|
+
}),
|
|
16942
|
+
tool({
|
|
16943
|
+
name: "wp_child_theme_create",
|
|
16944
|
+
description: "Create a child theme of Maggie Core for this site (slug maggie-<name>), by default from the site's current look: its Global Styles (colours, fonts with their files, styles), edited header and footer. Data files only, never code. activate: true switches the site to it and checks the homepage looks the same. Switching keeps the look only because it is copied into the child; Global Styles and edited parts are kept per theme in WordPress.",
|
|
16945
|
+
parameters: {
|
|
16946
|
+
type: "object",
|
|
16947
|
+
properties: {
|
|
16948
|
+
slug: str2("maggie-<name>, e.g. maggie-moonlake."),
|
|
16949
|
+
name: str2("Theme name, e.g. Moon Lake Spa."),
|
|
16950
|
+
from_site: bool("Copy the site's current look into it (default true)."),
|
|
16951
|
+
activate: bool("Switch the site to it now (default false).")
|
|
16952
|
+
},
|
|
16953
|
+
required: ["slug", "name"]
|
|
16954
|
+
},
|
|
16955
|
+
group: "site",
|
|
16956
|
+
need: WRITE,
|
|
16957
|
+
risk: "W3",
|
|
16958
|
+
gates: [...GATES],
|
|
16959
|
+
confirm: (a, lang) => say(
|
|
16960
|
+
lang,
|
|
16961
|
+
`Create the child theme ${q2(a.slug, "")} on your website${a.activate ? " and switch to it" : ""}.`,
|
|
16962
|
+
`\u5728\u7DB2\u7AD9\u4E0A\u5EFA\u7ACB\u5B50\u4E3B\u984C ${q2(a.slug, "")}${a.activate ? "\u4E26\u5207\u63DB\u904E\u53BB" : ""}\u3002`
|
|
16963
|
+
),
|
|
16964
|
+
run: async (ctx, a) => {
|
|
16965
|
+
const site = siteOf(ctx);
|
|
16966
|
+
const out = await call(site, {
|
|
16967
|
+
method: "POST",
|
|
16968
|
+
path: "/maggie-core/v1/child-theme",
|
|
16969
|
+
body: { slug: a.slug, name: a.name, from_site: a.from_site !== false }
|
|
16970
|
+
});
|
|
16971
|
+
if (!a.activate)
|
|
16972
|
+
return {
|
|
16973
|
+
...out,
|
|
16974
|
+
next: "Switch to it with wp_child_theme_activate when ready (the tool checks the homepage looks the same)."
|
|
16975
|
+
};
|
|
16976
|
+
const before = await homeShape(site);
|
|
16977
|
+
const act = await call(site, {
|
|
16978
|
+
method: "POST",
|
|
16979
|
+
path: "/maggie-core/v1/child-theme/activate",
|
|
16980
|
+
body: { slug: out.slug }
|
|
16981
|
+
});
|
|
16982
|
+
return { ...out, activated: act, check: compareShapes(before, await homeShape(site)) };
|
|
16983
|
+
}
|
|
16984
|
+
}),
|
|
16985
|
+
tool({
|
|
16986
|
+
name: "wp_child_theme_update",
|
|
16987
|
+
description: "Change a Maggie child theme's files (default: the active child): brand colours (by name, hex) and fonts (by name, CSS font-family of fonts already in it) in theme.json, extra theme.json styles, plain CSS (replace or append), the header or footer (block markup), templates, and the site's own patterns (shown in the editor's inserter). Data only, never code. Each change keeps a backup (wp_child_theme_restore). Changes made with wp_style_set / wp_header_set are stored in the database and win over the child's files.",
|
|
16988
|
+
parameters: {
|
|
16989
|
+
type: "object",
|
|
16990
|
+
properties: {
|
|
16991
|
+
slug: str2("The child theme (default the active one)."),
|
|
16992
|
+
colors: obj('e.g. { "olive": "#4F5B48" }.'),
|
|
16993
|
+
fonts: obj('Font families by slug: { "body": "Jost, sans-serif" } (files must already be in the child).'),
|
|
16994
|
+
styles: obj('theme.json styles to merge in (e.g. { "elements": { "button": { \u2026 } } }).'),
|
|
16995
|
+
css: str2("Replace the child's CSS (plain CSS)."),
|
|
16996
|
+
css_append: str2("Add CSS at the end."),
|
|
16997
|
+
header: str2("Header block markup (parts/header.html)."),
|
|
16998
|
+
footer: str2("Footer block markup (parts/footer.html)."),
|
|
16999
|
+
templates: obj('Templates by slug \u2192 block markup, e.g. { "page-landing": "\u2026" }.'),
|
|
17000
|
+
patterns: arr("Patterns to add or replace.", PATTERN_SCHEMA),
|
|
17001
|
+
remove_patterns: arr("Pattern slugs to remove.", { type: "string" })
|
|
17002
|
+
}
|
|
17003
|
+
},
|
|
17004
|
+
group: "site",
|
|
17005
|
+
need: WRITE,
|
|
17006
|
+
risk: "W3",
|
|
17007
|
+
gates: [...GATES],
|
|
17008
|
+
confirm: (a, lang) => say(
|
|
17009
|
+
lang,
|
|
17010
|
+
`Change the child theme ${q2(a.slug, "(the active one)")} on your website.`,
|
|
17011
|
+
`\u4FEE\u6539\u7DB2\u7AD9\u7684\u5B50\u4E3B\u984C ${q2(a.slug, "\uFF08\u76EE\u524D\u4F7F\u7528\u4E2D\u7684\uFF09")}\u3002`
|
|
17012
|
+
),
|
|
17013
|
+
run: async (ctx, a) => {
|
|
17014
|
+
const site = siteOf(ctx);
|
|
17015
|
+
const slug = await childSlug(site, a.slug);
|
|
17016
|
+
const info = await childInfo(site, slug);
|
|
17017
|
+
const cur = info.files ?? {};
|
|
17018
|
+
const files = {};
|
|
17019
|
+
const notes = [];
|
|
17020
|
+
if (a.colors || a.fonts || a.styles) {
|
|
17021
|
+
const tj = JSON.parse(cur["theme.json"] ?? '{"version":3,"settings":{}}');
|
|
17022
|
+
tj.settings = tj.settings ?? {};
|
|
17023
|
+
if (a.colors) {
|
|
17024
|
+
let palette = tj.settings.color?.palette ?? [];
|
|
17025
|
+
if (!palette.length) palette = ((await styleGet(site))._palette ?? []).map((c) => ({ ...c }));
|
|
17026
|
+
for (const [slugC, color] of Object.entries(a.colors)) {
|
|
17027
|
+
if (!/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(color))
|
|
17028
|
+
throw new Error(`${slugC}: "${color}" isn't a hex colour.`);
|
|
17029
|
+
const c = palette.find((x) => x.slug === slugC);
|
|
17030
|
+
if (c) c.color = color;
|
|
17031
|
+
else
|
|
17032
|
+
palette.push({
|
|
17033
|
+
slug: slugC,
|
|
17034
|
+
color,
|
|
17035
|
+
name: slugC.replace(/-/g, " ").replace(/\b\w/g, (m) => m.toUpperCase())
|
|
17036
|
+
});
|
|
17037
|
+
}
|
|
17038
|
+
const map = Object.fromEntries(palette.map((c) => [c.slug, c.color]));
|
|
17039
|
+
const bad = contrastReport(map).filter((r) => !r.ok);
|
|
17040
|
+
if (bad.length)
|
|
17041
|
+
notes.push(`Low contrast: ${bad.map((r) => `${r.text} on ${r.background} ${r.ratio}:1`).join(", ")}.`);
|
|
17042
|
+
tj.settings.color = { ...tj.settings.color ?? {}, palette };
|
|
17043
|
+
}
|
|
17044
|
+
if (a.fonts) {
|
|
17045
|
+
const fams = tj.settings.typography?.fontFamilies ?? [];
|
|
17046
|
+
if (!fams.length)
|
|
17047
|
+
throw new Error(
|
|
17048
|
+
"This child has no font list yet (fonts come from the parent): create it from the site first, then change fonts."
|
|
17049
|
+
);
|
|
17050
|
+
for (const [slugF, family] of Object.entries(a.fonts)) {
|
|
17051
|
+
const f = fams.find((x) => x.slug === slugF);
|
|
17052
|
+
if (!f) throw new Error(`No font "${slugF}" in the child. Fonts: ${fams.map((x) => x.slug).join(", ")}`);
|
|
17053
|
+
f.fontFamily = family;
|
|
17054
|
+
}
|
|
17055
|
+
}
|
|
17056
|
+
if (a.styles)
|
|
17057
|
+
tj.styles = deepMerge(tj.styles ?? {}, a.styles);
|
|
17058
|
+
files["theme.json"] = JSON.stringify(tj, null, " ");
|
|
17059
|
+
}
|
|
17060
|
+
if (typeof a.css === "string") files["style.css"] = a.css;
|
|
17061
|
+
if (typeof a.css_append === "string")
|
|
17062
|
+
files["style.css"] = `${(files["style.css"] ?? cur["style.css"] ?? "").replace(/\n*$/, "\n")}${a.css_append}
|
|
17063
|
+
`;
|
|
17064
|
+
const markup = (path2, m) => {
|
|
17065
|
+
if (typeof m !== "string" || !m.trim()) return;
|
|
17066
|
+
if (!parse(m).some((b) => b.name)) throw new Error(`${path2}: give block markup (<!-- wp:\u2026 -->).`);
|
|
17067
|
+
files[path2] = m;
|
|
17068
|
+
};
|
|
17069
|
+
markup("parts/header.html", a.header);
|
|
17070
|
+
markup("parts/footer.html", a.footer);
|
|
17071
|
+
for (const [t, m] of Object.entries(a.templates ?? {}))
|
|
17072
|
+
markup(`templates/${t}.html`, m);
|
|
17073
|
+
for (const p of a.patterns ?? []) {
|
|
17074
|
+
if (!/^[a-z0-9][a-z0-9-]{0,60}$/.test(p.slug ?? ""))
|
|
17075
|
+
throw new Error(`Pattern slug "${p.slug}": lowercase letters, digits and dashes.`);
|
|
17076
|
+
files[`maggie-patterns/${p.slug}.json`] = JSON.stringify({
|
|
17077
|
+
title: p.title,
|
|
17078
|
+
content: p.content,
|
|
17079
|
+
description: p.description ?? "",
|
|
17080
|
+
categories: p.categories?.length ? p.categories : ["maggie"]
|
|
17081
|
+
});
|
|
17082
|
+
}
|
|
17083
|
+
const del = (a.remove_patterns ?? []).map((x) => `maggie-patterns/${x}.json`);
|
|
17084
|
+
if (!Object.keys(files).length && !del.length) throw new Error("Nothing to change.");
|
|
17085
|
+
const out = await call(site, {
|
|
17086
|
+
method: "POST",
|
|
17087
|
+
path: "/maggie-core/v1/child-theme",
|
|
17088
|
+
body: { slug, files, delete: del }
|
|
17089
|
+
});
|
|
17090
|
+
return {
|
|
17091
|
+
slug,
|
|
17092
|
+
...out,
|
|
17093
|
+
...notes.length ? { notes } : {},
|
|
17094
|
+
...out.active ? {} : { note: "This child isn't active: switch with wp_child_theme_activate to see it." }
|
|
17095
|
+
};
|
|
17096
|
+
}
|
|
17097
|
+
}),
|
|
17098
|
+
tool({
|
|
17099
|
+
name: "wp_child_theme_activate",
|
|
17100
|
+
description: "Switch the site to a Maggie child theme, or back to Maggie Core (slug maggie-core). Checks the homepage before and after (words, fonts, brand colours) and reports differences. WordPress keeps Global Styles and edited header/footer per theme: a child made with wp_child_theme_create from the site carries them; switching back to Maggie Core brings its own back.",
|
|
17101
|
+
parameters: {
|
|
17102
|
+
type: "object",
|
|
17103
|
+
properties: { slug: str2("maggie-<name>, or maggie-core to go back.") },
|
|
17104
|
+
required: ["slug"]
|
|
17105
|
+
},
|
|
17106
|
+
group: "site",
|
|
17107
|
+
need: WRITE,
|
|
17108
|
+
risk: "W3",
|
|
17109
|
+
gates: [...GATES],
|
|
17110
|
+
confirm: (a, lang) => say(lang, `Switch your website's theme to ${q2(a.slug, "")}.`, `\u628A\u7DB2\u7AD9\u7684\u4E3B\u984C\u5207\u63DB\u70BA ${q2(a.slug, "")}\u3002`),
|
|
17111
|
+
run: async (ctx, a) => {
|
|
17112
|
+
const site = siteOf(ctx);
|
|
17113
|
+
const before = await homeShape(site);
|
|
17114
|
+
const out = await call(site, {
|
|
17115
|
+
method: "POST",
|
|
17116
|
+
path: "/maggie-core/v1/child-theme/activate",
|
|
17117
|
+
body: { slug: a.slug }
|
|
17118
|
+
});
|
|
17119
|
+
const check2 = compareShapes(before, await homeShape(site));
|
|
17120
|
+
return {
|
|
17121
|
+
...out,
|
|
17122
|
+
check: check2,
|
|
17123
|
+
...check2.checked && !check2.ok ? { undo: `Switch back with wp_child_theme_activate slug ${out.before}.` } : {}
|
|
17124
|
+
};
|
|
17125
|
+
}
|
|
17126
|
+
}),
|
|
17127
|
+
tool({
|
|
17128
|
+
name: "wp_child_theme_restore",
|
|
17129
|
+
description: "Put a child theme's files back as they were before one change (backup ids from wp_child_theme_status). The current files are backed up first, so a restore can be undone too.",
|
|
17130
|
+
parameters: {
|
|
17131
|
+
type: "object",
|
|
17132
|
+
properties: { slug: str2("The child theme."), backup: str2("Backup id, e.g. 20261104-183208-728.") },
|
|
17133
|
+
required: ["slug", "backup"]
|
|
17134
|
+
},
|
|
17135
|
+
group: "site",
|
|
17136
|
+
need: WRITE,
|
|
17137
|
+
risk: "W3",
|
|
17138
|
+
gates: [...GATES],
|
|
17139
|
+
confirm: (a, lang) => say(
|
|
17140
|
+
lang,
|
|
17141
|
+
`Restore the child theme ${q2(a.slug, "")} to backup ${String(a.backup ?? "")}.`,
|
|
17142
|
+
`\u628A\u5B50\u4E3B\u984C ${q2(a.slug, "")} \u9084\u539F\u5230\u5099\u4EFD ${String(a.backup ?? "")}\u3002`
|
|
17143
|
+
),
|
|
17144
|
+
run: (ctx, a) => call(siteOf(ctx), {
|
|
17145
|
+
method: "POST",
|
|
17146
|
+
path: "/maggie-core/v1/child-theme/restore",
|
|
17147
|
+
body: { slug: a.slug, backup: a.backup }
|
|
17148
|
+
})
|
|
17149
|
+
})
|
|
17150
|
+
];
|
|
17151
|
+
function deepMerge(a, b) {
|
|
17152
|
+
const out = { ...a };
|
|
17153
|
+
for (const [k, v] of Object.entries(b)) {
|
|
17154
|
+
const cur = out[k];
|
|
17155
|
+
out[k] = v && typeof v === "object" && !Array.isArray(v) && cur && typeof cur === "object" && !Array.isArray(cur) ? deepMerge(cur, v) : v;
|
|
17156
|
+
}
|
|
17157
|
+
return out;
|
|
17158
|
+
}
|
|
16753
17159
|
var seoFields = {
|
|
16754
17160
|
title: str2("SEO title (\u224860 characters; the site name is added by the theme)."),
|
|
16755
17161
|
description: str2("Meta description (\u2248150-160 characters)."),
|
|
@@ -17005,7 +17411,15 @@ var elementTools = [
|
|
|
17005
17411
|
}
|
|
17006
17412
|
})
|
|
17007
17413
|
];
|
|
17008
|
-
var wordpressTools = [
|
|
17414
|
+
var wordpressTools = [
|
|
17415
|
+
...siteTools,
|
|
17416
|
+
...pageTools,
|
|
17417
|
+
...sectionTools,
|
|
17418
|
+
...mediaTools,
|
|
17419
|
+
...childTools,
|
|
17420
|
+
...seoTools,
|
|
17421
|
+
...elementTools
|
|
17422
|
+
];
|
|
17009
17423
|
|
|
17010
17424
|
// ../../packages/tools/src/index.ts
|
|
17011
17425
|
var tools = [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cmo-so/mcp",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.3",
|
|
4
4
|
"description": "CMO.so's MCP server for Claude Desktop, Cursor, VS Code and other MCP clients: manage your websites, blog posts, LinkedIn queue and results with Maggie.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|