@cmo-so/mcp 0.9.0 → 0.9.1

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 +86 -16
  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.1",
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",
@@ -8838,11 +8838,11 @@ var confirmCopy = {
8838
8838
  },
8839
8839
  social_pictures_attach: {
8840
8840
  en: (a) => {
8841
- const n = Array.isArray(a.urls) ? a.urls.length : 0;
8841
+ const n = (Array.isArray(a.urls) ? a.urls.length : 0) + (Array.isArray(a.files) ? a.files.length : 0);
8842
8842
  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
8843
  },
8844
8844
  "zh-Hant": (a) => {
8845
- const n = Array.isArray(a.urls) ? a.urls.length : 0;
8845
+ const n = (Array.isArray(a.urls) ? a.urls.length : 0) + (Array.isArray(a.files) ? a.files.length : 0);
8846
8846
  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
8847
  }
8848
8848
  },
@@ -11613,6 +11613,17 @@ var channelsParam = {
11613
11613
  items: { type: "string", enum: CHANNEL_KEYS },
11614
11614
  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
11615
  };
11616
+ var FILE_SCHEMA = {
11617
+ type: "object",
11618
+ properties: {
11619
+ download_url: { type: "string" },
11620
+ file_id: { type: "string" },
11621
+ mime_type: { type: "string" },
11622
+ file_name: { type: "string" }
11623
+ },
11624
+ required: ["download_url", "file_id"]
11625
+ };
11626
+ var fileLinks = (value) => (Array.isArray(value) ? value : value ? [value] : []).map((f) => String(f?.download_url ?? "").trim()).filter(Boolean);
11616
11627
  var queuedAt = positionParam("the posts", "Leave out for the front (the next posting times).");
11617
11628
  var channelsOf = (value) => {
11618
11629
  const list2 = Array.isArray(value) ? value.map((v) => String(v).toLowerCase()).filter((v) => CHANNEL_KEYS.includes(v)) : [];
@@ -11982,27 +11993,35 @@ var socialTools = [
11982
11993
  }),
