@cmo-so/mcp 0.12.6 → 0.12.7
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 +495 -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.7",
|
|
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",
|
|
@@ -16727,6 +16727,9 @@ var sectionTools = [
|
|
|
16727
16727
|
section: str2("Section id from wp_page_get (s2, m4) or its type (faq)."),
|
|
16728
16728
|
edits: EDITS,
|
|
16729
16729
|
slots: obj("Complete new content for the section (same type unless type is given)."),
|
|
16730
|
+
markup: str2(
|
|
16731
|
+
"Replace the section with this block markup (one top-level block; written by a coding agent; PHP never)."
|
|
16732
|
+
),
|
|
16730
16733
|
type: str2("With slots: change the section to this type."),
|
|
16731
16734
|
faq: obj("FAQ changes.", {
|
|
16732
16735
|
add: arr("Questions to add.", obj("", { q: str2(""), a: str2("") })),
|
|
@@ -16758,6 +16761,10 @@ var sectionTools = [
|
|
|
16758
16761
|
if (type !== "faq") throw new Error(`Section ${ref} is a ${type} section, not the FAQ.`);
|
|
16759
16762
|
next = editFaq(target.block, a.faq, kit);
|
|
16760
16763
|
note = { questions: describe2({ ...target, block: next }, kit.syncedTitles).counts?.questions };
|
|
16764
|
+
} else if (typeof a.markup === "string") {
|
|
16765
|
+
const bl = checkMarkup(a.markup, "markup").filter((b) => b.name);
|
|
16766
|
+
if (bl.length !== 1) throw new Error("markup: give exactly one top-level block (wrap several in a group).");
|
|
16767
|
+
next = bl[0];
|
|
16761
16768
|
} else if (a.slots) {
|
|
16762
16769
|
const t = String(a.type ?? type);
|
|
16763
16770
|
const keepName = target.block.attrs.metadata?.name;
|
|
@@ -16778,7 +16785,7 @@ var sectionTools = [
|
|
|
16778
16785
|
);
|
|
16779
16786
|
next = r.block;
|
|
16780
16787
|
note = { changed: r.changed };
|
|
16781
|
-
} else throw new Error("Give edits, slots or faq.");
|
|
16788
|
+
} else throw new Error("Give edits, slots, markup or faq.");
|
|
16782
16789
|
return { blocks: replace(blocks, ref, next, kit.syncedTitles), note };
|
|
16783
16790
|
})
|
|
16784
16791
|
}),
|
|
@@ -17670,7 +17677,493 @@ var elementTools = [
|
|
|
17670
17677
|
}
|
|
17671
17678
|
})
|
|
17672
17679
|
];
|
|
17680
|
+
function checkMarkup(markup, what) {
|
|
17681
|
+
if (typeof markup !== "string" || !markup.trim()) throw new Error(`${what}: give block markup (<!-- wp:\u2026 -->).`);
|
|
17682
|
+
if (/<\?(php|=)?/i.test(markup)) throw new Error(`${what}: PHP isn't allowed.`);
|
|
17683
|
+
const blocks = parse(markup);
|
|
17684
|
+
if (!blocks.some((b) => b.name)) throw new Error(`${what}: give block markup (<!-- wp:\u2026 -->), not plain HTML.`);
|
|
17685
|
+
return blocks;
|
|
17686
|
+
}
|
|
17687
|
+
var POST_FIELDS = "id,title,slug,link,status,date,modified,categories,tags,featured_media,excerpt,meta";
|
|
17688
|
+
async function termIds(site, taxonomy, names) {
|
|
17689
|
+
if (!Array.isArray(names)) return void 0;
|
|
17690
|
+
const ids2 = [];
|
|
17691
|
+
for (const n2 of names) {
|
|
17692
|
+
if (typeof n2 === "number") {
|
|
17693
|
+
ids2.push(n2);
|
|
17694
|
+
continue;
|
|
17695
|
+
}
|
|
17696
|
+
const name = String(n2).trim();
|
|
17697
|
+
if (!name) continue;
|
|
17698
|
+
const found = await call(site, {
|
|
17699
|
+
path: `/wp/v2/${taxonomy}`,
|
|
17700
|
+
query: { search: name, per_page: 20, _fields: "id,name" }
|
|
17701
|
+
});
|
|
17702
|
+
const hit = found.find((t) => t.name.toLowerCase() === name.toLowerCase());
|
|
17703
|
+
ids2.push(
|
|
17704
|
+
hit ? hit.id : (await call(site, { method: "POST", path: `/wp/v2/${taxonomy}`, body: { name } })).id
|
|
17705
|
+
);
|
|
17706
|
+
}
|
|
17707
|
+
return ids2;
|
|
17708
|
+
}
|
|
17709
|
+
var fullTools = [
|
|
17710
|
+
tool({
|
|
17711
|
+
name: "wp_page_markup_get",
|
|
17712
|
+
description: "A page's full block markup (Gutenberg), for coding agents that edit blocks directly; with section, only that section's markup. Pair with wp_page_markup_set or wp_section_update markup.",
|
|
17713
|
+
parameters: {
|
|
17714
|
+
type: "object",
|
|
17715
|
+
properties: { page: PAGE, section: str2("Only this section (id or type).") },
|
|
17716
|
+
required: ["page"]
|
|
17717
|
+
},
|
|
17718
|
+
group: "site",
|
|
17719
|
+
need: READ,
|
|
17720
|
+
risk: "R0",
|
|
17721
|
+
gates: ["WP"],
|
|
17722
|
+
run: async (ctx, a) => {
|
|
17723
|
+
const site = siteOf(ctx);
|
|
17724
|
+
const p = await findPage(site, a.page, true);
|
|
17725
|
+
const raw = p.content?.raw ?? "";
|
|
17726
|
+
if (a.section) {
|
|
17727
|
+
const kit = await siteKit(site);
|
|
17728
|
+
const l = sectionAt(parse(raw), String(a.section), kit.syncedTitles);
|
|
17729
|
+
return { id: p.id, path: path(p.link), modified: p.modified, section: l.id, markup: serialize([l.block]) };
|
|
17730
|
+
}
|
|
17731
|
+
return { id: p.id, path: path(p.link), modified: p.modified, template: p.template || "default", markup: raw };
|
|
17732
|
+
}
|
|
17733
|
+
}),
|
|
17734
|
+
tool({
|
|
17735
|
+
name: "wp_page_markup_set",
|
|
17736
|
+
description: "Replace a page's whole content with block markup a coding agent wrote (Gutenberg blocks, Maggie Core classes and Tailwind classes allowed; PHP never). Read it first with wp_page_markup_get and pass its modified as expect_modified. preview shows the sections before and after without saving. wp_page_undo goes back.",
|
|
17737
|
+
parameters: {
|
|
17738
|
+
type: "object",
|
|
17739
|
+
properties: {
|
|
17740
|
+
page: PAGE,
|
|
17741
|
+
markup: str2("The page's new block markup."),
|
|
17742
|
+
expect_modified: EXPECT,
|
|
17743
|
+
preview: PREVIEW
|
|
17744
|
+
},
|
|
17745
|
+
required: ["page", "markup"]
|
|
17746
|
+
},
|
|
17747
|
+
group: "site",
|
|
17748
|
+
need: WRITE,
|
|
17749
|
+
risk: "W3",
|
|
17750
|
+
gates: [...GATES],
|
|
17751
|
+
confirm: (a, lang) => say(
|
|
17752
|
+
lang,
|
|
17753
|
+
`Replace the content of page ${q2(a.page, "")} with new block markup.`,
|
|
17754
|
+
`\u7528\u65B0\u7684\u5340\u584A\u5167\u5BB9\u53D6\u4EE3\u9801\u9762 ${q2(a.page, "")}\u3002`
|
|
17755
|
+
),
|
|
17756
|
+
run: (ctx, a) => changePage(ctx, a, () => ({ blocks: checkMarkup(a.markup, "markup") }))
|
|
17757
|
+
}),
|
|
17758
|
+
tool({
|
|
17759
|
+
name: "wp_posts_list",
|
|
17760
|
+
description: "Blog posts on the site (newest first): id, title, path, status, date, categories and tags. Search words, status, category.",
|
|
17761
|
+
parameters: {
|
|
17762
|
+
type: "object",
|
|
17763
|
+
properties: {
|
|
17764
|
+
search: str2("Words in the title or content."),
|
|
17765
|
+
status: { type: "string", enum: ["publish", "draft", "future", "private", "any"], description: "Default any." },
|
|
17766
|
+
category: str2("Category name or id."),
|
|
17767
|
+
limit: int2("How many (default 30, at most 100).")
|
|
17768
|
+
}
|
|
17769
|
+
},
|
|
17770
|
+
group: "site",
|
|
17771
|
+
need: READ,
|
|
17772
|
+
risk: "R0",
|
|
17773
|
+
gates: ["WP"],
|
|
17774
|
+
run: async (ctx, a) => {
|
|
17775
|
+
const site = siteOf(ctx);
|
|
17776
|
+
const cat = a.category ? (await termIds(site, "categories", [a.category]).catch(() => void 0))?.[0] : void 0;
|
|
17777
|
+
const rows = await call(site, {
|
|
17778
|
+
path: "/wp/v2/posts",
|
|
17779
|
+
query: {
|
|
17780
|
+
context: "edit",
|
|
17781
|
+
search: a.search || void 0,
|
|
17782
|
+
status: a.status && a.status !== "any" ? String(a.status) : "publish,draft,future,private,pending",
|
|
17783
|
+
categories: cat,
|
|
17784
|
+
per_page: Math.min(100, Math.max(1, Number(a.limit) || 30)),
|
|
17785
|
+
_fields: "id,title,link,status,date,categories,tags"
|
|
17786
|
+
}
|
|
17787
|
+
});
|
|
17788
|
+
return {
|
|
17789
|
+
count: rows.length,
|
|
17790
|
+
items: rows.map((r) => ({
|
|
17791
|
+
id: r.id,
|
|
17792
|
+
title: r.title.raw ?? r.title.rendered,
|
|
17793
|
+
path: path(r.link),
|
|
17794
|
+
status: r.status,
|
|
17795
|
+
date: r.date,
|
|
17796
|
+
categories: r.categories,
|
|
17797
|
+
tags: r.tags
|
|
17798
|
+
}))
|
|
17799
|
+
};
|
|
17800
|
+
}
|
|
17801
|
+
}),
|
|
17802
|
+
tool({
|
|
17803
|
+
name: "wp_post_get",
|
|
17804
|
+
description: "One blog post in full: title, slug, status, dates, categories, tags, featured image, excerpt, SEO fields and its content (block markup or HTML).",
|
|
17805
|
+
parameters: { type: "object", properties: { post: int2("The post id (wp_posts_list).") }, required: ["post"] },
|
|
17806
|
+
group: "site",
|
|
17807
|
+
need: READ,
|
|
17808
|
+
risk: "R0",
|
|
17809
|
+
gates: ["WP"],
|
|
17810
|
+
run: async (ctx, a) => {
|
|
17811
|
+
const p = await call(siteOf(ctx), {
|
|
17812
|
+
path: `/wp/v2/posts/${Number(a.post)}`,
|
|
17813
|
+
query: { context: "edit", _fields: `${POST_FIELDS},content` }
|
|
17814
|
+
});
|
|
17815
|
+
return {
|
|
17816
|
+
...p,
|
|
17817
|
+
title: p.title.raw,
|
|
17818
|
+
path: path(p.link),
|
|
17819
|
+
excerpt: p.excerpt?.raw ?? "",
|
|
17820
|
+
content: p.content?.raw ?? ""
|
|
17821
|
+
};
|
|
17822
|
+
}
|
|
17823
|
+
}),
|
|
17824
|
+
tool({
|
|
17825
|
+
name: "wp_post_save",
|
|
17826
|
+
description: "Create a blog post (no post id) or change one (post id): title, content (block markup or HTML), excerpt, slug (a published post's old address gets a 301), status (draft, publish, private, future with date), date, categories and tags (names; new ones are created), featured_media, SEO title/description. New posts are drafts unless status says otherwise. CMO.so's blog autopilot keeps writing its own posts; this is for the owner's own changes.",
|
|
17827
|
+
parameters: {
|
|
17828
|
+
type: "object",
|
|
17829
|
+
properties: {
|
|
17830
|
+
post: int2("The post to change (leave out to create one)."),
|
|
17831
|
+
title: str2(""),
|
|
17832
|
+
content: str2("Block markup or HTML."),
|
|
17833
|
+
excerpt: str2(""),
|
|
17834
|
+
slug: str2(""),
|
|
17835
|
+
status: { type: "string", enum: ["draft", "publish", "private", "future"], description: "" },
|
|
17836
|
+
date: str2("Publish date (ISO 8601), for scheduled posts."),
|
|
17837
|
+
categories: arr("Category names (or ids).", { type: "string" }),
|
|
17838
|
+
tags: arr("Tag names (or ids).", { type: "string" }),
|
|
17839
|
+
featured_media: int2("Media id (wp_media_upload)."),
|
|
17840
|
+
seo_title: str2(""),
|
|
17841
|
+
seo_description: str2(""),
|
|
17842
|
+
expect_modified: EXPECT
|
|
17843
|
+
}
|
|
17844
|
+
},
|
|
17845
|
+
group: "site",
|
|
17846
|
+
need: WRITE,
|
|
17847
|
+
risk: "W3",
|
|
17848
|
+
gates: [...GATES],
|
|
17849
|
+
confirm: (a, lang) => a.post ? say(
|
|
17850
|
+
lang,
|
|
17851
|
+
`Change blog post #${String(a.post)} on your website${a.status === "publish" ? " (published)" : ""}.`,
|
|
17852
|
+
`\u4FEE\u6539\u7DB2\u7AD9\u4E0A\u7684\u6587\u7AE0 #${String(a.post)}\u3002`
|
|
17853
|
+
) : say(
|
|
17854
|
+
lang,
|
|
17855
|
+
`Create the blog post ${q2(a.title, "")} on your website (${a.status === "publish" ? "published" : "as a draft"}).`,
|
|
17856
|
+
`\u5728\u7DB2\u7AD9\u4E0A\u5EFA\u7ACB\u6587\u7AE0 ${q2(a.title, "")}\u3002`
|
|
17857
|
+
),
|
|
17858
|
+
run: async (ctx, a) => {
|
|
17859
|
+
const site = siteOf(ctx);
|
|
17860
|
+
const body = {};
|
|
17861
|
+
for (const k of ["title", "excerpt", "slug", "status", "date"])
|
|
17862
|
+
if (typeof a[k] === "string" && a[k] !== "") body[k] = a[k];
|
|
17863
|
+
if (typeof a.content === "string") {
|
|
17864
|
+
if (/<\?(php|=)?/i.test(a.content)) throw new Error("content: PHP isn't allowed.");
|
|
17865
|
+
body.content = a.content;
|
|
17866
|
+
}
|
|
17867
|
+
if (typeof a.featured_media === "number") body.featured_media = a.featured_media;
|
|
17868
|
+
const cats = await termIds(site, "categories", a.categories);
|
|
17869
|
+
const tags = await termIds(site, "tags", a.tags);
|
|
17870
|
+
if (cats) body.categories = cats;
|
|
17871
|
+
if (tags) body.tags = tags;
|
|
17872
|
+
const meta4 = {};
|
|
17873
|
+
if (typeof a.seo_title === "string") meta4.maggie_seo_title = a.seo_title;
|
|
17874
|
+
if (typeof a.seo_description === "string") meta4.maggie_seo_description = a.seo_description;
|
|
17875
|
+
if (Object.keys(meta4).length) body.meta = meta4;
|
|
17876
|
+
if (!a.post) {
|
|
17877
|
+
if (!body.title) throw new Error("A new post needs a title.");
|
|
17878
|
+
body.status = body.status ?? "draft";
|
|
17879
|
+
const p2 = await call(site, {
|
|
17880
|
+
method: "POST",
|
|
17881
|
+
path: "/wp/v2/posts",
|
|
17882
|
+
query: { _fields: "id,link,status,modified" },
|
|
17883
|
+
body
|
|
17884
|
+
});
|
|
17885
|
+
if (!p2?.id) throw new Error("WordPress didn't create the post.");
|
|
17886
|
+
return { created: true, id: p2.id, path: path(p2.link), status: p2.status, modified: p2.modified };
|
|
17887
|
+
}
|
|
17888
|
+
const before = await call(site, {
|
|
17889
|
+
path: `/wp/v2/posts/${Number(a.post)}`,
|
|
17890
|
+
query: { context: "edit", _fields: "id,link,status,modified" }
|
|
17891
|
+
});
|
|
17892
|
+
if (a.expect_modified && before.modified !== a.expect_modified)
|
|
17893
|
+
throw new Error(
|
|
17894
|
+
`The post changed in WordPress since you read it (${before.modified}). Read it again and redo the change.`
|
|
17895
|
+
);
|
|
17896
|
+
if (!Object.keys(body).length) throw new Error("Nothing to change.");
|
|
17897
|
+
const p = await call(site, {
|
|
17898
|
+
method: "POST",
|
|
17899
|
+
path: `/wp/v2/posts/${Number(a.post)}`,
|
|
17900
|
+
query: { _fields: "id,link,status,modified" },
|
|
17901
|
+
body
|
|
17902
|
+
});
|
|
17903
|
+
let redirect = null;
|
|
17904
|
+
if (before.status === "publish" && path(p.link) !== path(before.link))
|
|
17905
|
+
redirect = (await redirectsSet(site, { set: [{ from: path(before.link), to: path(p.link), code: 301 }] })).items.find((r) => r.from === path(before.link));
|
|
17906
|
+
return { saved: true, id: p.id, path: path(p.link), status: p.status, modified: p.modified, redirect };
|
|
17907
|
+
}
|
|
17908
|
+
}),
|
|
17909
|
+
tool({
|
|
17910
|
+
name: "wp_post_delete",
|
|
17911
|
+
description: "Move a blog post to the WordPress trash (restorable for 30 days) and redirect its address (301) to another page, by default the blog list or the homepage.",
|
|
17912
|
+
parameters: {
|
|
17913
|
+
type: "object",
|
|
17914
|
+
properties: {
|
|
17915
|
+
post: int2("The post id."),
|
|
17916
|
+
redirect_to: str2('Where its visitors go (path); default /. "410" = gone.')
|
|
17917
|
+
},
|
|
17918
|
+
required: ["post"]
|
|
17919
|
+
},
|
|
17920
|
+
group: "site",
|
|
17921
|
+
need: WRITE,
|
|
17922
|
+
risk: "W3",
|
|
17923
|
+
gates: [...GATES],
|
|
17924
|
+
confirm: (a, lang) => say(
|
|
17925
|
+
lang,
|
|
17926
|
+
`Move blog post #${String(a.post)} to the trash and redirect its address.`,
|
|
17927
|
+
`\u628A\u6587\u7AE0 #${String(a.post)} \u79FB\u5230\u5783\u573E\u6876\u4E26\u8F49\u5740\u3002`
|
|
17928
|
+
),
|
|
17929
|
+
run: async (ctx, a) => {
|
|
17930
|
+
const site = siteOf(ctx);
|
|
17931
|
+
const p = await call(site, {
|
|
17932
|
+
path: `/wp/v2/posts/${Number(a.post)}`,
|
|
17933
|
+
query: { context: "edit", _fields: "id,link,status" }
|
|
17934
|
+
});
|
|
17935
|
+
await call(site, { method: "DELETE", path: `/wp/v2/posts/${p.id}`, query: { _fields: "id,status" } });
|
|
17936
|
+
const to = String(a.redirect_to ?? "/");
|
|
17937
|
+
const from = path(p.link);
|
|
17938
|
+
const r = p.status === "publish" ? await redirectsSet(site, { set: to === "410" ? [{ from, code: 410 }] : [{ from, to, code: 301 }] }) : null;
|
|
17939
|
+
return {
|
|
17940
|
+
trashed: true,
|
|
17941
|
+
id: p.id,
|
|
17942
|
+
path: from,
|
|
17943
|
+
redirect: r ? to === "410" ? { from, code: 410 } : { from, to, code: 301 } : null
|
|
17944
|
+
};
|
|
17945
|
+
}
|
|
17946
|
+
}),
|
|
17947
|
+
tool({
|
|
17948
|
+
name: "wp_terms_list",
|
|
17949
|
+
description: "The blog's categories or tags: id, name, slug, description, post count.",
|
|
17950
|
+
parameters: {
|
|
17951
|
+
type: "object",
|
|
17952
|
+
properties: {
|
|
17953
|
+
taxonomy: { type: "string", enum: ["categories", "tags"], description: "Default categories." },
|
|
17954
|
+
search: str2("")
|
|
17955
|
+
}
|
|
17956
|
+
},
|
|
17957
|
+
group: "site",
|
|
17958
|
+
need: READ,
|
|
17959
|
+
risk: "R0",
|
|
17960
|
+
gates: ["WP"],
|
|
17961
|
+
run: async (ctx, a) => {
|
|
17962
|
+
const tax = a.taxonomy === "tags" ? "tags" : "categories";
|
|
17963
|
+
const rows = await all(
|
|
17964
|
+
siteOf(ctx),
|
|
17965
|
+
`/wp/v2/${tax}`,
|
|
17966
|
+
{ search: a.search || void 0, _fields: "id,name,slug,description,count,parent" },
|
|
17967
|
+
500
|
|
17968
|
+
);
|
|
17969
|
+
return { count: rows.length, items: rows };
|
|
17970
|
+
}
|
|
17971
|
+
}),
|
|
17972
|
+
tool({
|
|
17973
|
+
name: "wp_term_save",
|
|
17974
|
+
description: "Create a category or tag (no id) or change one (id): name, slug, description, parent (categories). The theme shows the description on its archive page.",
|
|
17975
|
+
parameters: {
|
|
17976
|
+
type: "object",
|
|
17977
|
+
properties: {
|
|
17978
|
+
taxonomy: { type: "string", enum: ["categories", "tags"], description: "Default categories." },
|
|
17979
|
+
id: int2("The term to change."),
|
|
17980
|
+
name: str2(""),
|
|
17981
|
+
slug: str2(""),
|
|
17982
|
+
description: str2(""),
|
|
17983
|
+
parent: int2("Parent category id.")
|
|
17984
|
+
}
|
|
17985
|
+
},
|
|
17986
|
+
group: "site",
|
|
17987
|
+
need: WRITE,
|
|
17988
|
+
risk: "W3",
|
|
17989
|
+
gates: [...GATES],
|
|
17990
|
+
confirm: (a, lang) => say(
|
|
17991
|
+
lang,
|
|
17992
|
+
`${a.id ? "Change" : "Create"} the ${a.taxonomy === "tags" ? "tag" : "category"} ${q2(a.name, a.id ? `#${String(a.id)}` : "")} on your website.`,
|
|
17993
|
+
`${a.id ? "\u4FEE\u6539" : "\u5EFA\u7ACB"}\u7DB2\u7AD9\u7684${a.taxonomy === "tags" ? "\u6A19\u7C64" : "\u5206\u985E"} ${q2(a.name, "")}\u3002`
|
|
17994
|
+
),
|
|
17995
|
+
run: async (ctx, a) => {
|
|
17996
|
+
const tax = a.taxonomy === "tags" ? "tags" : "categories";
|
|
17997
|
+
const body = {};
|
|
17998
|
+
for (const k of ["name", "slug", "description"]) if (typeof a[k] === "string") body[k] = a[k];
|
|
17999
|
+
if (typeof a.parent === "number" && tax === "categories") body.parent = a.parent;
|
|
18000
|
+
if (!a.id && !body.name) throw new Error("A new term needs a name.");
|
|
18001
|
+
return call(siteOf(ctx), {
|
|
18002
|
+
method: "POST",
|
|
18003
|
+
path: a.id ? `/wp/v2/${tax}/${Number(a.id)}` : `/wp/v2/${tax}`,
|
|
18004
|
+
query: { _fields: "id,name,slug,description,count,link" },
|
|
18005
|
+
body
|
|
18006
|
+
});
|
|
18007
|
+
}
|
|
18008
|
+
}),
|
|
18009
|
+
tool({
|
|
18010
|
+
name: "wp_settings_get",
|
|
18011
|
+
description: "The site's general settings: title, tagline, search-engine visibility, language, timezone, date and time formats, start of week, posts per page, homepage and posts page, logo and icon.",
|
|
18012
|
+
parameters: { type: "object", properties: {} },
|
|
18013
|
+
group: "site",
|
|
18014
|
+
need: READ,
|
|
18015
|
+
risk: "R0",
|
|
18016
|
+
gates: ["WP"],
|
|
18017
|
+
run: (ctx) => call(siteOf(ctx), {
|
|
18018
|
+
path: "/wp/v2/settings",
|
|
18019
|
+
query: {
|
|
18020
|
+
_fields: "title,description,url,blog_public,language,timezone,date_format,time_format,start_of_week,posts_per_page,show_on_front,page_on_front,page_for_posts,site_logo,site_icon"
|
|
18021
|
+
}
|
|
18022
|
+
})
|
|
18023
|
+
}),
|
|
18024
|
+
tool({
|
|
18025
|
+
name: "wp_settings_set",
|
|
18026
|
+
description: "Change the site's general settings: title, tagline (description), search_engines (true = let Google index the site, false = discourage it), timezone (e.g. Europe/London), date_format, time_format, start_of_week (0 Sunday \u2026 6), posts_per_page, site_logo and site_icon (media ids). Returns before and after. Homepage and posts page: wp_site_home_set.",
|
|
18027
|
+
parameters: {
|
|
18028
|
+
type: "object",
|
|
18029
|
+
properties: {
|
|
18030
|
+
title: str2("Site title."),
|
|
18031
|
+
description: str2("Tagline."),
|
|
18032
|
+
search_engines: bool("true: visible to search engines; false: ask them not to index the site."),
|
|
18033
|
+
timezone: str2("e.g. Europe/London."),
|
|
18034
|
+
date_format: str2("e.g. j F Y."),
|
|
18035
|
+
time_format: str2("e.g. H:i."),
|
|
18036
|
+
start_of_week: int2("0 Sunday \u2026 6 Saturday."),
|
|
18037
|
+
posts_per_page: int2("Posts on blog pages."),
|
|
18038
|
+
site_logo: int2("Media id of the logo."),
|
|
18039
|
+
site_icon: int2("Media id of the site icon (512\xD7512).")
|
|
18040
|
+
}
|
|
18041
|
+
},
|
|
18042
|
+
group: "site",
|
|
18043
|
+
need: WRITE,
|
|
18044
|
+
risk: "W3",
|
|
18045
|
+
gates: [...GATES],
|
|
18046
|
+
confirm: (a, lang) => a.search_engines === true ? say(
|
|
18047
|
+
lang,
|
|
18048
|
+
"Change your website's settings and let search engines index it.",
|
|
18049
|
+
"\u4FEE\u6539\u7DB2\u7AD9\u8A2D\u5B9A\uFF0C\u4E26\u5141\u8A31\u641C\u5C0B\u5F15\u64CE\u6536\u9304\u3002"
|
|
18050
|
+
) : a.search_engines === false ? say(
|
|
18051
|
+
lang,
|
|
18052
|
+
"Change your website's settings and ask search engines not to index it.",
|
|
18053
|
+
"\u4FEE\u6539\u7DB2\u7AD9\u8A2D\u5B9A\uFF0C\u4E26\u8981\u6C42\u641C\u5C0B\u5F15\u64CE\u4E0D\u8981\u6536\u9304\u3002"
|
|
18054
|
+
) : say(lang, "Change your website's general settings.", "\u4FEE\u6539\u7DB2\u7AD9\u7684\u4E00\u822C\u8A2D\u5B9A\u3002"),
|
|
18055
|
+
run: async (ctx, a) => {
|
|
18056
|
+
const site = siteOf(ctx);
|
|
18057
|
+
const body = {};
|
|
18058
|
+
for (const k of ["title", "description", "date_format", "time_format", "timezone"])
|
|
18059
|
+
if (typeof a[k] === "string") body[k] = a[k];
|
|
18060
|
+
for (const k of ["start_of_week", "posts_per_page", "site_logo", "site_icon"])
|
|
18061
|
+
if (typeof a[k] === "number") body[k] = a[k];
|
|
18062
|
+
if (typeof a.search_engines === "boolean") body.blog_public = a.search_engines ? 1 : 0;
|
|
18063
|
+
if (!Object.keys(body).length) throw new Error("Nothing to change.");
|
|
18064
|
+
const fields = Object.keys(body).join(",");
|
|
18065
|
+
const before = await call(site, { path: "/wp/v2/settings", query: { _fields: fields } });
|
|
18066
|
+
if ("blog_public" in body && !("blog_public" in before))
|
|
18067
|
+
throw new Error(
|
|
18068
|
+
"This site's theme doesn't expose search-engine visibility yet: update Maggie Core (wp_theme_update) to 0.11.4 or later."
|
|
18069
|
+
);
|
|
18070
|
+
const after = await call(site, {
|
|
18071
|
+
method: "POST",
|
|
18072
|
+
path: "/wp/v2/settings",
|
|
18073
|
+
query: { _fields: fields },
|
|
18074
|
+
body
|
|
18075
|
+
});
|
|
18076
|
+
return { before, after };
|
|
18077
|
+
}
|
|
18078
|
+
}),
|
|
18079
|
+
tool({
|
|
18080
|
+
name: "wp_plugins_list",
|
|
18081
|
+
description: "The site's plugins: name, version, active or not, and whether an update is waiting.",
|
|
18082
|
+
parameters: { type: "object", properties: {} },
|
|
18083
|
+
group: "site",
|
|
18084
|
+
need: READ,
|
|
18085
|
+
risk: "R0",
|
|
18086
|
+
gates: ["WP"],
|
|
18087
|
+
run: async (ctx) => {
|
|
18088
|
+
const rows = await call(siteOf(ctx), { path: "/wp/v2/plugins", query: { _fields: "plugin,name,version,status" } });
|
|
18089
|
+
return { count: rows.length, items: rows };
|
|
18090
|
+
}
|
|
18091
|
+
}),
|
|
18092
|
+
tool({
|
|
18093
|
+
name: "wp_plugin_set",
|
|
18094
|
+
description: "Install a plugin from WordPress.org by its slug (e.g. wpforms-lite), and/or activate or deactivate an installed one (plugin id from wp_plugins_list, e.g. akismet/akismet). Plugins can't be deleted from here. Installing runs the plugin's code on the site: only well-known plugins the owner asked for.",
|
|
18095
|
+
parameters: {
|
|
18096
|
+
type: "object",
|
|
18097
|
+
properties: {
|
|
18098
|
+
install: str2("WordPress.org slug to install."),
|
|
18099
|
+
plugin: str2("Installed plugin id (folder/file) to change."),
|
|
18100
|
+
status: {
|
|
18101
|
+
type: "string",
|
|
18102
|
+
enum: ["active", "inactive"],
|
|
18103
|
+
description: "Activate or deactivate (after installing, default inactive)."
|
|
18104
|
+
}
|
|
18105
|
+
}
|
|
18106
|
+
},
|
|
18107
|
+
group: "site",
|
|
18108
|
+
need: WRITE,
|
|
18109
|
+
risk: "W3",
|
|
18110
|
+
gates: [...GATES],
|
|
18111
|
+
confirm: (a, lang) => a.install ? say(
|
|
18112
|
+
lang,
|
|
18113
|
+
`Install the plugin ${q2(a.install, "")} from WordPress.org on your website${a.status === "active" ? " and activate it" : ""}.`,
|
|
18114
|
+
`\u5728\u7DB2\u7AD9\u4E0A\u5B89\u88DD WordPress.org \u63D2\u4EF6 ${q2(a.install, "")}${a.status === "active" ? "\u4E26\u555F\u7528" : ""}\u3002`
|
|
18115
|
+
) : say(
|
|
18116
|
+
lang,
|
|
18117
|
+
`${a.status === "active" ? "Activate" : "Deactivate"} the plugin ${q2(a.plugin, "")} on your website.`,
|
|
18118
|
+
`${a.status === "active" ? "\u555F\u7528" : "\u505C\u7528"}\u7DB2\u7AD9\u4E0A\u7684\u63D2\u4EF6 ${q2(a.plugin, "")}\u3002`
|
|
18119
|
+
),
|
|
18120
|
+
run: async (ctx, a) => {
|
|
18121
|
+
const site = siteOf(ctx);
|
|
18122
|
+
if (a.install) {
|
|
18123
|
+
if (!/^[a-z0-9-]{2,80}$/.test(String(a.install)))
|
|
18124
|
+
throw new Error("install: a WordPress.org plugin slug, e.g. wpforms-lite.");
|
|
18125
|
+
return call(site, {
|
|
18126
|
+
method: "POST",
|
|
18127
|
+
path: "/wp/v2/plugins",
|
|
18128
|
+
query: { _fields: "plugin,name,version,status" },
|
|
18129
|
+
body: { slug: a.install, status: a.status === "active" ? "active" : "inactive" }
|
|
18130
|
+
});
|
|
18131
|
+
}
|
|
18132
|
+
if (!a.plugin || !a.status) throw new Error("Give install (a slug), or plugin and status.");
|
|
18133
|
+
if (!/^[a-z0-9._-]+(\/[a-z0-9._-]+)?$/i.test(String(a.plugin)))
|
|
18134
|
+
throw new Error("plugin: the id from wp_plugins_list, e.g. akismet/akismet.");
|
|
18135
|
+
return call(site, {
|
|
18136
|
+
method: "POST",
|
|
18137
|
+
path: `/wp/v2/plugins/${String(a.plugin).replace(/\.php$/, "")}`,
|
|
18138
|
+
query: { _fields: "plugin,name,version,status" },
|
|
18139
|
+
body: { status: a.status }
|
|
18140
|
+
});
|
|
18141
|
+
}
|
|
18142
|
+
}),
|
|
18143
|
+
tool({
|
|
18144
|
+
name: "wp_api_read",
|
|
18145
|
+
description: "Read any WordPress REST route the site builder may use (GET only), for coding agents exploring a site: e.g. /wp/v2/types, /wp/v2/taxonomies, /wp/v2/templates, /wp/v2/block-patterns/patterns, /wp/v2/global-styles/themes/maggie-core, /maggie-core/v1/site. Results are trimmed to 60 items.",
|
|
18146
|
+
parameters: {
|
|
18147
|
+
type: "object",
|
|
18148
|
+
properties: { route: str2("The REST route under /wp-json, e.g. /wp/v2/types."), query: obj("Query parameters.") },
|
|
18149
|
+
required: ["route"]
|
|
18150
|
+
},
|
|
18151
|
+
group: "site",
|
|
18152
|
+
need: READ,
|
|
18153
|
+
risk: "R0",
|
|
18154
|
+
gates: ["WP"],
|
|
18155
|
+
run: async (ctx, a) => {
|
|
18156
|
+
const r = await siteOf(ctx).request({
|
|
18157
|
+
path: String(a.route),
|
|
18158
|
+
query: a.query ?? {}
|
|
18159
|
+
});
|
|
18160
|
+
const data = Array.isArray(r.data) ? r.data.slice(0, 60) : r.data;
|
|
18161
|
+
return { status: r.status, total: r.total ?? null, data };
|
|
18162
|
+
}
|
|
18163
|
+
})
|
|
18164
|
+
];
|
|
17673
18165
|
var wordpressTools = [
|
|
18166
|
+
...fullTools,
|
|
17674
18167
|
...siteTools,
|
|
17675
18168
|
...pageTools,
|
|
17676
18169
|
...sectionTools,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cmo-so/mcp",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.7",
|
|
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",
|