@cmo-so/mcp 0.9.0 → 0.9.2

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.
Files changed (2) hide show
  1. package/dist/cmo-mcp.mjs +159 -18
  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.9.0",
7202
+ version: "0.9.2",
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",
@@ -7784,8 +7784,14 @@ function domainApi(api2, domain) {
7784
7784
  /** How a posted Instagram post is doing (read from Instagram, kept 15 minutes; `refresh` reads again). */
7785
7785
  socialInsights: (id, refresh = false) => api2.request(`${base}/social/${id}/insights${refresh ? "?refresh=true" : ""}`),
7786
7786
  deletePostedSocial: (id) => api2.request(`${base}/social/${id}/published`, { method: "DELETE" }),
7787
- /** Change a posted LinkedIn post and put it up again: the old one comes down, the new text goes out now. */
7788
- republishSocial: (id, socialCopy) => post(`${base}/social/${id}/republish`, socialCopy ? { social_copy: socialCopy } : {}),
7787
+ /**
7788
+ * Change a posted post. LinkedIn changes its text in place (likes and comments stay); with `repost`, and on
7789
+ * the other channels, the old one comes down and the new text goes out now.
7790
+ */
7791
+ republishSocial: (id, socialCopy, repost = false) => post(`${base}/social/${id}/republish`, {
7792
+ ...socialCopy ? { social_copy: socialCopy } : {},
7793
+ ...repost ? { repost: true } : {}
7794
+ }),
7789
7795
  restoreSocial: (id) => post(`${base}/social/${id}/restore`),
7790
7796
  pauseSocial: () => post(`${base}/social/pause`),
7791
7797
  /**
@@ -8785,6 +8791,18 @@ var confirmCopy = {
8785
8791
  return n.length > 1 ? `\u6279\u51C6 ${n.length} \u7BC7\u8CBC\u6587\uFF1A${n.map((i) => `#${i}`).join("\u3001")}\u3002\u6BCF\u7BC7\u5728\u63A5\u4E0B\u4F86\u7684\u767C\u6587\u6642\u9593\u767C\u51FA\uFF0C\u4F60\u7684\u4EBA\u8108\u90FD\u770B\u5F97\u5230\u3002` : `\u6279\u51C6\u8CBC\u6587 #${n[0]}\u3002\u5B83\u6703\u5728\u4E0B\u4E00\u500B\u767C\u6587\u6642\u9593\u767C\u51FA\uFF0C\u4F60\u7684\u4EBA\u8108\u90FD\u770B\u5F97\u5230\u3002`;
8786
8792
  }
8787
8793
  },
8794
+ social_publish_now: {
8795
+ en: (a) => `Post #${a.id} on its channel now, not at its posting time (even while posting is paused). Everyone who follows you can see it.`,
8796
+ "zh-Hant": (a) => `\u7ACB\u5373\u5728\u5B83\u7684\u983B\u9053\u767C\u51FA\u8CBC\u6587 #${a.id}\uFF0C\u4E0D\u7B49\u767C\u6587\u6642\u9593\uFF08\u66AB\u505C\u767C\u6587\u6642\u4E5F\u6703\u767C\uFF09\u3002\u767C\u51FA\u5F8C\u95DC\u6CE8\u4F60\u7684\u4EBA\u90FD\u770B\u5F97\u5230\u3002`
8797
+ },
8798
+ social_posted_update: {
8799
+ en: (a) => a.repost ? `Take post #${a.id} down (its likes and comments go) and put it up again${a.social_copy ? ` as ${q(a.social_copy)}` : ""}.` : `Change the text of the posted post #${a.id} to ${q(a.social_copy)}. On LinkedIn it stays up with its likes and comments; on other channels it comes down and goes out again.`,
8800
+ "zh-Hant": (a) => a.repost ? `\u64A4\u4E0B\u8CBC\u6587 #${a.id}\uFF08\u8B9A\u548C\u7559\u8A00\u6703\u6D88\u5931\uFF09\uFF0C\u518D\u91CD\u65B0\u767C\u51FA${a.social_copy ? `\uFF0C\u5167\u5BB9\u6539\u70BA ${q(a.social_copy)}` : ""}\u3002` : `\u628A\u5DF2\u767C\u51FA\u7684\u8CBC\u6587 #${a.id} \u6539\u70BA ${q(a.social_copy)}\u3002LinkedIn \u4E0A\u539F\u5730\u4FEE\u6539\uFF0C\u8B9A\u548C\u7559\u8A00\u4FDD\u7559\uFF1B\u5176\u4ED6\u983B\u9053\u6703\u64A4\u4E0B\u5F8C\u91CD\u767C\u3002`
8801
+ },
8802
+ social_posted_delete: {
8803
+ en: (a) => `Take post #${a.id} down from its channel, with its likes and comments. This can't be undone.`,
8804
+ "zh-Hant": (a) => `\u628A\u8CBC\u6587 #${a.id} \u5F9E\u5B83\u7684\u983B\u9053\u64A4\u4E0B\uFF0C\u8B9A\u548C\u7559\u8A00\u4E00\u8D77\u522A\u9664\uFF0C\u7121\u6CD5\u5FA9\u539F\u3002`
8805
+ },
8788
8806
  social_skip: { en: (a) => `Skip post #${a.id}.`, "zh-Hant": (a) => `\u8DF3\u904E\u8CBC\u6587 #${a.id}\u3002` },
8789
8807
  social_restore: {
8790
8808
  en: (a) => `Bring back post #${a.id}.`,
@@ -8838,11 +8856,11 @@ var confirmCopy = {
8838
8856
  },
8839
8857
  social_pictures_attach: {
8840
8858
  en: (a) => {
8841
- const n = Array.isArray(a.urls) ? a.urls.length : 0;
8859
+ const n = (Array.isArray(a.urls) ? a.urls.length : 0) + (Array.isArray(a.files) ? a.files.length : 0);
8842
8860
  return `${a.append ? "Add" : "Put"} your ${n} picture${n === 1 ? "" : "s"} ${a.append ? "after the pictures on" : "on"} post #${a.id}${a.append ? "" : ", in place of its picture"} (free). On LinkedIn a set goes out as a swipeable PDF carousel.`;
8843
8861
  },
8844
8862
  "zh-Hant": (a) => {
8845
- const n = Array.isArray(a.urls) ? a.urls.length : 0;
8863
+ const n = (Array.isArray(a.urls) ? a.urls.length : 0) + (Array.isArray(a.files) ? a.files.length : 0);
8846
8864
  return `${a.append ? `\u5728\u8CBC\u6587 #${a.id} \u73FE\u6709\u5716\u7247\u5F8C\u52A0\u4E0A` : `\u628A\u8CBC\u6587 #${a.id} \u7684\u5716\u7247\u63DB\u6210`}\u4F60\u7684 ${n} \u5F35\u5716\u7247\uFF08\u514D\u8CBB\uFF09\u3002\u5728 LinkedIn \u4E0A\uFF0C\u4E00\u7D44\u5716\u7247\u6703\u4EE5\u53EF\u6ED1\u52D5\u7684 PDF \u8F2A\u64AD\u767C\u51FA\u3002`;
8847
8865
  }
8848
8866
  },
@@ -11613,6 +11631,17 @@ var channelsParam = {
11613
11631
  items: { type: "string", enum: CHANNEL_KEYS },
11614
11632
  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."
11615
11633
  };
11634
+ var FILE_SCHEMA = {
11635
+ type: "object",
11636
+ properties: {
11637
+ download_url: { type: "string" },
11638
+ file_id: { type: "string" },
11639
+ mime_type: { type: "string" },
11640
+ file_name: { type: "string" }
11641
+ },
11642
+ required: ["download_url", "file_id"]
11643
+ };
11644
+ var fileLinks = (value) => (Array.isArray(value) ? value : value ? [value] : []).map((f) => String(f?.download_url ?? "").trim()).filter(Boolean);
11616
11645
  var queuedAt = positionParam("the posts", "Leave out for the front (the next posting times).");
11617
11646
  var channelsOf = (value) => {
11618
11647
  const list2 = Array.isArray(value) ? value.map((v) => String(v).toLowerCase()).filter((v) => CHANNEL_KEYS.includes(v)) : [];
@@ -11736,6 +11765,59 @@ var socialTools = [
11736
11765
  return { approved: results.filter((r) => r.ok).length, results };
11737
11766
  }
11738
11767
  }),
11768
+ tool({
11769
+ name: "social_publish_now",
11770
+ description: "Post one social post on its channel now instead of at its posting time: a draft waiting for approval, a queued post, or one that didn't go out. It goes even while posting is paused. It goes out in the background: the answer says it is publishing; read social_post_get after a minute for its link (or why it couldn't go). Not while its picture is being made.",
11771
+ parameters: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
11772
+ group: "social",
11773
+ need: write5,
11774
+ risk: "W3",
11775
+ gates: ["VER"],
11776
+ run: async (ctx, { id }) => {
11777
+ const n = Number(id);
11778
+ if (!Number.isInteger(n) || n <= 0) throw new Error("Which post?");
11779
+ const post = await ws(ctx).publishSocialNow(n, { background: true });
11780
+ return post.status === "published" ? post : { ...post, note: "Posting now. Check social_post_get in a minute for its link." };
11781
+ }
11782
+ }),
11783
+ tool({
11784
+ name: "social_posted_update",
11785
+ description: "Change a social post that has already gone out. On LinkedIn the new text replaces the old in place: the post keeps its likes and comments (its pictures can't change this way). With repost, and on Facebook, Instagram and Google, the old post comes down with its likes and comments and the new version goes out now. Without social_copy and with repost, it goes out again as it is (e.g. after new pictures).",
11786
+ parameters: {
11787
+ type: "object",
11788
+ properties: {
11789
+ id: { type: "integer" },
11790
+ social_copy: { type: "string", description: "The post's new text" },
11791
+ repost: { type: "boolean", description: "Take it down and put it up again instead of editing it" }
11792
+ },
11793
+ required: ["id"]
11794
+ },
11795
+ group: "social",
11796
+ need: write5,
11797
+ risk: "W3",
11798
+ gates: ["VER"],
11799
+ run: async (ctx, { id, social_copy, repost }) => {
11800
+ const n = Number(id);
11801
+ if (!Number.isInteger(n) || n <= 0) throw new Error("Which post?");
11802
+ const copy = typeof social_copy === "string" ? social_copy.trim() : "";
11803
+ if (!copy && repost !== true) throw new Error("Give the new text (social_copy), or repost: true.");
11804
+ return ws(ctx).republishSocial(n, copy || void 0, repost === true);
11805
+ }
11806
+ }),
11807
+ tool({
11808
+ name: "social_posted_delete",
11809
+ description: "Take a social post that has gone out down from its channel (LinkedIn, Facebook, Instagram, Google). Its likes and comments go with it; this can't be undone.",
11810
+ parameters: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
11811
+ group: "social",
11812
+ need: write5,
11813
+ risk: "W3",
11814
+ gates: ["VER"],
11815
+ run: async (ctx, { id }) => {
11816
+ const n = Number(id);
11817
+ if (!Number.isInteger(n) || n <= 0) throw new Error("Which post?");
11818
+ return ws(ctx).deletePostedSocial(n);
11819
+ }
11820
+ }),
11739
11821
  tool({
11740
11822
  name: "social_skip",
11741
11823
  description: "Skip a LinkedIn post (it won't be posted; it can be restored).",
@@ -11982,27 +12064,35 @@ var socialTools = [
11982
12064
  }),
11983
12065
  tool({
11984
12066
  name: "social_pictures_attach",
11985
- description: "Put the user's own pictures on a post, in order: public https links to image files (JPEG, PNG or WebP), for example slides another assistant made. 1-20 pictures; append adds them after the post's current ones. Free (no AI). They replace Maggie's picture; a post from an idea shares them with its other channels. Instagram and Facebook post the first 10, Google Business the first. On LinkedIn a set goes out as a swipeable PDF carousel unless social_carousel_set says images. Preview first.",
12067
+ description: "Put the user's own pictures on a post, in order: pictures from this chat (files: ones the user added or that were made here) or public https links to image files (urls), JPEG, PNG or WebP, for example carousel slides. 1-20 pictures; append adds them after the post's current ones. Free (no AI). They replace Maggie's picture; a post from an idea shares them with its other channels. Instagram and Facebook post the first 10, Google Business the first. On LinkedIn a set goes out as a swipeable PDF carousel unless social_carousel_set says images. If you can pass neither, give the user social_upload_link. Preview first.",
11986
12068
  parameters: {
11987
12069
  type: "object",
11988
12070
  properties: {
11989
12071
  id: { type: "integer", description: "The post (from social_list), not gone out yet" },
12072
+ files: {
12073
+ type: "array",
12074
+ items: FILE_SCHEMA,
12075
+ description: "Pictures from this chat, in the order they should appear"
12076
+ },
11990
12077
  urls: {
11991
12078
  type: "array",
11992
12079
  items: { type: "string" },
11993
- description: "Public https links to the image files, in the order they should appear"
12080
+ description: "Public https links to the image files, in order (after the files, when both are given)"
11994
12081
  },
11995
12082
  append: { type: "boolean", description: "Add after the post's current pictures instead of replacing them" }
11996
12083
  },
11997
- required: ["id", "urls"]
12084
+ required: ["id"]
11998
12085
  },
12086
+ fileParams: ["files"],
11999
12087
  group: "social",
12000
12088
  need: write5,
12001
12089
  risk: "W1",
12002
12090
  gates: ["VER"],
12003
- run: async (ctx, { id, urls, append }) => {
12004
- const list2 = (Array.isArray(urls) ? urls : []).map((u) => String(u).trim()).filter(Boolean);
12005
- if (!list2.length) throw new Error("Give at least one https link to a picture.");
12091
+ run: async (ctx, { id, urls, files, append }) => {
12092
+ const list2 = [...fileLinks(files), ...(Array.isArray(urls) ? urls : []).map((u) => String(u).trim())].filter(
12093
+ Boolean
12094
+ );
12095
+ if (!list2.length) throw new Error("Give the pictures (files from this chat, or https links).");
12006
12096
  if (list2.length > 20) throw new Error("A post takes up to 20 pictures.");
12007
12097
  if (list2.some((u) => !u.startsWith("https://"))) throw new Error("Every picture needs a public https link.");
12008
12098
  const out = await ws(ctx).setSocialPictures(Number(id), { urls: list2, append: append === true });
@@ -12015,30 +12105,57 @@ var socialTools = [
12015
12105
  }),
12016
12106
  tool({
12017
12107
  name: "social_carousel_pdf_attach",
12018
- description: "A LinkedIn post goes out as this PDF: a swipeable document carousel (a public https link to the PDF, up to 300 pages and 20 MB, no password). Its first 20 pages also become the post's pictures (what its other channels post). Free. Change the pictures later and the carousel is made from them instead. Preview first.",
12108
+ description: "A LinkedIn post goes out as this PDF: a swipeable document carousel. The PDF from this chat (file: one the user added or that was made here) or a public https link to it (url); up to 300 pages and 20 MB, no password. Its first 20 pages also become the post's pictures (what its other channels post). Free. Change the pictures later and the carousel is made from them instead. If you can pass neither, give the user social_upload_link. Preview first.",
12019
12109
  parameters: {
12020
12110
  type: "object",
12021
12111
  properties: {
12022
12112
  id: { type: "integer", description: "A LinkedIn post (from social_list), not gone out yet" },
12113
+ file: { ...FILE_SCHEMA, description: "The PDF from this chat" },
12023
12114
  url: { type: "string", description: "Public https link to the PDF file" },
12024
12115
  title: {
12025
12116
  type: "string",
12026
12117
  description: "Shown on the carousel (up to 120 characters); the post's topic if left out"
12027
12118
  }
12028
12119
  },
12029
- required: ["id", "url"]
12120
+ required: ["id"]
12030
12121
  },
12122
+ fileParams: ["file"],
12031
12123
  group: "social",
12032
12124
  need: write5,
12033
12125
  risk: "W1",
12034
12126
  gates: ["VER", "LI"],
12035
- run: async (ctx, { id, url, title }) => {
12036
- const link = String(url ?? "").trim();
12037
- if (!link.startsWith("https://")) throw new Error("Give a public https link to the PDF.");
12127
+ run: async (ctx, { id, url, file, title }) => {
12128
+ const link = fileLinks(file)[0] ?? String(url ?? "").trim();
12129
+ if (!link.startsWith("https://"))
12130
+ throw new Error("Give the PDF (a file from this chat, or a public https link).");
12038
12131
  const out = await ws(ctx).attachSocialCarouselPdf(Number(id), { url: link, ...title ? { title } : {} });
12039
12132
  return { id: out.id, pages: out.pages, pictures: out.pictures, carousel: out.carousel ?? null };
12040
12133
  }
12041
12134
  }),
12135
+ tool({
12136
+ name: "social_upload_link",
12137
+ description: "A link that opens one post's picture in the CMO.so app, where the user uploads their own photos (up to 20, a carousel on LinkedIn) or, on a computer, a PDF carousel themselves. Use it when you can't pass the files (this chat can't send files to tools, or they are on the user's device). Give the link; nothing changes until they upload.",
12138
+ parameters: {
12139
+ type: "object",
12140
+ properties: { id: { type: "integer", description: "The post (from social_list), not gone out yet" } },
12141
+ required: ["id"]
12142
+ },
12143
+ group: "social",
12144
+ need: read5,
12145
+ risk: "R0",
12146
+ run: async (ctx, { id }) => {
12147
+ const post = await ws(ctx).socialPost(Number(id));
12148
+ if (["published", "publishing", "cancelled"].includes(String(post.status)))
12149
+ throw new Error("That post has already gone out (or was skipped): its pictures can't change.");
12150
+ const site = ctx.domain ? `&site=${encodeURIComponent(ctx.domain)}` : "";
12151
+ return {
12152
+ id: post.id,
12153
+ channel: post.provider ?? null,
12154
+ url: `https://app.cmo.so/social?picture=${post.id}${site}`,
12155
+ how: post.provider === "linkedin" ? "Open it, then Upload several photos (in order), or on a computer Upload a PDF carousel." : "Open it, then Upload several photos (in order)."
12156
+ };
12157
+ }
12158
+ }),
12042
12159
  tool({
12043
12160
  name: "social_carousel_set",
12044
12161
  description: "How a LinkedIn post with a set of pictures goes out: format pdf (a swipeable PDF carousel made from the pictures, the default) or images (several pictures); and the carousel's title. Preview first.",
@@ -23167,6 +23284,30 @@ function description(t) {
23167
23284
  ].filter(Boolean);
23168
23285
  return `${t.description}${notes.length ? ` (${notes.join("; ")}.)` : ""} Changes data: call without confirm for a preview, then with confirm: true and the confirmation after the user agrees.`;
23169
23286
  }
23287
+ function fileMeta(t) {
23288
+ return t.fileParams?.length ? { "openai/fileParams": t.fileParams } : void 0;
23289
+ }
23290
+ function withFreshFiles(t, signed, now) {
23291
+ if (!t.fileParams?.length) return signed;
23292
+ const fresh = /* @__PURE__ */ new Map();
23293
+ const collect = (v) => {
23294
+ for (const f of Array.isArray(v) ? v : [v])
23295
+ if (f && typeof f === "object" && typeof f.file_id === "string")
23296
+ fresh.set(f.file_id, f);
23297
+ };
23298
+ for (const p of t.fileParams) collect(now[p]);
23299
+ const swap = (v) => {
23300
+ if (Array.isArray(v)) return v.map(swap);
23301
+ if (v && typeof v === "object" && typeof v.file_id === "string") {
23302
+ const f = fresh.get(v.file_id);
23303
+ return f && typeof f.download_url === "string" ? { ...v, download_url: f.download_url } : v;
23304
+ }
23305
+ return v;
23306
+ };
23307
+ const out = { ...signed };
23308
+ for (const p of t.fileParams) if (p in out) out[p] = swap(out[p]);
23309
+ return out;
23310
+ }
23170
23311
  function toolArgs(args) {
23171
23312
  const out = {};
23172
23313
  for (const [k, v] of Object.entries(args ?? {})) if (!CONTROL_ARGS.includes(k) && v !== void 0) out[k] = v;
@@ -23296,7 +23437,7 @@ function createMcpServer(deps) {
23296
23437
  description: description(t),
23297
23438
  inputSchema: inputSchema(t),
23298
23439
  annotations: { title: t.name.replace(/_/g, " "), ...annotations(t) },
23299
- ...deps.widgets && toolMeta(t.name) ? { _meta: toolMeta(t.name) } : {}
23440
+ ...fileMeta(t) || deps.widgets && toolMeta(t.name) ? { _meta: { ...deps.widgets ? toolMeta(t.name) : {}, ...fileMeta(t) } } : {}
23300
23441
  }))
23301
23442
  };
23302
23443
  });
@@ -23332,7 +23473,7 @@ function createMcpServer(deps) {
23332
23473
  return fail(
23333
23474
  access.keyAccess === "read" && isWrite(t) ? "This key can only read. Reconnect with write access to make changes." : `You don't have permission to do this on ${domain ?? "this website"}.`
23334
23475
  );
23335
- const callArgs = approval ? approval.calls[0]?.args ?? {} : toolArgs(args);
23476
+ const callArgs = approval ? withFreshFiles(t, approval.calls[0]?.args ?? {}, toolArgs(args)) : toolArgs(args);
23336
23477
  const ctx = { api: api2, domain, client: "mcp" };
23337
23478
  if (!isWrite(t)) return ok(await t.run(ctx, callArgs), viewForTool(t.name) ? { workspace: domain } : void 0);
23338
23479
  let standing = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmo-so/mcp",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
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",