@cmo-so/mcp 0.7.7 → 0.8.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.
- package/dist/cmo-mcp.mjs +443 -13
- 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.8.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",
|
|
@@ -7257,6 +7257,9 @@ import { createRequire as __cr } from 'node:module'; const require = __cr(import
|
|
|
7257
7257
|
// src/mcp.ts
|
|
7258
7258
|
import { randomBytes } from "node:crypto";
|
|
7259
7259
|
|
|
7260
|
+
// ../../packages/mcp/src/index.ts
|
|
7261
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
7262
|
+
|
|
7260
7263
|
// ../../packages/api-client/src/errors.ts
|
|
7261
7264
|
var OAuthError = class extends Error {
|
|
7262
7265
|
constructor(error2, description2, status) {
|
|
@@ -7410,6 +7413,23 @@ function createApi({ baseUrl, session: session2, fetch: fetchImpl = globalThis.f
|
|
|
7410
7413
|
apps: () => request("/v1/me/apps"),
|
|
7411
7414
|
revokeApp: (grantId) => request(`/v1/me/apps/${encodeURIComponent(grantId)}`, { method: "DELETE" }),
|
|
7412
7415
|
invoices: () => request("/v1/me/invoices"),
|
|
7416
|
+
/** Standing permissions: one of your keys may run some writes on one website without a preview, up to a daily cap. */
|
|
7417
|
+
standing: () => request(
|
|
7418
|
+
"/v1/me/standing"
|
|
7419
|
+
),
|
|
7420
|
+
/** From another key than `key_id` (409/422 otherwise); replaces any earlier one for that key and website. */
|
|
7421
|
+
grantStanding: (p) => request("/v1/me/standing", {
|
|
7422
|
+
method: "POST",
|
|
7423
|
+
headers: { "Content-Type": "application/json" },
|
|
7424
|
+
body: JSON.stringify(p)
|
|
7425
|
+
}),
|
|
7426
|
+
revokeStanding: (id) => request(`/v1/me/standing/${id}`, { method: "DELETE" }),
|
|
7427
|
+
/** The MCP, just before a write: records one use when the key's permission covers it. */
|
|
7428
|
+
claimStanding: (p) => request("/v1/me/standing/claim", {
|
|
7429
|
+
method: "POST",
|
|
7430
|
+
headers: { "Content-Type": "application/json" },
|
|
7431
|
+
body: JSON.stringify(p)
|
|
7432
|
+
}),
|
|
7413
7433
|
/**
|
|
7414
7434
|
* Take a website off your own list (archives your claim; reversible on ai.cmo.so). Nothing changes for
|
|
7415
7435
|
* anyone else; a team member also leaves the team.
|
|
@@ -7993,9 +8013,7 @@ function domainApi(api2, domain) {
|
|
|
7993
8013
|
/** Disconnect the website's LinkedIn account. */
|
|
7994
8014
|
disconnectLinkedIn: (connectionId) => api2.request(`${base}/social/linkedin/${connectionId}`, { method: "DELETE" }),
|
|
7995
8015
|
/** Published articles a LinkedIn post can be made from. */
|
|
7996
|
-
socialSources: (search = "") => api2.request(
|
|
7997
|
-
`${base}/social/sources?search=${encodeURIComponent(search)}`
|
|
7998
|
-
),
|
|
8016
|
+
socialSources: (search = "") => api2.request(`${base}/social/sources?search=${encodeURIComponent(search)}`),
|
|
7999
8017
|
/** Maggie writes a post about one article for each channel; `failed` lists a channel that could not be written while the others were. */
|
|
8000
8018
|
socialFromPost: (sourcePostId, channels) => post(`${base}/social/from-post`, {
|
|
8001
8019
|
source_post_id: sourcePostId,
|
|
@@ -8221,7 +8239,39 @@ function domainApi(api2, domain) {
|
|
|
8221
8239
|
startPromptCheck: (prompt) => post(`${base}/visibility/prompt-check`, { prompt }),
|
|
8222
8240
|
promptCheck: (taskId) => api2.request(`${base}/visibility/prompt-check/${encodeURIComponent(taskId)}`),
|
|
8223
8241
|
report: (range = "30d") => api2.request(`${base}/report?range=${range}`),
|
|
8224
|
-
platform: () => api2.request(`${base}/platform`)
|
|
8242
|
+
platform: () => api2.request(`${base}/platform`),
|
|
8243
|
+
// Ops Autopilot (cmo-so docs/prd/ops-autopilot.md): Maggie's daily operations on the website.
|
|
8244
|
+
opsAutopilot: () => api2.request(`${base}/ops-autopilot`),
|
|
8245
|
+
setOpsAutopilot: (s) => put(`${base}/ops-autopilot`, s),
|
|
8246
|
+
// The report inbox: Maggie's reports and agents' reports.
|
|
8247
|
+
reports: (q2 = {}) => {
|
|
8248
|
+
const params = new URLSearchParams();
|
|
8249
|
+
if (q2.kind) params.set("kind", q2.kind);
|
|
8250
|
+
if (q2.unread) params.set("unread", "true");
|
|
8251
|
+
if (q2.limit) params.set("limit", String(q2.limit));
|
|
8252
|
+
const qs = params.toString();
|
|
8253
|
+
return api2.request(`${base}/reports${qs ? `?${qs}` : ""}`);
|
|
8254
|
+
},
|
|
8255
|
+
inboxReport: (id) => api2.request(`${base}/reports/${id}`),
|
|
8256
|
+
saveReport: (r) => post(`${base}/reports`, r),
|
|
8257
|
+
readAllReports: () => post(`${base}/reports/read-all`, {}),
|
|
8258
|
+
/** One social post in full (social_list clips its text). */
|
|
8259
|
+
socialPost: (id) => api2.request(`${base}/social/${id}`),
|
|
8260
|
+
// Ops Autopilot: ideas the weekly clean-up archived (restorable), post health, Google reviews.
|
|
8261
|
+
ideaCleanup: (limit = 50) => api2.request(`${base}/idea-cleanup?limit=${limit}`),
|
|
8262
|
+
restoreIdeaCleanup: (id) => post(`${base}/idea-cleanup/${id}/restore`, {}),
|
|
8263
|
+
postHealth: (limit = 30) => api2.request(`${base}/post-health?limit=${limit}`),
|
|
8264
|
+
googleReviews: (q2 = {}) => {
|
|
8265
|
+
const params = new URLSearchParams();
|
|
8266
|
+
if (q2.status) params.set("status", q2.status);
|
|
8267
|
+
if (q2.limit) params.set("limit", String(q2.limit));
|
|
8268
|
+
const qs = params.toString();
|
|
8269
|
+
return api2.request(
|
|
8270
|
+
`${base}/google-business/reviews${qs ? `?${qs}` : ""}`
|
|
8271
|
+
);
|
|
8272
|
+
},
|
|
8273
|
+
redraftGoogleReview: (id) => post(`${base}/google-business/reviews/${id}/draft`, {}),
|
|
8274
|
+
replyGoogleReview: (id, text3) => post(`${base}/google-business/reviews/${id}/reply`, text3 === void 0 ? {} : { text: text3 })
|
|
8225
8275
|
};
|
|
8226
8276
|
}
|
|
8227
8277
|
|
|
@@ -8412,6 +8462,13 @@ function sitemapCheckLine(check) {
|
|
|
8412
8462
|
|
|
8413
8463
|
// ../../packages/domain/src/copy/maggie.ts
|
|
8414
8464
|
var q = (s) => `\u201C${String(s ?? "").slice(0, 120)}\u201D`;
|
|
8465
|
+
var CHANNEL_EN = {
|
|
8466
|
+
linkedin: "LinkedIn",
|
|
8467
|
+
facebook: "Facebook",
|
|
8468
|
+
instagram: "Instagram",
|
|
8469
|
+
google_business: "Google Business"
|
|
8470
|
+
};
|
|
8471
|
+
var CHANNEL_ZH = { ...CHANNEL_EN, google_business: "Google \u5546\u5BB6" };
|
|
8415
8472
|
var named = (title, id) => {
|
|
8416
8473
|
if (title != null && String(title).trim()) return ` ${q(title)}`;
|
|
8417
8474
|
const s = String(id ?? "").trim();
|
|
@@ -8595,6 +8652,48 @@ var confirmCopy = {
|
|
|
8595
8652
|
en: (a) => a.paused ? "Pause publishing blog posts." : "Resume publishing blog posts.",
|
|
8596
8653
|
"zh-Hant": (a) => a.paused ? "\u66AB\u505C\u767C\u4F48\u90E8\u843D\u683C\u6587\u7AE0\u3002" : "\u6062\u5FA9\u767C\u4F48\u90E8\u843D\u683C\u6587\u7AE0\u3002"
|
|
8597
8654
|
},
|
|
8655
|
+
ops_autopilot_set: {
|
|
8656
|
+
en: (a) => {
|
|
8657
|
+
const f = Object.entries(a.features ?? {});
|
|
8658
|
+
const on2 = f.filter(([, v]) => v).map(([k]) => k.replace(/_/g, " "));
|
|
8659
|
+
const off = f.filter(([, v]) => !v).map(([k]) => k.replace(/_/g, " "));
|
|
8660
|
+
const parts = [
|
|
8661
|
+
on2.length ? `turn on ${on2.join(", ")}` : "",
|
|
8662
|
+
off.length ? `turn off ${off.join(", ")}` : "",
|
|
8663
|
+
a.weekly_email === true ? "email the Monday report to the owner" : "",
|
|
8664
|
+
a.weekly_email === false ? "stop emailing the Monday report" : ""
|
|
8665
|
+
].filter(Boolean);
|
|
8666
|
+
return `Ops Autopilot: ${parts.join("; ") || "no change"}.`;
|
|
8667
|
+
},
|
|
8668
|
+
"zh-Hant": (a) => {
|
|
8669
|
+
const f = Object.entries(a.features ?? {});
|
|
8670
|
+
const on2 = f.filter(([, v]) => v).map(([k]) => k);
|
|
8671
|
+
const off = f.filter(([, v]) => !v).map(([k]) => k);
|
|
8672
|
+
const parts = [
|
|
8673
|
+
on2.length ? `\u958B\u555F ${on2.join("\u3001")}` : "",
|
|
8674
|
+
off.length ? `\u95DC\u9589 ${off.join("\u3001")}` : "",
|
|
8675
|
+
a.weekly_email === true ? "\u628A\u9031\u4E00\u5831\u544A\u5BC4\u7D66\u7DB2\u7AD9\u64C1\u6709\u8005" : "",
|
|
8676
|
+
a.weekly_email === false ? "\u4E0D\u518D\u5BC4\u9031\u4E00\u5831\u544A" : ""
|
|
8677
|
+
].filter(Boolean);
|
|
8678
|
+
return `Ops Autopilot\uFF1A${parts.join("\uFF1B") || "\u6C92\u6709\u8B8A\u66F4"}\u3002`;
|
|
8679
|
+
}
|
|
8680
|
+
},
|
|
8681
|
+
idea_restore_cleanup: {
|
|
8682
|
+
en: (a) => `Bring back the archived idea #${a.id} to the waiting list.`,
|
|
8683
|
+
"zh-Hant": (a) => `\u628A\u5DF2\u6B78\u6A94\u7684 idea #${a.id} \u653E\u56DE\u7B49\u5F85\u5217\u8868\u3002`
|
|
8684
|
+
},
|
|
8685
|
+
gbp_review_draft: {
|
|
8686
|
+
en: (a) => `Write a new reply draft for Google review #${a.id} (one AI generation). Nothing is posted.`,
|
|
8687
|
+
"zh-Hant": (a) => `\u70BA Google \u8A55\u50F9 #${a.id} \u91CD\u5BEB\u56DE\u8986\u8349\u7A3F\uFF08\u7528\u4E00\u6B21 AI \u751F\u6210\uFF09\uFF0C\u4E0D\u6703\u767C\u4F48\u3002`
|
|
8688
|
+
},
|
|
8689
|
+
gbp_review_reply: {
|
|
8690
|
+
en: (a) => `Post ${a.text ? `the reply ${q(a.text)}` : "Maggie's draft"} under Google review #${a.id}. Everyone who sees the review will see it.`,
|
|
8691
|
+
"zh-Hant": (a) => `\u5728 Google \u8A55\u50F9 #${a.id} \u4E0B\u767C\u4F48${a.text ? `\u56DE\u8986 ${q(a.text)}` : " Maggie \u7684\u8349\u7A3F"}\uFF0C\u770B\u5230\u9019\u5247\u8A55\u50F9\u7684\u4EBA\u90FD\u770B\u5F97\u5230\u3002`
|
|
8692
|
+
},
|
|
8693
|
+
report_save: {
|
|
8694
|
+
en: (a) => `Save the report ${q(a.title)} to the website's inbox (the owner gets a notification).`,
|
|
8695
|
+
"zh-Hant": (a) => `\u628A\u5831\u544A ${q(a.title)} \u5B58\u9032\u7DB2\u7AD9\u7684\u6536\u4EF6\u5323\uFF08\u64C1\u6709\u8005\u6703\u6536\u5230\u901A\u77E5\uFF09\u3002`
|
|
8696
|
+
},
|
|
8598
8697
|
posts_per_day_set: {
|
|
8599
8698
|
en: (a) => `Publish ${a.posts_per_day} blog post${Number(a.posts_per_day) === 1 ? "" : "s"} a day.`,
|
|
8600
8699
|
"zh-Hant": (a) => `\u6BCF\u5929\u767C\u4F48 ${a.posts_per_day} \u7BC7\u90E8\u843D\u683C\u6587\u7AE0\u3002`
|
|
@@ -8769,8 +8868,8 @@ var confirmCopy = {
|
|
|
8769
8868
|
"zh-Hant": () => "\u7522\u751F\u4E00\u500B\u9023\u63A5 LinkedIn \u7684\u9023\u7D50\u3002"
|
|
8770
8869
|
},
|
|
8771
8870
|
social_approve_all: {
|
|
8772
|
-
en: () =>
|
|
8773
|
-
"zh-Hant": () =>
|
|
8871
|
+
en: (a) => `Approve every ${a.channel ? `${CHANNEL_EN[String(a.channel)] ?? a.channel} ` : ""}post waiting for your OK${a.channel ? "" : ", on every channel"}, including any held for a picture check. They go out one by one at each channel's posting times, where people can see them.`,
|
|
8872
|
+
"zh-Hant": (a) => `\u6279\u51C6\u6240\u6709\u7B49\u4F60\u78BA\u8A8D\u7684${a.channel ? ` ${CHANNEL_ZH[String(a.channel)] ?? a.channel} ` : "\uFF08\u6240\u6709\u6E20\u9053\uFF09"}\u8CBC\u6587\uFF0C\u5305\u62EC\u56E0\u5716\u7247\u6AA2\u67E5\u800C\u66AB\u505C\u7684\u3002\u5B83\u5011\u6703\u5728\u5404\u6E20\u9053\u63A5\u4E0B\u4F86\u7684\u767C\u6587\u6642\u9593\u9010\u7BC7\u767C\u51FA\uFF0C\u5927\u5BB6\u90FD\u770B\u5F97\u5230\u3002`
|
|
8774
8873
|
},
|
|
8775
8874
|
series_create: {
|
|
8776
8875
|
en: (a) => `Plan and write a LinkedIn series about ${q(a.brief)}: ${a.count || 3} posts that read as a series. Each waits in Review for your OK. Uses one AI generation a post.`,
|
|
@@ -8839,6 +8938,18 @@ var confirmCopy = {
|
|
|
8839
8938
|
en: (a) => `Disconnect ${q(a.app)} from your account. It stops working now and has to be connected again.`,
|
|
8840
8939
|
"zh-Hant": (a) => `\u5F9E\u4F60\u7684\u5E33\u865F\u65B7\u958B ${q(a.app)}\uFF0C\u5B83\u6703\u7ACB\u5373\u505C\u7528\uFF0C\u4E4B\u5F8C\u8981\u91CD\u65B0\u9023\u63A5\u3002`
|
|
8841
8940
|
},
|
|
8941
|
+
standing_permission_set: {
|
|
8942
|
+
en: (a) => `Let the key ${q(a.key_name)} run ${Object.entries(a.tools ?? {}).map(([tool2, cap]) => `${tool2}${cap ? ` (up to ${cap} a day)` : ""}`).join(
|
|
8943
|
+
", "
|
|
8944
|
+
)} on ${a.website} without asking you first${a.days ? `, for ${a.days} days` : ", until you revoke it"}. Every run is logged.`,
|
|
8945
|
+
"zh-Hant": (a) => `\u8B93\u91D1\u9470 ${q(a.key_name)} \u5728 ${a.website} \u4E0A\u4E0D\u7D93\u4F60\u78BA\u8A8D\u76F4\u63A5\u57F7\u884C\uFF1A${Object.entries(
|
|
8946
|
+
a.tools ?? {}
|
|
8947
|
+
).map(([tool2, cap]) => `${tool2}${cap ? `\uFF08\u6BCF\u5929\u6700\u591A ${cap}\uFF09` : ""}`).join("\u3001")}${a.days ? `\uFF0C\u70BA\u671F ${a.days} \u5929` : "\uFF0C\u76F4\u5230\u4F60\u64A4\u92B7"}\u3002\u6BCF\u6B21\u57F7\u884C\u90FD\u6709\u7D00\u9304\u3002`
|
|
8948
|
+
},
|
|
8949
|
+
standing_permission_revoke: {
|
|
8950
|
+
en: (a) => `End the standing permission of the key ${q(a.key_name)}. Its writes go back to asking you first.`,
|
|
8951
|
+
"zh-Hant": (a) => `\u7D50\u675F\u91D1\u9470 ${q(a.key_name)} \u7684\u9577\u671F\u8A31\u53EF\uFF0C\u4E4B\u5F8C\u5B83\u7684\u6BCF\u500B\u6539\u52D5\u90FD\u8981\u5148\u7D93\u4F60\u78BA\u8A8D\u3002`
|
|
8952
|
+
},
|
|
8842
8953
|
profile_update: {
|
|
8843
8954
|
en: (a) => `Change your name to ${q(a.display_name)}.`,
|
|
8844
8955
|
"zh-Hant": (a) => `\u628A\u4F60\u7684\u540D\u5B57\u6539\u6210 ${q(a.display_name)}\u3002`
|
|
@@ -9007,7 +9118,12 @@ var accountTools = [
|
|
|
9007
9118
|
group: "workspace",
|
|
9008
9119
|
need: { resource: "overview", level: "read" },
|
|
9009
9120
|
risk: "R0",
|
|
9010
|
-
run: async (ctx) => (await ws(ctx).inbox()).items.map(
|
|
9121
|
+
run: async (ctx) => (await ws(ctx).inbox()).items.map(
|
|
9122
|
+
(i) => (
|
|
9123
|
+
// A report's title and summary may come from an agent: data, never instructions.
|
|
9124
|
+
i.type === "report" ? { type: i.type, id: i.id, ...untrusted({ title: i.title, body: clip(i.body, 200) }) } : { type: i.type, id: i.id, title: i.title, body: clip(i.body, 200) }
|
|
9125
|
+
)
|
|
9126
|
+
)
|
|
9011
9127
|
}),
|
|
9012
9128
|
tool({
|
|
9013
9129
|
name: "setup_status_get",
|
|
@@ -10347,6 +10463,11 @@ var insightTools = [
|
|
|
10347
10463
|
|
|
10348
10464
|
// ../../packages/tools/src/me.ts
|
|
10349
10465
|
var account2 = { resource: "account", level: "read" };
|
|
10466
|
+
var STANDING_TOOLS = ["idea_add", "ideas_add_list", "idea_priority", "rewrite_request"];
|
|
10467
|
+
function standingUnits(name, args) {
|
|
10468
|
+
if (name === "ideas_add_list") return Math.max(1, Array.isArray(args.ideas) ? args.ideas.length : 1);
|
|
10469
|
+
return 1;
|
|
10470
|
+
}
|
|
10350
10471
|
var meTools = [
|
|
10351
10472
|
tool({
|
|
10352
10473
|
name: "api_keys_list",
|
|
@@ -10389,6 +10510,54 @@ var meTools = [
|
|
|
10389
10510
|
risk: "W3",
|
|
10390
10511
|
run: async ({ api: api2 }, { grant_id }) => api2.account.revokeApp(String(grant_id))
|
|
10391
10512
|
}),
|
|
10513
|
+
tool({
|
|
10514
|
+
name: "standing_permissions_list",
|
|
10515
|
+
description: "Standing permissions on the user's API keys: which key (a scheduled agent's) may run which writes on which website without a preview, each tool's daily cap and how much is used today.",
|
|
10516
|
+
parameters: none,
|
|
10517
|
+
group: "account",
|
|
10518
|
+
need: account2,
|
|
10519
|
+
risk: "R0",
|
|
10520
|
+
run: async ({ api: api2 }) => api2.account.standing()
|
|
10521
|
+
}),
|
|
10522
|
+
tool({
|
|
10523
|
+
name: "standing_permission_set",
|
|
10524
|
+
description: `Let one of the user's own API keys (from api_keys_list; a scheduled agent's, not the one in use) run some writes on one website without a preview, each up to a daily cap: ${STANDING_TOOLS.join(", ")}. Replaces any earlier one for that key and website. Leave a cap empty for the default (ideas 10 a day, priority changes 25, rewrites 2). Every run is in the action log; standing_permission_revoke ends it.`,
|
|
10525
|
+
parameters: {
|
|
10526
|
+
type: "object",
|
|
10527
|
+
properties: {
|
|
10528
|
+
key_id: { type: "integer" },
|
|
10529
|
+
key_name: { type: "string", description: "The key's name, for the card" },
|
|
10530
|
+
website: { type: "string", description: "The website (domain)" },
|
|
10531
|
+
tools: {
|
|
10532
|
+
type: "object",
|
|
10533
|
+
description: "Daily cap per tool; include only the tools to allow (0 or empty for the default).",
|
|
10534
|
+
properties: Object.fromEntries(STANDING_TOOLS.map((name) => [name, { type: "integer" }]))
|
|
10535
|
+
},
|
|
10536
|
+
days: { type: "integer", description: "How long it lasts (1-366); leave out for until revoked" }
|
|
10537
|
+
},
|
|
10538
|
+
required: ["key_id", "key_name", "website", "tools"]
|
|
10539
|
+
},
|
|
10540
|
+
group: "account",
|
|
10541
|
+
need: account2,
|
|
10542
|
+
risk: "W3",
|
|
10543
|
+
run: async ({ api: api2 }, { key_id, website, tools: tools2, days }) => api2.account.grantStanding({ key_id: Number(key_id), website: String(website), tools: tools2, days: days ?? null })
|
|
10544
|
+
}),
|
|
10545
|
+
tool({
|
|
10546
|
+
name: "standing_permission_revoke",
|
|
10547
|
+
description: "End a standing permission (from standing_permissions_list): its key goes back to previews for every write.",
|
|
10548
|
+
parameters: {
|
|
10549
|
+
type: "object",
|
|
10550
|
+
properties: {
|
|
10551
|
+
id: { type: "integer" },
|
|
10552
|
+
key_name: { type: "string", description: "The key's name, for the card" }
|
|
10553
|
+
},
|
|
10554
|
+
required: ["id", "key_name"]
|
|
10555
|
+
},
|
|
10556
|
+
group: "account",
|
|
10557
|
+
need: account2,
|
|
10558
|
+
risk: "W2",
|
|
10559
|
+
run: async ({ api: api2 }, { id }) => api2.account.revokeStanding(Number(id))
|
|
10560
|
+
}),
|
|
10392
10561
|
tool({
|
|
10393
10562
|
name: "invoices_list",
|
|
10394
10563
|
description: "The user's latest invoices across the subscriptions they pay for: date, amount, status and a link to view or download each.",
|
|
@@ -10529,6 +10698,236 @@ var memoryTools = [
|
|
|
10529
10698
|
];
|
|
10530
10699
|
var MEMORY_TOOLS = new Set(memoryTools.map((t) => t.name));
|
|
10531
10700
|
|
|
10701
|
+
// ../../packages/tools/src/ops.ts
|
|
10702
|
+
var overview = { resource: "overview", level: "read" };
|
|
10703
|
+
var autopilotWrite = { resource: "autopilot", level: "write" };
|
|
10704
|
+
var reportWrite = { resource: "report", level: "write" };
|
|
10705
|
+
var OPS_FEATURES = [
|
|
10706
|
+
"top_up",
|
|
10707
|
+
"precheck",
|
|
10708
|
+
"retry",
|
|
10709
|
+
"weekly_report",
|
|
10710
|
+
"gsc_ideas",
|
|
10711
|
+
"ai_visibility",
|
|
10712
|
+
"idea_cleanup",
|
|
10713
|
+
"competitor_watch",
|
|
10714
|
+
"insight_memory",
|
|
10715
|
+
"post_health",
|
|
10716
|
+
"gbp_reviews"
|
|
10717
|
+
];
|
|
10718
|
+
var opsTools = [
|
|
10719
|
+
tool({
|
|
10720
|
+
name: "ops_autopilot_get",
|
|
10721
|
+
description: "Maggie's daily operations on the website (Ops Autopilot): whether it is available, and each feature with on/off, when it runs and how its last run went. Features: top_up (fills each social channel's empty slots for the week every morning), precheck (checks each social post before it goes out: no links or phone numbers on Google, a picture on Instagram, no placeholders or promised rankings; holds it as a draft if not), retry (tries a failed post once more), weekly_report (Monday report in the inbox), gsc_ideas (Search Console queries near page one become blog ideas), ai_visibility (weekly AI answer check), idea_cleanup (archives stale low-priority ideas weekly, restorable), competitor_watch (competitors' new articles become ideas), insight_memory (what performed best is remembered), post_health (checks new posts once they're live), gbp_reviews (new Google reviews with reply drafts).",
|
|
10722
|
+
parameters: none,
|
|
10723
|
+
group: "workspace",
|
|
10724
|
+
need: overview,
|
|
10725
|
+
risk: "R0",
|
|
10726
|
+
run: async (ctx) => ws(ctx).opsAutopilot()
|
|
10727
|
+
}),
|
|
10728
|
+
tool({
|
|
10729
|
+
name: "ops_autopilot_set",
|
|
10730
|
+
description: "Switch Ops Autopilot features on or off for this website, and whether the Monday report is also emailed to the website's owner. Only features from ops_autopilot_get; a feature that isn't available yet can be switched but won't run.",
|
|
10731
|
+
parameters: {
|
|
10732
|
+
type: "object",
|
|
10733
|
+
properties: {
|
|
10734
|
+
features: {
|
|
10735
|
+
type: "object",
|
|
10736
|
+
description: `Feature \u2192 true (on) or false (off). Features: ${OPS_FEATURES.join(", ")}`,
|
|
10737
|
+
properties: Object.fromEntries(OPS_FEATURES.map((f) => [f, { type: "boolean" }]))
|
|
10738
|
+
},
|
|
10739
|
+
weekly_email: { type: "boolean", description: "Also email the Monday report to the website's owner" }
|
|
10740
|
+
}
|
|
10741
|
+
},
|
|
10742
|
+
group: "workspace",
|
|
10743
|
+
need: autopilotWrite,
|
|
10744
|
+
risk: "W2",
|
|
10745
|
+
idempotent: true,
|
|
10746
|
+
run: async (ctx, { features, weekly_email }) => {
|
|
10747
|
+
const known = Object.fromEntries(
|
|
10748
|
+
Object.entries(features ?? {}).filter(([k]) => OPS_FEATURES.includes(k)).map(([k, v]) => [k, Boolean(v)])
|
|
10749
|
+
);
|
|
10750
|
+
return ws(ctx).setOpsAutopilot({
|
|
10751
|
+
...Object.keys(known).length ? { features: known } : {},
|
|
10752
|
+
...weekly_email === void 0 ? {} : { weekly_email: Boolean(weekly_email) }
|
|
10753
|
+
});
|
|
10754
|
+
}
|
|
10755
|
+
}),
|
|
10756
|
+
tool({
|
|
10757
|
+
name: "reports_list",
|
|
10758
|
+
description: "The website's report inbox, newest first: Maggie's own reports (weekly, daily operations, post health, Google reviews) and reports agents saved. Title and summary each; read one with report_get.",
|
|
10759
|
+
parameters: {
|
|
10760
|
+
type: "object",
|
|
10761
|
+
properties: {
|
|
10762
|
+
kind: {
|
|
10763
|
+
type: "string",
|
|
10764
|
+
enum: ["weekly", "daily_ops", "agent", "post_health", "reviews", "ai_visibility", "search"]
|
|
10765
|
+
},
|
|
10766
|
+
unread: { type: "boolean", description: "Only reports nobody has opened yet" },
|
|
10767
|
+
limit: { type: "integer", minimum: 1, maximum: 50 }
|
|
10768
|
+
}
|
|
10769
|
+
},
|
|
10770
|
+
group: "reports",
|
|
10771
|
+
need: overview,
|
|
10772
|
+
risk: "R0",
|
|
10773
|
+
run: async (ctx, { kind, unread, limit }) => {
|
|
10774
|
+
const r = await ws(ctx).reports({ kind, unread, limit: int(limit, 1, 50, 20) });
|
|
10775
|
+
return {
|
|
10776
|
+
unread: r.unread,
|
|
10777
|
+
items: r.items.map(({ title, summary, ...rest }) => ({ ...rest, ...untrusted({ title, summary }) }))
|
|
10778
|
+
};
|
|
10779
|
+
}
|
|
10780
|
+
}),
|
|
10781
|
+
tool({
|
|
10782
|
+
name: "report_get",
|
|
10783
|
+
description: "One report in full (its text in Markdown and any data); marks it read.",
|
|
10784
|
+
parameters: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
|
|
10785
|
+
group: "reports",
|
|
10786
|
+
need: overview,
|
|
10787
|
+
risk: "R0",
|
|
10788
|
+
run: async (ctx, { id }) => {
|
|
10789
|
+
const r = await ws(ctx).inboxReport(int(id, 1, Number.MAX_SAFE_INTEGER, 0));
|
|
10790
|
+
const { title, summary, body_md, data, ...rest } = r;
|
|
10791
|
+
return { ...rest, ...untrusted({ title, summary, body_md, data }) };
|
|
10792
|
+
}
|
|
10793
|
+
}),
|
|
10794
|
+
tool({
|
|
10795
|
+
name: "report_save",
|
|
10796
|
+
description: "Save a report to the website's inbox (for an agent's scheduled run: what it found and did). The owner gets a push and sees it in the app's Inbox. Up to 20 agent reports a day per website, 50,000 characters each. Saving changes nothing else.",
|
|
10797
|
+
parameters: {
|
|
10798
|
+
type: "object",
|
|
10799
|
+
properties: {
|
|
10800
|
+
title: { type: "string", description: "Short, e.g. 'Daily analysis, 22 Oct'" },
|
|
10801
|
+
summary: { type: "string", description: "One or two sentences (up to 500 characters) shown in the Inbox" },
|
|
10802
|
+
body_md: { type: "string", description: "The full report in Markdown" }
|
|
10803
|
+
},
|
|
10804
|
+
required: ["title"]
|
|
10805
|
+
},
|
|
10806
|
+
group: "reports",
|
|
10807
|
+
need: reportWrite,
|
|
10808
|
+
risk: "W1",
|
|
10809
|
+
run: async (ctx, { title, summary, body_md }) => ws(ctx).saveReport({
|
|
10810
|
+
title: String(title ?? "").slice(0, 200),
|
|
10811
|
+
...summary ? { summary: String(summary).slice(0, 500) } : {},
|
|
10812
|
+
...body_md ? { body_md: String(body_md).slice(0, 5e4) } : {}
|
|
10813
|
+
})
|
|
10814
|
+
}),
|
|
10815
|
+
tool({
|
|
10816
|
+
name: "idea_cleanup_list",
|
|
10817
|
+
description: "Ideas Ops Autopilot archived in its weekly clean-up (stale, low-priority ideas Maggie found herself; never ones a person added), newest first, each with why. Bring one back with idea_restore_cleanup.",
|
|
10818
|
+
parameters: { type: "object", properties: { limit: { type: "integer", minimum: 1, maximum: 200 } } },
|
|
10819
|
+
group: "blog",
|
|
10820
|
+
need: { resource: "autopilot", level: "read" },
|
|
10821
|
+
risk: "R0",
|
|
10822
|
+
run: async (ctx, { limit }) => {
|
|
10823
|
+
const r = await ws(ctx).ideaCleanup(int(limit, 1, 200, 50));
|
|
10824
|
+
return { items: r.items.map(({ reason, ...rest }) => ({ ...rest, ...untrusted({ reason }) })) };
|
|
10825
|
+
}
|
|
10826
|
+
}),
|
|
10827
|
+
tool({
|
|
10828
|
+
name: "idea_restore_cleanup",
|
|
10829
|
+
description: "Bring an idea the weekly clean-up archived back to the waiting list (id from idea_cleanup_list).",
|
|
10830
|
+
parameters: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
|
|
10831
|
+
group: "blog",
|
|
10832
|
+
need: autopilotWrite,
|
|
10833
|
+
risk: "W1",
|
|
10834
|
+
idempotent: true,
|
|
10835
|
+
run: async (ctx, { id }) => ws(ctx).restoreIdeaCleanup(int(id, 1, Number.MAX_SAFE_INTEGER, 0))
|
|
10836
|
+
}),
|
|
10837
|
+
tool({
|
|
10838
|
+
name: "post_health_get",
|
|
10839
|
+
description: "Checks of the blog posts that went live in the last week (Ops Autopilot, daily): whether each page opens, and has a title, a meta description, a canonical link to itself, an og:image and working links on the site. Each with its issues in plain words.",
|
|
10840
|
+
parameters: { type: "object", properties: { limit: { type: "integer", minimum: 1, maximum: 100 } } },
|
|
10841
|
+
group: "reports",
|
|
10842
|
+
need: { resource: "report", level: "read" },
|
|
10843
|
+
risk: "R0",
|
|
10844
|
+
run: async (ctx, { limit }) => ws(ctx).postHealth(int(limit, 1, 100, 30))
|
|
10845
|
+
}),
|
|
10846
|
+
tool({
|
|
10847
|
+
name: "gbp_reviews_list",
|
|
10848
|
+
description: "Reviews of the website's Google Business profile (read every 6 hours by Ops Autopilot), newest first, each with Maggie's reply draft or the reply already sent. status: none (no reply yet), drafted (a draft waits to be sent), sent.",
|
|
10849
|
+
parameters: {
|
|
10850
|
+
type: "object",
|
|
10851
|
+
properties: {
|
|
10852
|
+
status: { type: "string", enum: ["none", "drafted", "sent", "failed"] },
|
|
10853
|
+
limit: { type: "integer", minimum: 1, maximum: 100 }
|
|
10854
|
+
}
|
|
10855
|
+
},
|
|
10856
|
+
group: "social",
|
|
10857
|
+
need: { resource: "autopilot", level: "read" },
|
|
10858
|
+
risk: "R0",
|
|
10859
|
+
run: async (ctx, { status, limit }) => {
|
|
10860
|
+
const r = await ws(ctx).googleReviews({ status, limit: int(limit, 1, 100, 30) });
|
|
10861
|
+
return {
|
|
10862
|
+
connected: r.connected,
|
|
10863
|
+
waiting: r.waiting,
|
|
10864
|
+
items: r.items.map(({ reviewer, comment, ...rest }) => ({ ...rest, ...untrusted({ reviewer, comment }) }))
|
|
10865
|
+
};
|
|
10866
|
+
}
|
|
10867
|
+
}),
|
|
10868
|
+
tool({
|
|
10869
|
+
name: "gbp_review_draft",
|
|
10870
|
+
description: "Ask Maggie for a new reply draft to one Google review (one AI generation). Nothing is posted.",
|
|
10871
|
+
parameters: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
|
|
10872
|
+
group: "social",
|
|
10873
|
+
need: autopilotWrite,
|
|
10874
|
+
risk: "W2",
|
|
10875
|
+
gates: ["LLM"],
|
|
10876
|
+
run: async (ctx, { id }) => {
|
|
10877
|
+
const r = await ws(ctx).redraftGoogleReview(int(id, 1, Number.MAX_SAFE_INTEGER, 0));
|
|
10878
|
+
const { reviewer, comment, ...rest } = r;
|
|
10879
|
+
return { ...rest, ...untrusted({ reviewer, comment }) };
|
|
10880
|
+
}
|
|
10881
|
+
}),
|
|
10882
|
+
tool({
|
|
10883
|
+
name: "gbp_review_reply",
|
|
10884
|
+
description: "Post the reply to one Google review on the website's Google Business profile: the draft, or your own text (up to 1,000 characters). Public: everyone who sees the review sees the reply. A review can be answered once here.",
|
|
10885
|
+
parameters: {
|
|
10886
|
+
type: "object",
|
|
10887
|
+
properties: {
|
|
10888
|
+
id: { type: "integer" },
|
|
10889
|
+
text: { type: "string", description: "The reply; leave out to send the draft" }
|
|
10890
|
+
},
|
|
10891
|
+
required: ["id"]
|
|
10892
|
+
},
|
|
10893
|
+
group: "social",
|
|
10894
|
+
need: autopilotWrite,
|
|
10895
|
+
risk: "W3",
|
|
10896
|
+
run: async (ctx, { id, text: text3 }) => {
|
|
10897
|
+
const r = await ws(ctx).replyGoogleReview(
|
|
10898
|
+
int(id, 1, Number.MAX_SAFE_INTEGER, 0),
|
|
10899
|
+
text3 === void 0 ? void 0 : String(text3).slice(0, 1e3)
|
|
10900
|
+
);
|
|
10901
|
+
const { reviewer, comment, ...rest } = r;
|
|
10902
|
+
return { ...rest, ...untrusted({ reviewer, comment }) };
|
|
10903
|
+
}
|
|
10904
|
+
}),
|
|
10905
|
+
tool({
|
|
10906
|
+
name: "social_post_get",
|
|
10907
|
+
description: "One social post in full: its whole text (social_list shortens it to about 300 characters), its pictures, its link, its channel and status, when it goes out, and why it was held before going out, if it was. Use it to check a post's ending (a web address or phone number on Google, a cut-off sentence) before approving or fixing it.",
|
|
10908
|
+
parameters: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
|
|
10909
|
+
group: "social",
|
|
10910
|
+
need: { resource: "autopilot", level: "read" },
|
|
10911
|
+
risk: "R0",
|
|
10912
|
+
run: async (ctx, { id }) => {
|
|
10913
|
+
const p = await ws(ctx).socialPost(int(id, 1, Number.MAX_SAFE_INTEGER, 0));
|
|
10914
|
+
return {
|
|
10915
|
+
id: p.id,
|
|
10916
|
+
channel: p.provider,
|
|
10917
|
+
status: p.status,
|
|
10918
|
+
eta: p.eta ?? null,
|
|
10919
|
+
text: p.social_copy,
|
|
10920
|
+
characters: String(p.social_copy ?? "").length,
|
|
10921
|
+
picture: p.image_url ?? null,
|
|
10922
|
+
pictures: p.image_urls ?? [],
|
|
10923
|
+
link: p.cta_url ?? p.source_url ?? null,
|
|
10924
|
+
held: p.held ?? null,
|
|
10925
|
+
failure: p.failure_message ?? null
|
|
10926
|
+
};
|
|
10927
|
+
}
|
|
10928
|
+
})
|
|
10929
|
+
];
|
|
10930
|
+
|
|
10532
10931
|
// ../../packages/tools/src/publishing.ts
|
|
10533
10932
|
var read3 = { resource: "autopilot", level: "read" };
|
|
10534
10933
|
var write4 = { resource: "autopilot", level: "write" };
|
|
@@ -11566,12 +11965,18 @@ var socialTools = [
|
|
|
11566
11965
|
}),
|
|
11567
11966
|
tool({
|
|
11568
11967
|
name: "social_sources_list",
|
|
11569
|
-
description: "Published articles a social post can be made from (optionally matching a search).",
|
|
11968
|
+
description: "Published articles a social post can be made from (optionally matching a search), newest first, up to 30: each with when it was published and used_on, the channels it was shared to in the last 30 days with the latest date (empty = not shared lately; an article is free to share again 30 days after its last post).",
|
|
11570
11969
|
parameters: { type: "object", properties: { search: { type: "string" } } },
|
|
11571
11970
|
group: "social",
|
|
11572
11971
|
need: read5,
|
|
11573
11972
|
risk: "R0",
|
|
11574
|
-
run: async (ctx, { search }) => (await ws(ctx).socialSources(search ?? "")).items.slice(0,
|
|
11973
|
+
run: async (ctx, { search }) => (await ws(ctx).socialSources(search ?? "")).items.slice(0, 30).map((s) => ({
|
|
11974
|
+
id: s.id,
|
|
11975
|
+
...untrusted({ title: s.title }),
|
|
11976
|
+
url: s.url,
|
|
11977
|
+
published_at: s.published_at ?? null,
|
|
11978
|
+
used_on: s.used_on ?? {}
|
|
11979
|
+
}))
|
|
11575
11980
|
}),
|
|
11576
11981
|
tool({
|
|
11577
11982
|
name: "social_from_article",
|
|
@@ -12547,6 +12952,7 @@ var tools = [
|
|
|
12547
12952
|
...accountTools,
|
|
12548
12953
|
...meTools,
|
|
12549
12954
|
...reportTools,
|
|
12955
|
+
...opsTools,
|
|
12550
12956
|
...insightTools,
|
|
12551
12957
|
...brandTools,
|
|
12552
12958
|
...blogTools,
|
|
@@ -22534,6 +22940,7 @@ var CONTROL_ARGS = ["workspace", "confirm", "confirmation"];
|
|
|
22534
22940
|
var NOT_ON_MCP = /* @__PURE__ */ new Set(["billing_link"]);
|
|
22535
22941
|
var DESTRUCTIVE = /_(remove|delete|discard|revoke)$/;
|
|
22536
22942
|
var isAccountTool = (t) => t.need.resource === "account";
|
|
22943
|
+
var isStanding = (name) => STANDING_TOOLS.includes(name);
|
|
22537
22944
|
function inputSchema(t) {
|
|
22538
22945
|
const props = { ...t.parameters.properties ?? {} };
|
|
22539
22946
|
props.workspace = WORKSPACE;
|
|
@@ -22675,9 +23082,10 @@ function createMcpServer(deps) {
|
|
|
22675
23082
|
{ capabilities: { tools: {}, resources: {}, prompts: {} }, instructions: INSTRUCTIONS }
|
|
22676
23083
|
);
|
|
22677
23084
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
22678
|
-
const
|
|
23085
|
+
const key = (await me().catch(() => null))?.key;
|
|
23086
|
+
const keyAccess = key?.access ?? null;
|
|
22679
23087
|
return {
|
|
22680
|
-
tools: offered().filter((t) => !(isWrite(t) && keyAccess === "read")).map((t) => ({
|
|
23088
|
+
tools: offered().filter((t) => !(isWrite(t) && keyAccess === "read")).filter((t) => !(isWrite(t) && key?.standing && !isStanding(t.name))).map((t) => ({
|
|
22681
23089
|
name: t.name,
|
|
22682
23090
|
title: t.name.replace(/_/g, " "),
|
|
22683
23091
|
description: description(t),
|
|
@@ -22693,6 +23101,11 @@ function createMcpServer(deps) {
|
|
|
22693
23101
|
const t = toolByName.get(name);
|
|
22694
23102
|
if (!t || excluded(name)) return fail(`There is no tool called ${name}.`);
|
|
22695
23103
|
try {
|
|
23104
|
+
const agentKey = isWrite(t) && (await me().catch(() => null))?.key?.standing === true;
|
|
23105
|
+
if (agentKey && (args.confirm === true || !isStanding(t.name)))
|
|
23106
|
+
return fail(
|
|
23107
|
+
"This is an agent's key: it can only make the changes its standing permission covers, and can't confirm anything itself. Leave other changes for the person."
|
|
23108
|
+
);
|
|
22696
23109
|
let approval = null;
|
|
22697
23110
|
if (isWrite(t) && args.confirm === true) {
|
|
22698
23111
|
if (deps.previewOnly)
|
|
@@ -22717,6 +23130,23 @@ function createMcpServer(deps) {
|
|
|
22717
23130
|
const callArgs = approval ? approval.calls[0]?.args ?? {} : toolArgs(args);
|
|
22718
23131
|
const ctx = { api: api2, domain, client: "mcp" };
|
|
22719
23132
|
if (!isWrite(t)) return ok(await t.run(ctx, callArgs), viewForTool(t.name) ? { workspace: domain } : void 0);
|
|
23133
|
+
let standing = null;
|
|
23134
|
+
if (!approval && !deps.previewOnly && domain && isStanding(t.name)) {
|
|
23135
|
+
const claim2 = await api2.account?.claimStanding({ website: domain, tool: t.name, units: standingUnits(t.name, callArgs) }).catch(() => null);
|
|
23136
|
+
if (claim2?.allowed) {
|
|
23137
|
+
standing = claim2.permission_id;
|
|
23138
|
+
approval = {
|
|
23139
|
+
id: randomUUID2(),
|
|
23140
|
+
calls: [{ name: t.name, args: callArgs }],
|
|
23141
|
+
principal,
|
|
23142
|
+
domain,
|
|
23143
|
+
expires: Date.now()
|
|
23144
|
+
};
|
|
23145
|
+
} else if (agentKey) {
|
|
23146
|
+
const why = claim2?.reason === "daily_cap" ? `today's limit for ${t.name} is used up (${claim2.daily_cap})` : claim2?.reason === "tool_not_included" ? `the standing permission doesn't include ${t.name}` : claim2 ? `there is no standing permission for ${domain} (${claim2.reason})` : "the permission check failed; try again later";
|
|
23147
|
+
return fail(`Not done: ${why}.`);
|
|
23148
|
+
}
|
|
23149
|
+
}
|
|
22720
23150
|
if (!approval) {
|
|
22721
23151
|
const calls = [{ name: t.name, args: callArgs }];
|
|
22722
23152
|
const summary2 = t.confirm?.(callArgs, "en") ?? t.description;
|
|
@@ -22730,7 +23160,7 @@ function createMcpServer(deps) {
|
|
|
22730
23160
|
next: "Show the preview to the user. If they agree, call this tool again with confirm: true and this confirmation (it runs exactly what was previewed). Nothing has changed yet."
|
|
22731
23161
|
});
|
|
22732
23162
|
}
|
|
22733
|
-
const summary = t.confirm?.(callArgs, "en") ?? t.description
|
|
23163
|
+
const summary = `${t.confirm?.(callArgs, "en") ?? t.description}${standing ? ` (standing permission #${standing})` : ""}`;
|
|
22734
23164
|
try {
|
|
22735
23165
|
const result = await t.run({ api: withIdempotency(api2, approval.id), domain, client: "mcp" }, callArgs);
|
|
22736
23166
|
const added = t.name === "website_add" ? result?.domain : null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cmo-so/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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",
|