11983
11994
  tool({
11984
11995
  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.",
11996
+ 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
11997
  parameters: {
11987
11998
  type: "object",
11988
11999
  properties: {
11989
12000
  id: { type: "integer", description: "The post (from social_list), not gone out yet" },
12001
+ files: {
12002
+ type: "array",
12003
+ items: FILE_SCHEMA,
12004
+ description: "Pictures from this chat, in the order they should appear"
12005
+ },
11990
12006
  urls: {
11991
12007
  type: "array",
11992
12008
  items: { type: "string" },
11993
- description: "Public https links to the image files, in the order they should appear"
12009
+ description: "Public https links to the image files, in order (after the files, when both are given)"
11994
12010
  },
11995
12011
  append: { type: "boolean", description: "Add after the post's current pictures instead of replacing them" }
11996
12012
  },
11997
- required: ["id", "urls"]
12013
+ required: ["id"]
11998
12014
  },
12015
+ fileParams: ["files"],
11999
12016
  group: "social",
12000
12017
  need: write5,
12001
12018
  risk: "W1",
12002
12019
  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.");
12020
+ run: async (ctx, { id, urls, files, append }) => {
12021
+ const list2 = [...fileLinks(files), ...(Array.isArray(urls) ? urls : []).map((u) => String(u).trim())].filter(
12022
+ Boolean
12023
+ );
12024
+ if (!list2.length) throw new Error("Give the pictures (files from this chat, or https links).");
12006
12025
  if (list2.length > 20) throw new Error("A post takes up to 20 pictures.");
12007
12026
  if (list2.some((u) => !u.startsWith("https://"))) throw new Error("Every picture needs a public https link.");
12008
12027
  const out = await ws(ctx).setSocialPictures(Number(id), { urls: list2, append: append === true });
@@ -12015,30 +12034,57 @@ var socialTools = [
12015
12034
  }),
12016
12035
  tool({
12017
12036
  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.",
12037
+ 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
12038
  parameters: {
12020
12039
  type: "object",
12021
12040
  properties: {
12022
12041
  id: { type: "integer", description: "A LinkedIn post (from social_list), not gone out yet" },
12042
+ file: { ...FILE_SCHEMA, description: "The PDF from this chat" },
12023
12043
  url: { type: "string", description: "Public https link to the PDF file" },
12024
12044
  title: {
12025
12045
  type: "string",
12026
12046
  description: "Shown on the carousel (up to 120 characters); the post's topic if left out"
12027
12047
  }
12028
12048
  },
12029
- required: ["id", "url"]
12049
+ required: ["id"]
12030
12050
  },
12051
+ fileParams: ["file"],
12031
12052
  group: "social",
12032
12053
  need: write5,
12033
12054
  risk: "W1",
12034
12055
  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.");
12056
+ run: async (ctx, { id, url, file, title }) => {
12057
+ const link = fileLinks(file)[0] ?? String(url ?? "").trim();
12058
+ if (!link.startsWith("https://"))
12059
+ throw new Error("Give the PDF (a file from this chat, or a public https link).");
12038
12060
  const out = await ws(ctx).attachSocialCarouselPdf(Number(id), { url: link, ...title ? { title } : {} });
12039
12061
  return { id: out.id, pages: out.pages, pictures: out.pictures, carousel: out.carousel ?? null };
12040
12062
  }
12041
12063
  }),
12064
+ tool({
12065
+ name: "social_upload_link",
12066
+ 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.",
12067
+ parameters: {
12068
+ type: "object",
12069
+ properties: { id: { type: "integer", description: "The post (from social_list), not gone out yet" } },
12070
+ required: ["id"]
12071
+ },
12072
+ group: "social",
12073
+ need: read5,
12074
+ risk: "R0",
12075
+ run: async (ctx, { id }) => {
12076
+ const post = await ws(ctx).socialPost(Number(id));
12077
+ if (["published", "publishing", "cancelled"].includes(String(post.status)))
12078
+ throw new Error("That post has already gone out (or was skipped): its pictures can't change.");
12079
+ const site = ctx.domain ? `&site=${encodeURIComponent(ctx.domain)}` : "";
12080
+ return {
12081
+ id: post.id,
12082
+ channel: post.provider ?? null,
12083
+ url: `https://app.cmo.so/social?picture=${post.id}${site}`,
12084
+ 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)."
12085
+ };
12086
+ }
12087
+ }),
12042
12088
  tool({
12043
12089
  name: "social_carousel_set",
12044
12090
  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 +23213,30 @@ function description(t) {
23167
23213
  ].filter(Boolean);
23168
23214
  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
23215
  }
23216
+ function fileMeta(t) {
23217
+ return t.fileParams?.length ? { "openai/fileParams": t.fileParams } : void 0;
23218
+ }
23219
+ function withFreshFiles(t, signed, now) {
23220
+ if (!t.fileParams?.length) return signed;
23221
+ const fresh = /* @__PURE__ */ new Map();
23222
+ const collect = (v) => {
23223
+ for (const f of Array.isArray(v) ? v : [v])
23224
+ if (f && typeof f === "object" && typeof f.file_id === "string")
23225
+ fresh.set(f.file_id, f);
23226
+ };
23227
+ for (const p of t.fileParams) collect(now[p]);
23228
+ const swap = (v) => {
23229
+ if (Array.isArray(v)) return v.map(swap);
23230
+ if (v && typeof v === "object" && typeof v.file_id === "string") {
23231
+ const f = fresh.get(v.file_id);
23232
+ return f && typeof f.download_url === "string" ? { ...v, download_url: f.download_url } : v;
23233
+ }
23234
+ return v;
23235
+ };
23236
+ const out = { ...signed };
23237
+ for (const p of t.fileParams) if (p in out) out[p] = swap(out[p]);
23238
+ return out;
23239
+ }
23170
23240
  function toolArgs(args) {
23171
23241
  const out = {};
23172
23242
  for (const [k, v] of Object.entries(args ?? {})) if (!CONTROL_ARGS.includes(k) && v !== void 0) out[k] = v;
@@ -23296,7 +23366,7 @@ function createMcpServer(deps) {
23296
23366
  description: description(t),
23297
23367
  inputSchema: inputSchema(t),
23298
23368
  annotations: { title: t.name.replace(/_/g, " "), ...annotations(t) },
23299
- ...deps.widgets && toolMeta(t.name) ? { _meta: toolMeta(t.name) } : {}
23369
+ ...fileMeta(t) || deps.widgets && toolMeta(t.name) ? { _meta: { ...deps.widgets ? toolMeta(t.name) : {}, ...fileMeta(t) } } : {}
23300
23370
  }))
23301
23371
  };
23302
23372
  });
@@ -23332,7 +23402,7 @@ function createMcpServer(deps) {
23332
23402
  return fail(
23333
23403
  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
23404
  );
23335
- const callArgs = approval ? approval.calls[0]?.args ?? {} : toolArgs(args);
23405
+ const callArgs = approval ? withFreshFiles(t, approval.calls[0]?.args ?? {}, toolArgs(args)) : toolArgs(args);
23336
23406
  const ctx = { api: api2, domain, client: "mcp" };
23337
23407
  if (!isWrite(t)) return ok(await t.run(ctx, callArgs), viewForTool(t.name) ? { workspace: domain } : void 0);
23338
23408
  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.1",
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",