@cmo-so/mcp 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cmo-mcp.mjs +179 -11
- 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.
|
|
7202
|
+
version: "0.6.0",
|
|
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",
|
|
@@ -7537,7 +7537,8 @@ var MEMORY_CHANNELS = [
|
|
|
7537
7537
|
"facebook",
|
|
7538
7538
|
"instagram",
|
|
7539
7539
|
"google_business",
|
|
7540
|
-
"youtube"
|
|
7540
|
+
"youtube",
|
|
7541
|
+
"tiktok"
|
|
7541
7542
|
];
|
|
7542
7543
|
function addWebsite(api2, url) {
|
|
7543
7544
|
return api2.request("/v1/domains", {
|
|
@@ -7789,6 +7790,20 @@ function domainApi(api2, domain) {
|
|
|
7789
7790
|
),
|
|
7790
7791
|
/** Disconnect: the token is deleted and Google's access revoked (YouTube's policy). */
|
|
7791
7792
|
disconnectYouTube: (connectionId) => api2.request(`${base}/social/youtube/${connectionId}`, { method: "DELETE" }),
|
|
7793
|
+
/** A TikTok sign-in link for this website (docs/prd/tiktok.md §7); back with ?tiktok=connected (error&message=…). */
|
|
7794
|
+
connectTikTok: (returnTo = "close") => post(`${base}/social/tiktok/connect`, { return_to: returnTo }),
|
|
7795
|
+
/** What the TikTok account allows right now: show it in the TikTok post editor (TikTok's guidelines). */
|
|
7796
|
+
tiktokCreator: () => api2.request(`${base}/social/tiktok/creator`),
|
|
7797
|
+
/** Disconnect: TikTok's access is revoked and the token deleted. */
|
|
7798
|
+
disconnectTikTok: (connectionId) => api2.request(`${base}/social/tiktok/${connectionId}`, { method: "DELETE" }),
|
|
7799
|
+
/** A TikTok post's settings: who can view it, comments / Duet / Stitch, content disclosure, AI label. */
|
|
7800
|
+
setSocialTikTok: (id, answers) => api2.request(`${base}/social/${id}`, {
|
|
7801
|
+
method: "PATCH",
|
|
7802
|
+
headers: { "Content-Type": "application/json" },
|
|
7803
|
+
body: JSON.stringify({ tiktok: answers })
|
|
7804
|
+
}),
|
|
7805
|
+
/** Ask to join the beta of these channels: an email says what we need (reply or WhatsApp); the team is told. */
|
|
7806
|
+
requestBeta: (req) => post(`${base}/social/beta-request`, req),
|
|
7792
7807
|
/** One channel's own pause switch (pauseSocial / resumeSocial hold every channel). */
|
|
7793
7808
|
setChannelPaused: (channel, paused) => post(`${base}/social/channels/${channel}/${paused ? "pause" : "resume"}`),
|
|
7794
7809
|
/** How often one channel posts. */
|
|
@@ -7971,8 +7986,13 @@ function domainApi(api2, domain) {
|
|
|
7971
7986
|
video: (videoId) => api2.request(`${base}/social/video/${videoId}`),
|
|
7972
7987
|
/** Another part or framing; prepared again in the background. */
|
|
7973
7988
|
editVideo: (videoId, edit) => post(`${base}/social/video/${videoId}/edit`, { edit }),
|
|
7974
|
-
/** A YouTube
|
|
7989
|
+
/** A post on one video channel (YouTube by default, or TikTok) from a ready video, waiting in Review; Maggie
|
|
7990
|
+
* writes the details (YouTube) or the caption (TikTok) unless write is false. */
|
|
7975
7991
|
postFromVideo: (videoId, opts = {}) => post(`${base}/social/video/${videoId}/post`, opts),
|
|
7992
|
+
/** Make a LinkedIn, Facebook or Instagram post a video post: it goes out as this video (its picture is dropped). */
|
|
7993
|
+
attachVideo: (id, videoId) => post(`${base}/social/${id}/video`, { video_id: videoId }),
|
|
7994
|
+
/** Take the video off a post: it's a text post again. */
|
|
7995
|
+
detachVideo: (id) => api2.request(`${base}/social/${id}/video`, { method: "DELETE" }),
|
|
7976
7996
|
youtubeSettings: () => api2.request(`${base}/social/youtube/settings`),
|
|
7977
7997
|
setYouTubeSettings: (s) => put(`${base}/social/youtube/settings`, s),
|
|
7978
7998
|
/** A YouTube post's title, tags, visibility, made-for-kids or altered-content answer. */
|
|
@@ -8220,6 +8240,9 @@ var list = (xs, n = 5) => {
|
|
|
8220
8240
|
};
|
|
8221
8241
|
var ids = (a) => (Array.isArray(a.ids) && a.ids.length ? a.ids : a.id != null ? [a.id] : []).map(Number);
|
|
8222
8242
|
var keys = (a) => Object.keys(a ?? {}).filter((k) => a[k] !== void 0);
|
|
8243
|
+
var videoChannels = (a) => (Array.isArray(a.channels) && a.channels.length ? a.channels : ["youtube"]).map(
|
|
8244
|
+
(c) => ({ tiktok: "TikTok", linkedin: "LinkedIn", facebook: "Facebook", instagram: "Instagram" })[String(c)] ?? "YouTube"
|
|
8245
|
+
);
|
|
8223
8246
|
var confirmCopy = {
|
|
8224
8247
|
// --------------------------------------------------------- account / setup
|
|
8225
8248
|
website_add: {
|
|
@@ -8405,9 +8428,31 @@ var confirmCopy = {
|
|
|
8405
8428
|
en: () => "Make a link to connect your YouTube channel.",
|
|
8406
8429
|
"zh-Hant": () => "\u7522\u751F\u9023\u63A5\u4F60 YouTube \u983B\u9053\u7684\u9023\u7D50\u3002"
|
|
8407
8430
|
},
|
|
8431
|
+
social_video_attach: {
|
|
8432
|
+
en: (a) => `Make post #${a.id} a video post with ${a.url} (its picture is removed).`,
|
|
8433
|
+
"zh-Hant": (a) => `\u628A\u8CBC\u6587 #${a.id} \u6539\u6210\u5F71\u7247\u8CBC\u6587\uFF0C\u7528 ${a.url}\uFF08\u5716\u7247\u6703\u79FB\u9664\uFF09\u3002`
|
|
8434
|
+
},
|
|
8435
|
+
social_video_detach: {
|
|
8436
|
+
en: (a) => `Take the video off post #${a.id} (it's a text post again).`,
|
|
8437
|
+
"zh-Hant": (a) => `\u79FB\u9664\u8CBC\u6587 #${a.id} \u7684\u5F71\u7247\uFF08\u8B8A\u56DE\u6587\u5B57\u8CBC\u6587\uFF09\u3002`
|
|
8438
|
+
},
|
|
8439
|
+
tiktok_connect_link: {
|
|
8440
|
+
en: () => "Make a link to connect your TikTok account.",
|
|
8441
|
+
"zh-Hant": () => "\u7522\u751F\u9023\u63A5\u4F60 TikTok \u5E33\u865F\u7684\u9023\u7D50\u3002"
|
|
8442
|
+
},
|
|
8408
8443
|
social_video_add: {
|
|
8409
|
-
en: (a) =>
|
|
8410
|
-
|
|
8444
|
+
en: (a) => {
|
|
8445
|
+
const names = videoChannels(a).join(" and ");
|
|
8446
|
+
return `Make a ${names} post from ${a.url}${a.write === false ? "" : ` (Maggie writes it: one AI generation per channel)`}; it waits for your OK.`;
|
|
8447
|
+
},
|
|
8448
|
+
"zh-Hant": (a) => {
|
|
8449
|
+
const names = videoChannels(a).join(" \u548C ");
|
|
8450
|
+
return `\u7528 ${a.url} \u505A ${names} \u5F71\u7247\u8CBC\u6587${a.write === false ? "" : "\uFF08Maggie \u4F86\u5BEB\uFF1A\u6BCF\u500B\u6E20\u9053\u7528\u4E00\u6B21 AI \u751F\u6210\uFF09"}\uFF0C\u7B49\u4F60\u78BA\u8A8D\u5F8C\u624D\u767C\u3002`;
|
|
8451
|
+
}
|
|
8452
|
+
},
|
|
8453
|
+
social_tiktok_set: {
|
|
8454
|
+
en: (a) => `Set TikTok post #${a.id}: ${keys(a).filter((k) => k !== "id").map((k) => `${k} ${String(a[k])}`).join(", ") || "its settings"}.`,
|
|
8455
|
+
"zh-Hant": (a) => `\u8A2D\u5B9A TikTok \u8CBC\u6587 #${a.id}\uFF1A${keys(a).filter((k) => k !== "id").map((k) => `${k} ${String(a[k])}`).join("\u3001") || "\u8A2D\u5B9A"}\u3002`
|
|
8411
8456
|
},
|
|
8412
8457
|
social_youtube_set: {
|
|
8413
8458
|
en: (a) => `Change YouTube post #${a.id}: ${keys(a).filter((k) => k !== "id").join(", ") || "its details"}.`,
|
|
@@ -10430,11 +10475,13 @@ var read5 = { resource: "autopilot", level: "read" };
|
|
|
10430
10475
|
var write5 = { resource: "autopilot", level: "write" };
|
|
10431
10476
|
var moves2 = ["up", "down", "top"];
|
|
10432
10477
|
var CHANNEL_KEYS = ["linkedin", "facebook", "instagram", "google_business"];
|
|
10433
|
-
var ALL_CHANNEL_KEYS = [...CHANNEL_KEYS, "youtube"];
|
|
10478
|
+
var ALL_CHANNEL_KEYS = [...CHANNEL_KEYS, "youtube", "tiktok"];
|
|
10479
|
+
var VIDEO_KEYS = ["linkedin", "facebook", "instagram", "youtube", "tiktok"];
|
|
10480
|
+
var TIKTOK_PRIVACY = ["PUBLIC_TO_EVERYONE", "MUTUAL_FOLLOW_FRIENDS", "FOLLOWER_OF_CREATOR", "SELF_ONLY"];
|
|
10434
10481
|
var channelsParam = {
|
|
10435
10482
|
type: "array",
|
|
10436
10483
|
items: { type: "string", enum: CHANNEL_KEYS },
|
|
10437
|
-
description: "Which channels to write for: linkedin, facebook, instagram, google_business. Leave out to write for every connected channel (one post each, one credit each); give one only when the user names it. YouTube
|
|
10484
|
+
description: "Which channels to write for: linkedin, facebook, instagram, google_business. Leave out to write for every connected channel (one post each, one credit each); give one only when the user names it. YouTube and TikTok take a video: use social_video_add."
|
|
10438
10485
|
};
|
|
10439
10486
|
var channelsOf = (value) => {
|
|
10440
10487
|
const list2 = Array.isArray(value) ? value.map((v) => String(v).toLowerCase()).filter((v) => CHANNEL_KEYS.includes(v)) : [];
|
|
@@ -10461,6 +10508,13 @@ var item = (i) => ({
|
|
|
10461
10508
|
...i.provider === "youtube" ? {
|
|
10462
10509
|
youtube: i.youtube ?? null,
|
|
10463
10510
|
video: i.video ? { status: i.video.status, seconds: i.video.duration, shape: i.video.shape, error: i.video.error } : null
|
|
10511
|
+
} : {},
|
|
10512
|
+
...i.video_asset_id && i.provider !== "youtube" && i.provider !== "tiktok" ? {
|
|
10513
|
+
video: i.video ? { status: i.video.status, seconds: i.video.duration, shape: i.video.shape, error: i.video.error } : { id: i.video_asset_id }
|
|
10514
|
+
} : {},
|
|
10515
|
+
...i.provider === "tiktok" ? {
|
|
10516
|
+
tiktok: i.tiktok ?? null,
|
|
10517
|
+
video: i.video ? { status: i.video.status, seconds: i.video.duration, shape: i.video.shape, error: i.video.error } : null
|
|
10464
10518
|
} : {}
|
|
10465
10519
|
});
|
|
10466
10520
|
var socialTools = [
|
|
@@ -11439,13 +11493,110 @@ var socialTools = [
|
|
|
11439
11493
|
return { url: out.url, expires_in_seconds: out.expires_in_seconds };
|
|
11440
11494
|
}
|
|
11441
11495
|
}),
|
|
11496
|
+
tool({
|
|
11497
|
+
name: "social_video_attach",
|
|
11498
|
+
description: "Make a LinkedIn, Facebook or Instagram post that hasn't gone out a video post: it goes out as this video (a DIRECT link to a video file, 5-30 seconds; never a platform page) with its text, and its picture is removed (a video post has no picture). Not for Google Business (posts can't carry video). Preview first.",
|
|
11499
|
+
parameters: {
|
|
11500
|
+
type: "object",
|
|
11501
|
+
properties: {
|
|
11502
|
+
id: { type: "integer", description: "The post (from social_list)" },
|
|
11503
|
+
url: { type: "string", description: "Direct https link to the video file" }
|
|
11504
|
+
},
|
|
11505
|
+
required: ["id", "url"]
|
|
11506
|
+
},
|
|
11507
|
+
group: "social",
|
|
11508
|
+
need: write5,
|
|
11509
|
+
risk: "W1",
|
|
11510
|
+
gates: ["VER"],
|
|
11511
|
+
run: async (ctx, a) => {
|
|
11512
|
+
const d = ws(ctx);
|
|
11513
|
+
let v = await d.videoFromLink(String(a.url));
|
|
11514
|
+
const until = Date.now() + 15e4;
|
|
11515
|
+
while ((v.status === "checking" || v.status === "uploading") && Date.now() < until) {
|
|
11516
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
11517
|
+
v = await d.video(v.id);
|
|
11518
|
+
}
|
|
11519
|
+
if (v.status !== "ready")
|
|
11520
|
+
return { ok: false, video: { status: v.status, error: v.error, seconds: v.source_duration } };
|
|
11521
|
+
return { ok: true, post: item(await d.attachVideo(Number(a.id), v.id)) };
|
|
11522
|
+
}
|
|
11523
|
+
}),
|
|
11524
|
+
tool({
|
|
11525
|
+
name: "social_video_detach",
|
|
11526
|
+
description: "Take the video off a LinkedIn, Facebook or Instagram post that hasn't gone out: it's a text post again (a picture can be added after). Preview first.",
|
|
11527
|
+
parameters: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
|
|
11528
|
+
group: "social",
|
|
11529
|
+
need: write5,
|
|
11530
|
+
risk: "W1",
|
|
11531
|
+
gates: ["VER"],
|
|
11532
|
+
run: async (ctx, a) => item(await ws(ctx).detachVideo(Number(a.id)))
|
|
11533
|
+
}),
|
|
11534
|
+
tool({
|
|
11535
|
+
name: "tiktok_connect_link",
|
|
11536
|
+
description: "A link for the user to connect their TikTok account (or reconnect): they sign in to TikTok and allow Maggie to see their profile and post videos. Give them the link to open; never ask for a password. TikTok is in beta (testers only; until TikTok reviews our app, videos post as 'Only me' and the account must be private). TikTok posts are short videos (5-30 seconds) that the user approves one by one.",
|
|
11537
|
+
parameters: none,
|
|
11538
|
+
group: "social",
|
|
11539
|
+
need: write5,
|
|
11540
|
+
risk: "W1",
|
|
11541
|
+
gates: ["VER"],
|
|
11542
|
+
run: async (ctx) => {
|
|
11543
|
+
const out = await ws(ctx).connectTikTok("close");
|
|
11544
|
+
return { url: out.url, expires_in_seconds: out.expires_in_seconds };
|
|
11545
|
+
}
|
|
11546
|
+
}),
|
|
11547
|
+
tool({
|
|
11548
|
+
name: "tiktok_creator",
|
|
11549
|
+
description: "What the user's TikTok account allows right now (TikTok asks to check before every post): its name and @username, who a video can be shown to (privacy_options), whether comments / Duet / Stitch are turned off on the account, the longest video it can post, and whether it can post now (problem). Read it before asking the user the TikTok settings of a post.",
|
|
11550
|
+
parameters: none,
|
|
11551
|
+
group: "social",
|
|
11552
|
+
need: read5,
|
|
11553
|
+
risk: "R0",
|
|
11554
|
+
run: async (ctx) => ws(ctx).tiktokCreator()
|
|
11555
|
+
}),
|
|
11556
|
+
tool({
|
|
11557
|
+
name: "social_tiktok_set",
|
|
11558
|
+
description: "Set a TikTok post's settings, ONLY as the user chose them (TikTok's rules: never pick for them, nothing is preset): privacy (one of the account's privacy_options from tiktok_creator: PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY), allow_comment / allow_duet / allow_stitch (off unless the user turns them on; not possible when the account turned them off), disclose (the video promotes a brand, product or service) with your_brand (their own: labelled 'Promotional content') and / or branded (a third party: labelled 'Paid partnership'; can't be SELF_ONLY), is_aigc (AI-generated content label). The caption is the post's text (social_update_copy). Before the user approves, tell them: 'By posting, you agree to TikTok's Music Usage Confirmation' (with branded content: 'TikTok's Branded Content Policy and Music Usage Confirmation'). Preview first.",
|
|
11559
|
+
parameters: {
|
|
11560
|
+
type: "object",
|
|
11561
|
+
properties: {
|
|
11562
|
+
id: { type: "integer", description: "A TikTok post (from social_list)" },
|
|
11563
|
+
privacy: { type: "string", enum: TIKTOK_PRIVACY },
|
|
11564
|
+
allow_comment: { type: "boolean" },
|
|
11565
|
+
allow_duet: { type: "boolean" },
|
|
11566
|
+
allow_stitch: { type: "boolean" },
|
|
11567
|
+
disclose: { type: "boolean" },
|
|
11568
|
+
your_brand: { type: "boolean" },
|
|
11569
|
+
branded: { type: "boolean" },
|
|
11570
|
+
is_aigc: { type: "boolean" }
|
|
11571
|
+
},
|
|
11572
|
+
required: ["id"]
|
|
11573
|
+
},
|
|
11574
|
+
group: "social",
|
|
11575
|
+
need: write5,
|
|
11576
|
+
risk: "W1",
|
|
11577
|
+
gates: ["VER"],
|
|
11578
|
+
run: async (ctx, { id, privacy, ...rest }) => {
|
|
11579
|
+
const answers = Object.fromEntries(
|
|
11580
|
+
Object.entries(rest).filter(([, v]) => v !== void 0)
|
|
11581
|
+
);
|
|
11582
|
+
if (privacy) answers.privacy = privacy;
|
|
11583
|
+
const out = await ws(ctx).setSocialTikTok(Number(id), answers);
|
|
11584
|
+
return item(out);
|
|
11585
|
+
}
|
|
11586
|
+
}),
|
|
11442
11587
|
tool({
|
|
11443
11588
|
name: "social_video_add",
|
|
11444
|
-
description: "Make
|
|
11589
|
+
description: "Make video posts (channels: linkedin, facebook, instagram, youtube, tiktok; default youtube; one post each, never google_business: Google posts can't carry video) from a video: a DIRECT link to a video file (an .mp4 on their website, a public Dropbox link), never a YouTube / TikTok / Instagram page (we never download those; tell the user to upload the video in the app instead). The video must be 5-30 seconds: give start/end (seconds) to use a part of a longer one. vertical: make a wide video vertical (a Short) cropped at left, middle or right. Maggie writes YouTube's title, description and tags, TikTok's caption, or the LinkedIn / Facebook / Instagram text, from the video and the note (one AI generation per channel) unless write is false. Each post waits in Review: video posts are always approved by the user. YouTube asks whether it's made for kids; TikTok asks who can view it and the other TikTok settings (social_tiktok_set, as the user chooses). is_aigc: TikTok's AI-generated label, only when the user says the video was made with AI. Preview first.",
|
|
11445
11590
|
parameters: {
|
|
11446
11591
|
type: "object",
|
|
11447
11592
|
properties: {
|
|
11448
11593
|
url: { type: "string", description: "Direct https link to the video file" },
|
|
11594
|
+
channels: {
|
|
11595
|
+
type: "array",
|
|
11596
|
+
items: { type: "string", enum: [...VIDEO_KEYS] },
|
|
11597
|
+
description: "Where the video goes (one post each); default youtube"
|
|
11598
|
+
},
|
|
11599
|
+
is_aigc: { type: "boolean", description: "TikTok: the video is AI-generated (the user said so)" },
|
|
11449
11600
|
note: { type: "string", description: "What the video is about, in the user's words" },
|
|
11450
11601
|
write: { type: "boolean", description: "Let Maggie write the details (default true)" },
|
|
11451
11602
|
start: { type: "number", description: "Start of the part to use, seconds" },
|
|
@@ -11473,11 +11624,28 @@ var socialTools = [
|
|
|
11473
11624
|
}
|
|
11474
11625
|
if (v.status !== "ready")
|
|
11475
11626
|
return { ok: false, video: { status: v.status, error: v.error, seconds: v.source_duration } };
|
|
11476
|
-
const
|
|
11627
|
+
const wanted = (a.channels ?? ["youtube"]).filter(
|
|
11628
|
+
(c) => VIDEO_KEYS.includes(c)
|
|
11629
|
+
);
|
|
11630
|
+
const posts = [];
|
|
11631
|
+
for (const channel of wanted.length ? wanted : ["youtube"]) {
|
|
11632
|
+
posts.push(
|
|
11633
|
+
await d.postFromVideo(v.id, {
|
|
11634
|
+
channel,
|
|
11635
|
+
note: a.note ?? "",
|
|
11636
|
+
write: a.write !== false,
|
|
11637
|
+
...channel === "tiktok" && a.is_aigc != null ? { is_aigc: a.is_aigc } : {}
|
|
11638
|
+
})
|
|
11639
|
+
);
|
|
11640
|
+
}
|
|
11477
11641
|
return {
|
|
11478
11642
|
ok: true,
|
|
11479
|
-
post: item(
|
|
11480
|
-
next:
|
|
11643
|
+
...posts.length === 1 ? { post: item(posts[0]) } : { posts: posts.map(item) },
|
|
11644
|
+
next: [
|
|
11645
|
+
posts.some((p) => p.provider === "youtube") ? "YouTube: show the title and description; ask whether it's made for kids and which visibility, then social_youtube_set." : "",
|
|
11646
|
+
posts.some((p) => p.provider === "tiktok") ? "TikTok: show the caption; read tiktok_creator and ask who can view it, whether to allow comments / Duet / Stitch and whether it promotes a brand, then social_tiktok_set exactly as they say; tell them the Music Usage Confirmation line." : "",
|
|
11647
|
+
"The user approves each post."
|
|
11648
|
+
].filter(Boolean).join(" ")
|
|
11481
11649
|
};
|
|
11482
11650
|
}
|
|
11483
11651
|
}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cmo-so/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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",
|