@cmo-so/mcp 0.7.6 → 0.7.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 +186 -4
- 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.7.
|
|
7202
|
+
version: "0.7.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",
|
|
@@ -8202,9 +8202,9 @@ function domainApi(api2, domain) {
|
|
|
8202
8202
|
/** The last time posts delivered by API pull were found in the website's sitemap (gives them their address). */
|
|
8203
8203
|
sitemapMatch: () => api2.request(`${base}/rewrites/sitemap-match`),
|
|
8204
8204
|
/** Read the sitemap (found on its own when blank) and match API-pull posts to their live address. */
|
|
8205
|
-
runSitemapMatch: (
|
|
8205
|
+
runSitemapMatch: (sitemapUrl2) => post(
|
|
8206
8206
|
`${base}/rewrites/sitemap-match`,
|
|
8207
|
-
|
|
8207
|
+
sitemapUrl2 ? { sitemap_url: sitemapUrl2 } : {}
|
|
8208
8208
|
),
|
|
8209
8209
|
rewritePolicy: () => api2.request(`${base}/rewrite-policy`),
|
|
8210
8210
|
setRewritePolicy: (p) => put(`${base}/rewrite-policy`, p),
|
|
@@ -8341,6 +8341,75 @@ function createSession({ oauth: oauth2, store, skewMs = 6e4, now = Date.now, onS
|
|
|
8341
8341
|
};
|
|
8342
8342
|
}
|
|
8343
8343
|
|
|
8344
|
+
// ../../packages/api-client/src/sitemaps.ts
|
|
8345
|
+
var SITEMAPS_MAX = 20;
|
|
8346
|
+
var SITEMAP_LABEL_MAX = 80;
|
|
8347
|
+
var SITEMAP_OWNERS = ["own", "competitor", "other"];
|
|
8348
|
+
var SITEMAP_USES = {
|
|
8349
|
+
own: ["post_ideas", "channel_ideas", "internal_links", "post_matching"],
|
|
8350
|
+
competitor: ["post_ideas", "channel_ideas"],
|
|
8351
|
+
other: ["post_ideas", "channel_ideas"]
|
|
8352
|
+
};
|
|
8353
|
+
function sitemapUrl(input) {
|
|
8354
|
+
let v = input.trim();
|
|
8355
|
+
if (!v || /^[a-z][a-z0-9+.-]*:(?!\/\/|\d)/i.test(v)) return null;
|
|
8356
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(v)) v = `https://${v}`;
|
|
8357
|
+
let u;
|
|
8358
|
+
try {
|
|
8359
|
+
u = new URL(v);
|
|
8360
|
+
} catch {
|
|
8361
|
+
return null;
|
|
8362
|
+
}
|
|
8363
|
+
if (u.protocol !== "http:" && u.protocol !== "https:" || u.username || u.password) return null;
|
|
8364
|
+
if (!u.hostname.includes(".") || /\s/.test(v)) return null;
|
|
8365
|
+
u.protocol = "https:";
|
|
8366
|
+
return u.toString();
|
|
8367
|
+
}
|
|
8368
|
+
function sitemapUses(owner, uses) {
|
|
8369
|
+
const allowed2 = SITEMAP_USES[owner];
|
|
8370
|
+
const kept = allowed2.filter((u) => uses?.includes(u));
|
|
8371
|
+
return kept.length ? kept : [...allowed2];
|
|
8372
|
+
}
|
|
8373
|
+
function onWebsite(url, domain) {
|
|
8374
|
+
let host;
|
|
8375
|
+
try {
|
|
8376
|
+
host = new URL(url).hostname.toLowerCase();
|
|
8377
|
+
} catch {
|
|
8378
|
+
return false;
|
|
8379
|
+
}
|
|
8380
|
+
const d = domain.trim().toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/^www\./, "");
|
|
8381
|
+
return Boolean(d) && (host === d || host.endsWith(`.${d}`));
|
|
8382
|
+
}
|
|
8383
|
+
function cleanSitemap(entry, domain) {
|
|
8384
|
+
const url = sitemapUrl(entry.url ?? "");
|
|
8385
|
+
if (!url) return "The sitemap must be a web address starting with http:// or https://.";
|
|
8386
|
+
let owner = SITEMAP_OWNERS.includes(entry.owner) ? entry.owner : "other";
|
|
8387
|
+
if (owner === "own" && domain && !onWebsite(url, domain)) owner = "other";
|
|
8388
|
+
const label = (entry.label ?? "").trim().slice(0, SITEMAP_LABEL_MAX);
|
|
8389
|
+
const out = { ...entry, url, owner, label: label || null, uses: sitemapUses(owner, entry.uses) };
|
|
8390
|
+
if (!out.id) delete out.id;
|
|
8391
|
+
return out;
|
|
8392
|
+
}
|
|
8393
|
+
function checkSitemaps(list2) {
|
|
8394
|
+
if (list2.length > SITEMAPS_MAX) return `Up to ${SITEMAPS_MAX} sitemaps. Remove one first.`;
|
|
8395
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8396
|
+
for (const s of list2) {
|
|
8397
|
+
const k = s.url.trim().toLowerCase();
|
|
8398
|
+
if (seen.has(k)) return `${s.url} is already in the list.`;
|
|
8399
|
+
seen.add(k);
|
|
8400
|
+
}
|
|
8401
|
+
return null;
|
|
8402
|
+
}
|
|
8403
|
+
function sitemapCheckLine(check) {
|
|
8404
|
+
if (!check || check.status === "unchecked") return "Checked when you save";
|
|
8405
|
+
if (check.status === "error") return check.error?.trim() || "Couldn't read this sitemap.";
|
|
8406
|
+
const n = check.url_count;
|
|
8407
|
+
const found = n == null ? "Read" : `Found ${n.toLocaleString("en-GB")} ${n === 1 ? "page" : "pages"}`;
|
|
8408
|
+
const at = check.checked_at ? new Date(check.checked_at) : null;
|
|
8409
|
+
const when = at && !Number.isNaN(at.getTime()) ? at.toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" }) : null;
|
|
8410
|
+
return when ? `${found} \xB7 checked ${when}` : found;
|
|
8411
|
+
}
|
|
8412
|
+
|
|
8344
8413
|
// ../../packages/domain/src/copy/maggie.ts
|
|
8345
8414
|
var q = (s) => `\u201C${String(s ?? "").slice(0, 120)}\u201D`;
|
|
8346
8415
|
var named = (title, id) => {
|
|
@@ -8428,6 +8497,22 @@ var confirmCopy = {
|
|
|
8428
8497
|
en: (a) => `Remove the competitor ${q(a.name)}.`,
|
|
8429
8498
|
"zh-Hant": (a) => `\u522A\u9664\u7AF6\u722D\u5C0D\u624B ${q(a.name)}\u3002`
|
|
8430
8499
|
},
|
|
8500
|
+
sitemap_upsert: {
|
|
8501
|
+
en: (a) => {
|
|
8502
|
+
const whose = { own: "your site's ", competitor: "a competitor's ", other: "another site's " }[a.owner];
|
|
8503
|
+
const label = a.label ? ` (${q(a.label)})` : "";
|
|
8504
|
+
return a.url ? `Save ${whose ?? "the "}sitemap ${a.url}${label}.` : `Change the sitemap${label}: ${keys(a).filter((k) => k !== "id").join(", ")}.`;
|
|
8505
|
+
},
|
|
8506
|
+
"zh-Hant": (a) => {
|
|
8507
|
+
const whose = { own: "\u81EA\u5DF1\u7DB2\u7AD9\u7684", competitor: "\u7AF6\u722D\u5C0D\u624B\u7684", other: "\u5176\u4ED6\u7DB2\u7AD9\u7684" }[a.owner];
|
|
8508
|
+
const label = a.label ? `\uFF08${q(a.label)}\uFF09` : "";
|
|
8509
|
+
return a.url ? `\u5132\u5B58${whose ?? ""} sitemap ${a.url}${label}\u3002` : `\u4FEE\u6539 sitemap${label}\uFF1A${keys(a).filter((k) => k !== "id").join("\u3001")}\u3002`;
|
|
8510
|
+
}
|
|
8511
|
+
},
|
|
8512
|
+
sitemap_remove: {
|
|
8513
|
+
en: (a) => a.url ?? a.id ? `Remove the sitemap ${a.url ?? a.id}.` : "Remove a sitemap.",
|
|
8514
|
+
"zh-Hant": (a) => a.url ?? a.id ? `\u522A\u9664 sitemap ${a.url ?? a.id}\u3002` : "\u522A\u9664\u4E00\u500B sitemap\u3002"
|
|
8515
|
+
},
|
|
8431
8516
|
brand_research_update: {
|
|
8432
8517
|
en: (a) => `Update your market notes: ${keys(a).join(", ") || "nothing"}.`,
|
|
8433
8518
|
"zh-Hant": (a) => `\u66F4\u65B0\u5E02\u5834\u8CC7\u6599\uFF1A${keys(a).join("\u3001") || "\u6C92\u6709"}\u3002`
|
|
@@ -9880,6 +9965,85 @@ var brandTools = [
|
|
|
9880
9965
|
return { found: out.found, suggestions: out.items.map(({ id, name, website }) => ({ id, name, website })) };
|
|
9881
9966
|
}
|
|
9882
9967
|
}),
|
|
9968
|
+
tool({
|
|
9969
|
+
name: "sitemap_list",
|
|
9970
|
+
description: "The sitemaps saved in the business profile: the website's own, competitors' and other sites' (industry sites, partners), each with what it is used for (post_ideas = blog post ideas, channel_ideas = social post ideas, internal_links and post_matching = own sitemaps only) and its last check (pages found or the error). Max 20.",
|
|
9971
|
+
parameters: none,
|
|
9972
|
+
group: "brand",
|
|
9973
|
+
need: { resource: "overview", level: "read" },
|
|
9974
|
+
risk: "R0",
|
|
9975
|
+
run: async (ctx) => {
|
|
9976
|
+
const list2 = (await ws(ctx).brand()).sitemaps ?? [];
|
|
9977
|
+
return { max: SITEMAPS_MAX, sitemaps: list2.map(sitemapResult) };
|
|
9978
|
+
}
|
|
9979
|
+
}),
|
|
9980
|
+
tool({
|
|
9981
|
+
name: "sitemap_upsert",
|
|
9982
|
+
description: "Add a sitemap to the business profile, or change one (found by id from sitemap_list, else by url). owner: own (the website's own; its host must be the website or a subdomain, otherwise it is saved as other), competitor, or other. uses: own may have post_ideas, channel_ideas, internal_links, post_matching; competitor/other only post_ideas, channel_ideas; leave out for all allowed. A new sitemap needs url and owner. Only sitemap addresses the user gives or confirms (never guess one). Saving checks the sitemap; an unreachable one is still saved with the error. Max 20.",
|
|
9983
|
+
parameters: {
|
|
9984
|
+
type: "object",
|
|
9985
|
+
properties: {
|
|
9986
|
+
id: { type: "string", description: "The sitemap to change (from sitemap_list)" },
|
|
9987
|
+
url: { type: "string", description: "The sitemap's http(s) address; with id, its new address" },
|
|
9988
|
+
owner: { type: "string", enum: ["own", "competitor", "other"] },
|
|
9989
|
+
label: { type: "string", description: "A short name, up to 80 characters" },
|
|
9990
|
+
uses: {
|
|
9991
|
+
type: "array",
|
|
9992
|
+
items: { type: "string", enum: ["post_ideas", "channel_ideas", "internal_links", "post_matching"] }
|
|
9993
|
+
}
|
|
9994
|
+
}
|
|
9995
|
+
},
|
|
9996
|
+
group: "brand",
|
|
9997
|
+
need: write2,
|
|
9998
|
+
risk: "W1",
|
|
9999
|
+
run: async (ctx, args) => {
|
|
10000
|
+
const list2 = (await ws(ctx).brand()).sitemaps ?? [];
|
|
10001
|
+
const i = findSitemap(list2, args.id, args.id ? void 0 : args.url);
|
|
10002
|
+
if (args.id && i < 0) throw new Error(`No sitemap with id ${args.id}. Use sitemap_list.`);
|
|
10003
|
+
const old = i >= 0 ? list2[i] : void 0;
|
|
10004
|
+
if (!old && (!args.url || !args.owner)) throw new Error("A new sitemap needs url and owner.");
|
|
10005
|
+
const owner = args.owner ?? old?.owner ?? "other";
|
|
10006
|
+
const entry = cleanSitemap(
|
|
10007
|
+
{
|
|
10008
|
+
...old,
|
|
10009
|
+
url: args.url ?? old?.url ?? "",
|
|
10010
|
+
owner,
|
|
10011
|
+
label: args.label ?? old?.label ?? null,
|
|
10012
|
+
// A new owner without uses gets that owner's defaults.
|
|
10013
|
+
uses: args.uses ?? (args.owner && args.owner !== old?.owner ? [] : old?.uses ?? [])
|
|
10014
|
+
},
|
|
10015
|
+
ctx.domain ?? void 0
|
|
10016
|
+
);
|
|
10017
|
+
if (typeof entry === "string") throw new Error(entry);
|
|
10018
|
+
if (old && sitemapUrl(old.url) !== entry.url) delete entry.check;
|
|
10019
|
+
const next = old ? list2.map((x, n) => n === i ? entry : x) : [...list2, entry];
|
|
10020
|
+
const problem = checkSitemaps(next);
|
|
10021
|
+
if (problem) throw new Error(problem);
|
|
10022
|
+
const saved = (await ws(ctx).updateBrand({ sitemaps: next })).sitemaps ?? next;
|
|
10023
|
+
const mine = saved[findSitemap(saved, void 0, entry.url)];
|
|
10024
|
+
return {
|
|
10025
|
+
saved: mine ? sitemapResult(mine) : sitemapResult(entry),
|
|
10026
|
+
...(mine?.owner ?? entry.owner) !== owner ? { note: "Not on this website, so it was saved as another site's sitemap (owner other)." } : {},
|
|
10027
|
+
sitemaps: saved.length
|
|
10028
|
+
};
|
|
10029
|
+
}
|
|
10030
|
+
}),
|
|
10031
|
+
tool({
|
|
10032
|
+
name: "sitemap_remove",
|
|
10033
|
+
description: "Remove a sitemap from the business profile (by id from sitemap_list, or by url). Maggie stops reading it; ideas already found stay.",
|
|
10034
|
+
parameters: { type: "object", properties: { id: { type: "string" }, url: { type: "string" } } },
|
|
10035
|
+
group: "brand",
|
|
10036
|
+
need: write2,
|
|
10037
|
+
risk: "W1",
|
|
10038
|
+
run: async (ctx, { id, url }) => {
|
|
10039
|
+
if (!id && !url) throw new Error("Say which sitemap: id or url.");
|
|
10040
|
+
const list2 = (await ws(ctx).brand()).sitemaps ?? [];
|
|
10041
|
+
const i = findSitemap(list2, id, url);
|
|
10042
|
+
if (i < 0) throw new Error(`No sitemap ${id ?? url} was found. Use sitemap_list.`);
|
|
10043
|
+
const saved = (await ws(ctx).updateBrand({ sitemaps: list2.filter((_, n) => n !== i) })).sitemaps ?? [];
|
|
10044
|
+
return { removed: list2[i]?.url, sitemaps: saved.map((s) => s.url) };
|
|
10045
|
+
}
|
|
10046
|
+
}),
|
|
9883
10047
|
tool({
|
|
9884
10048
|
name: "brand_research_update",
|
|
9885
10049
|
description: "Change the profile's market notes: market_research (text), additional_information (anything else Maggie should know) or the SWOT list (each {name: Strengths|Weaknesses|Opportunities|Threats, description}). A list replaces the whole list.",
|
|
@@ -10015,6 +10179,22 @@ function brandGaps(b) {
|
|
|
10015
10179
|
check("industries", b.industries, "the industry, for examples and wording");
|
|
10016
10180
|
return { complete: gaps.length === 0, gaps };
|
|
10017
10181
|
}
|
|
10182
|
+
function findSitemap(list2, id, url) {
|
|
10183
|
+
if (id) return list2.findIndex((s) => s.id === id);
|
|
10184
|
+
const u = url ? (sitemapUrl(url) ?? url).toLowerCase() : "";
|
|
10185
|
+
return u ? list2.findIndex((s) => (sitemapUrl(s.url) ?? s.url).toLowerCase() === u) : -1;
|
|
10186
|
+
}
|
|
10187
|
+
function sitemapResult(s) {
|
|
10188
|
+
return {
|
|
10189
|
+
id: s.id ?? null,
|
|
10190
|
+
url: s.url,
|
|
10191
|
+
owner: s.owner,
|
|
10192
|
+
label: s.label ?? null,
|
|
10193
|
+
uses: s.uses,
|
|
10194
|
+
check: s.check?.status ?? "unchecked",
|
|
10195
|
+
status: sitemapCheckLine(s.check)
|
|
10196
|
+
};
|
|
10197
|
+
}
|
|
10018
10198
|
|
|
10019
10199
|
// ../../packages/tools/src/feedback.ts
|
|
10020
10200
|
var account = { resource: "account", level: "read" };
|
|
@@ -22422,7 +22602,9 @@ var OPEN_WORLD = /* @__PURE__ */ new Set([
|
|
|
22422
22602
|
"seo_audit_run",
|
|
22423
22603
|
"ai_visibility_prompt_check",
|
|
22424
22604
|
"idea_add",
|
|
22425
|
-
"competitor_suggestions_find"
|
|
22605
|
+
"competitor_suggestions_find",
|
|
22606
|
+
// Saving makes CMO.so fetch the sitemap from its website.
|
|
22607
|
+
"sitemap_upsert"
|
|
22426
22608
|
]);
|
|
22427
22609
|
var OVERWRITES = /* @__PURE__ */ new Set([
|
|
22428
22610
|
"ai_visibility_keywords_set",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cmo-so/mcp",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.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",
|