@socialneuron/mcp-server 1.8.1 → 1.8.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.
package/dist/sn.js CHANGED
@@ -19,7 +19,7 @@ var MCP_VERSION;
19
19
  var init_version = __esm({
20
20
  "src/lib/version.ts"() {
21
21
  "use strict";
22
- MCP_VERSION = "1.8.1";
22
+ MCP_VERSION = "1.8.2";
23
23
  }
24
24
  });
25
25
 
@@ -66,25 +66,140 @@ __export(credentials_exports, {
66
66
  saveSupabaseUrl: () => saveSupabaseUrl
67
67
  });
68
68
  import { execFileSync } from "node:child_process";
69
- import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
69
+ import {
70
+ closeSync,
71
+ constants,
72
+ existsSync,
73
+ fchmodSync,
74
+ fstatSync,
75
+ ftruncateSync,
76
+ lstatSync,
77
+ mkdirSync,
78
+ openSync,
79
+ readFileSync,
80
+ unlinkSync,
81
+ writeFileSync
82
+ } from "node:fs";
70
83
  import { homedir, platform } from "node:os";
71
84
  import { join } from "node:path";
85
+ function assertSafeCredentialPaths() {
86
+ if (platform() === "win32") return;
87
+ const uid = process.getuid?.();
88
+ if (existsSync(CONFIG_DIR)) {
89
+ const directory = lstatSync(CONFIG_DIR);
90
+ if (directory.isSymbolicLink() || !directory.isDirectory()) {
91
+ throw new Error("Unsafe Social Neuron credential directory. Refusing to use it.");
92
+ }
93
+ if (uid !== void 0 && directory.uid !== uid) {
94
+ throw new Error("Social Neuron credential directory is not owned by the current user.");
95
+ }
96
+ }
97
+ if (existsSync(CREDENTIALS_FILE)) {
98
+ const file = lstatSync(CREDENTIALS_FILE);
99
+ if (file.isSymbolicLink() || !file.isFile()) {
100
+ throw new Error("Unsafe Social Neuron credential file. Refusing to use it.");
101
+ }
102
+ if (uid !== void 0 && file.uid !== uid) {
103
+ throw new Error("Social Neuron credential file is not owned by the current user.");
104
+ }
105
+ }
106
+ }
107
+ function openValidatedCredentialPath(target, kind, flags) {
108
+ let fd;
109
+ try {
110
+ fd = openSync(
111
+ target,
112
+ flags | constants.O_NOFOLLOW | (kind === "directory" ? constants.O_DIRECTORY : 0)
113
+ );
114
+ } catch (error) {
115
+ if (error.code === "ENOENT") return null;
116
+ throw new Error(`Unsafe Social Neuron credential ${kind}. Refusing to use it.`);
117
+ }
118
+ const opened = fstatSync(fd);
119
+ const uid = process.getuid?.();
120
+ if ((kind === "directory" ? !opened.isDirectory() : !opened.isFile()) || uid !== void 0 && opened.uid !== uid || kind === "file" && opened.nlink !== 1) {
121
+ closeSync(fd);
122
+ throw new Error(`Unsafe Social Neuron credential ${kind}. Refusing to use it.`);
123
+ }
124
+ return fd;
125
+ }
126
+ function hardenCredentialPath(target, kind, mode) {
127
+ const fd = openValidatedCredentialPath(target, kind, constants.O_RDONLY);
128
+ if (fd === null) return;
129
+ try {
130
+ const opened = fstatSync(fd);
131
+ if ((opened.mode & 63) !== 0) fchmodSync(fd, mode);
132
+ } finally {
133
+ closeSync(fd);
134
+ }
135
+ }
136
+ function hardenCredentialPermissions() {
137
+ if (platform() === "win32") return;
138
+ hardenCredentialPath(CONFIG_DIR, "directory", 448);
139
+ hardenCredentialPath(CREDENTIALS_FILE, "file", 384);
140
+ }
72
141
  function readCredentialsFile() {
142
+ if (platform() !== "win32") hardenCredentialPath(CONFIG_DIR, "directory", 448);
143
+ const fd = platform() === "win32" ? (() => {
144
+ try {
145
+ return openSync(CREDENTIALS_FILE, constants.O_RDONLY);
146
+ } catch (error) {
147
+ if (error.code === "ENOENT") return null;
148
+ throw error;
149
+ }
150
+ })() : openValidatedCredentialPath(CREDENTIALS_FILE, "file", constants.O_RDONLY);
151
+ if (fd === null) return {};
73
152
  try {
74
- if (!existsSync(CREDENTIALS_FILE)) return {};
75
- const raw = readFileSync(CREDENTIALS_FILE, "utf-8");
76
- return JSON.parse(raw);
77
- } catch {
78
- return {};
153
+ if (platform() !== "win32") fchmodSync(fd, 384);
154
+ const parsed = JSON.parse(readFileSync(fd, "utf-8"));
155
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
156
+ throw new Error("Social Neuron credential file is invalid.");
157
+ }
158
+ return parsed;
159
+ } finally {
160
+ closeSync(fd);
79
161
  }
80
162
  }
81
163
  function writeCredentialsFile(data) {
82
164
  if (!existsSync(CONFIG_DIR)) {
83
165
  mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
84
166
  }
85
- writeFileSync(CREDENTIALS_FILE, JSON.stringify(data, null, 2) + "\n", {
86
- mode: 384
87
- });
167
+ assertSafeCredentialPaths();
168
+ const payload = JSON.stringify(data, null, 2) + "\n";
169
+ if (platform() === "win32") {
170
+ writeFileSync(CREDENTIALS_FILE, payload, { mode: 384 });
171
+ } else {
172
+ let fd;
173
+ try {
174
+ fd = openSync(
175
+ CREDENTIALS_FILE,
176
+ constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,
177
+ 384
178
+ );
179
+ } catch (error) {
180
+ if (error.code !== "EEXIST") {
181
+ throw new Error("Unsafe Social Neuron credential file. Refusing to write it.");
182
+ }
183
+ try {
184
+ fd = openSync(CREDENTIALS_FILE, constants.O_WRONLY | constants.O_NOFOLLOW);
185
+ } catch {
186
+ throw new Error("Unsafe Social Neuron credential file. Refusing to write it.");
187
+ }
188
+ }
189
+ try {
190
+ const opened = fstatSync(fd);
191
+ const uid = process.getuid?.();
192
+ if (!opened.isFile() || opened.nlink !== 1 || uid !== void 0 && opened.uid !== uid) {
193
+ throw new Error("Unsafe Social Neuron credential file. Refusing to write it.");
194
+ }
195
+ fchmodSync(fd, 384);
196
+ ftruncateSync(fd, 0);
197
+ writeFileSync(fd, payload);
198
+ } finally {
199
+ closeSync(fd);
200
+ }
201
+ }
202
+ hardenCredentialPermissions();
88
203
  }
89
204
  function macKeychainRead(service) {
90
205
  try {
@@ -294,7 +409,6 @@ async function validateApiKey(apiKey, _attempt = 0) {
294
409
  }
295
410
  );
296
411
  if (!response.ok) {
297
- const text = await response.text();
298
412
  const retryable = response.status === 429 || response.status >= 500;
299
413
  if (retryable && _attempt < VALIDATE_MAX_RETRIES) {
300
414
  await sleep(300 * (_attempt + 1));
@@ -303,11 +417,19 @@ async function validateApiKey(apiKey, _attempt = 0) {
303
417
  return {
304
418
  valid: false,
305
419
  retryable,
306
- error: `Validation failed (HTTP ${response.status}): ${text}`
420
+ error: `Validation failed (HTTP ${response.status}).`
307
421
  };
308
422
  }
309
- return await response.json();
310
- } catch (err) {
423
+ const result = await response.json();
424
+ if (!result.valid) {
425
+ return {
426
+ valid: false,
427
+ retryable: false,
428
+ error: "API key is invalid, expired, or revoked."
429
+ };
430
+ }
431
+ return result;
432
+ } catch {
311
433
  if (_attempt < VALIDATE_MAX_RETRIES) {
312
434
  await sleep(300 * (_attempt + 1));
313
435
  return validateApiKey(apiKey, _attempt + 1);
@@ -315,7 +437,7 @@ async function validateApiKey(apiKey, _attempt = 0) {
315
437
  return {
316
438
  valid: false,
317
439
  retryable: true,
318
- error: err instanceof Error ? err.message : String(err)
440
+ error: "Authentication service is temporarily unavailable."
319
441
  };
320
442
  }
321
443
  }
@@ -336,10 +458,12 @@ __export(posthog_exports, {
336
458
  initPostHog: () => initPostHog,
337
459
  shutdownPostHog: () => shutdownPostHog
338
460
  });
339
- import { createHash } from "node:crypto";
461
+ import { createHmac, randomBytes } from "node:crypto";
340
462
  import { PostHog } from "posthog-node";
341
463
  function hashUserId(userId) {
342
- return createHash("sha256").update(`${POSTHOG_SALT}:${userId}`).digest("hex").substring(0, 32);
464
+ const hmac = createHmac("sha256", POSTHOG_PSEUDONYMIZATION_KEY);
465
+ hmac.update(userId);
466
+ return hmac.digest("hex").substring(0, 32);
343
467
  }
344
468
  function initPostHog() {
345
469
  if (isTelemetryDisabled()) return;
@@ -375,13 +499,13 @@ async function shutdownPostHog() {
375
499
  client = null;
376
500
  }
377
501
  }
378
- var POSTHOG_SALT, client;
502
+ var POSTHOG_PSEUDONYMIZATION_KEY, client;
379
503
  var init_posthog = __esm({
380
504
  "src/lib/posthog.ts"() {
381
505
  "use strict";
382
506
  init_supabase();
383
507
  init_request_context();
384
- POSTHOG_SALT = "socialneuron-mcp-ph-v1";
508
+ POSTHOG_PSEUDONYMIZATION_KEY = process.env.POSTHOG_PSEUDONYMIZATION_KEY || randomBytes(32).toString("hex");
385
509
  client = null;
386
510
  }
387
511
  });
@@ -603,7 +727,7 @@ var init_supabase = __esm({
603
727
  });
604
728
 
605
729
  // src/cli/sn/parse.ts
606
- import { createHash as createHash2 } from "node:crypto";
730
+ import { createHash } from "node:crypto";
607
731
  function parseSnArgs(argv) {
608
732
  const parsed = { _: [] };
609
733
  for (let i = 0; i < argv.length; i += 1) {
@@ -633,8 +757,8 @@ function isEnabledFlag(value) {
633
757
  }
634
758
  function emitSnResult(payload, asJson) {
635
759
  if (asJson) {
636
- const envelope = { schema_version: "1", ...payload };
637
- process.stdout.write(JSON.stringify(envelope, null, 2) + "\n");
760
+ const envelope2 = { schema_version: "1", ...payload };
761
+ process.stdout.write(JSON.stringify(envelope2, null, 2) + "\n");
638
762
  }
639
763
  }
640
764
  function isValidHttpsUrl(value) {
@@ -669,7 +793,7 @@ async function checkUrlReachability(url) {
669
793
  } catch (error) {
670
794
  return {
671
795
  ok: false,
672
- error: error instanceof Error ? error.message : String(error)
796
+ error: "Network request failed."
673
797
  };
674
798
  } finally {
675
799
  clearTimeout(timer);
@@ -699,7 +823,7 @@ function buildPublishIdempotencyKey(input) {
699
823
  title: input.title ?? "",
700
824
  scheduledAt: input.scheduledAt ?? ""
701
825
  });
702
- return `sn_${createHash2("sha256").update(material).digest("hex").slice(0, 24)}`;
826
+ return `sn_${createHash("sha256").update(material).digest("hex").slice(0, 24)}`;
703
827
  }
704
828
  function normalizePlatforms(platformsRaw) {
705
829
  const caseMap = {
@@ -802,6 +926,52 @@ var edge_function_exports = {};
802
926
  __export(edge_function_exports, {
803
927
  callEdgeFunction: () => callEdgeFunction
804
928
  });
929
+ function safeGatewayError(responseText, status) {
930
+ try {
931
+ const parsed = JSON.parse(responseText);
932
+ const nested = parsed.error && typeof parsed.error === "object" ? parsed.error : null;
933
+ const candidates = [
934
+ nested?.error_type,
935
+ nested?.code,
936
+ parsed.error_type,
937
+ parsed.error_code,
938
+ parsed.code,
939
+ typeof parsed.error === "string" ? parsed.error : null
940
+ ];
941
+ for (const value of candidates) {
942
+ if (typeof value !== "string") continue;
943
+ const normalized = value.trim().toLowerCase();
944
+ if (SAFE_GATEWAY_ERROR_CODES.has(normalized)) return normalized;
945
+ const embedded = [...SAFE_GATEWAY_ERROR_CODES].find((code) => normalized.includes(code));
946
+ if (embedded) return embedded;
947
+ }
948
+ } catch {
949
+ }
950
+ return `Backend request failed (HTTP ${status}).`;
951
+ }
952
+ function safeFailureData(responseText) {
953
+ try {
954
+ const parsed = JSON.parse(responseText);
955
+ const metric = (name) => {
956
+ const value = parsed[name];
957
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
958
+ };
959
+ const billingStatus = typeof parsed.billing_status === "string" && SAFE_BILLING_STATUSES.has(parsed.billing_status) ? parsed.billing_status : void 0;
960
+ const failureReason = parsed.failure_reason === "generation_failed" || parsed.failure_reason === "authentication_failed" || parsed.failure_reason === "cancelled_by_user" ? parsed.failure_reason : void 0;
961
+ const jobStatus = typeof parsed.status === "string" && SAFE_JOB_STATUSES.has(parsed.status) ? parsed.status : void 0;
962
+ const safe = {
963
+ ...jobStatus ? { status: jobStatus } : {},
964
+ ...metric("credits_reserved") !== void 0 ? { credits_reserved: metric("credits_reserved") } : {},
965
+ ...metric("credits_charged") !== void 0 ? { credits_charged: metric("credits_charged") } : {},
966
+ ...metric("credits_refunded") !== void 0 ? { credits_refunded: metric("credits_refunded") } : {},
967
+ ...billingStatus ? { billing_status: billingStatus } : {},
968
+ ...failureReason ? { failure_reason: failureReason } : {}
969
+ };
970
+ return Object.keys(safe).length > 0 ? safe : null;
971
+ } catch {
972
+ return null;
973
+ }
974
+ }
805
975
  function getApiKeyOrNull() {
806
976
  const envKey = process.env.SOCIALNEURON_API_KEY;
807
977
  if (envKey && envKey.trim().length) return envKey.trim();
@@ -872,33 +1042,28 @@ async function callEdgeFunction(functionName, body, options) {
872
1042
  clearTimeout(timer);
873
1043
  const responseText = await response.text();
874
1044
  if (!response.ok) {
875
- let errorMessage;
876
- try {
877
- const errorJson = JSON.parse(responseText);
878
- errorMessage = errorJson.error || errorJson.message || responseText;
879
- } catch {
880
- errorMessage = responseText || `HTTP ${response.status}`;
881
- }
1045
+ const errorCode = safeGatewayError(responseText, response.status);
1046
+ const failureData = safeFailureData(responseText);
882
1047
  if (response.status === 401) {
883
1048
  return {
884
- data: null,
1049
+ data: failureData,
885
1050
  error: `Authentication failed (HTTP 401). Run 'npx @socialneuron/mcp-server login' to re-authenticate.`
886
1051
  };
887
1052
  }
888
1053
  if (response.status === 403) {
889
1054
  return {
890
- data: null,
891
- error: `Forbidden (HTTP 403): ${errorMessage}. This action isn't permitted for your account, plan, or scope \u2014 your connection is still valid.`
1055
+ data: failureData,
1056
+ error: `Forbidden (HTTP 403): ${errorCode}. This action isn't permitted for your account, plan, or scope \u2014 your connection is still valid.`
892
1057
  };
893
1058
  }
894
1059
  if (response.status === 429) {
895
1060
  const retryAfter = response.headers.get("retry-after") || "60";
896
1061
  return {
897
- data: null,
1062
+ data: failureData,
898
1063
  error: `Rate limit exceeded (HTTP 429). Wait ${retryAfter}s before retrying. Reduce request frequency or upgrade your plan.`
899
1064
  };
900
1065
  }
901
- return { data: null, error: errorMessage };
1066
+ return { data: failureData, error: errorCode };
902
1067
  }
903
1068
  try {
904
1069
  const data = JSON.parse(responseText);
@@ -914,15 +1079,48 @@ async function callEdgeFunction(functionName, body, options) {
914
1079
  error: `Edge Function '${functionName}' timed out after ${timeoutMs}ms`
915
1080
  };
916
1081
  }
917
- const message = err instanceof Error ? err.message : String(err);
918
- return { data: null, error: message };
1082
+ return { data: null, error: "Network request failed. Please retry." };
919
1083
  }
920
1084
  }
1085
+ var SAFE_GATEWAY_ERROR_CODES, SAFE_BILLING_STATUSES, SAFE_JOB_STATUSES;
921
1086
  var init_edge_function = __esm({
922
1087
  "src/lib/edge-function.ts"() {
923
1088
  "use strict";
924
1089
  init_supabase();
925
1090
  init_request_context();
1091
+ SAFE_GATEWAY_ERROR_CODES = /* @__PURE__ */ new Set([
1092
+ "daily_limit_reached",
1093
+ "insufficient_credits",
1094
+ "project_scope_mismatch",
1095
+ "schedule_conflict",
1096
+ "post_not_found",
1097
+ "post_not_reschedulable",
1098
+ "post_in_progress",
1099
+ "not_cancellable",
1100
+ "publishing_in_progress",
1101
+ "plan_upgrade_required",
1102
+ "rate_limited",
1103
+ "validation_error",
1104
+ "permission_denied",
1105
+ "not_found"
1106
+ ]);
1107
+ SAFE_BILLING_STATUSES = /* @__PURE__ */ new Set([
1108
+ "reserved",
1109
+ "charged",
1110
+ "refunded",
1111
+ "failed_no_charge",
1112
+ "refund_pending",
1113
+ "not_charged"
1114
+ ]);
1115
+ SAFE_JOB_STATUSES = /* @__PURE__ */ new Set([
1116
+ "queued",
1117
+ "pending",
1118
+ "processing",
1119
+ "completed",
1120
+ "failed",
1121
+ "cancelled",
1122
+ "canceled"
1123
+ ]);
926
1124
  }
927
1125
  });
928
1126
 
@@ -938,6 +1136,7 @@ function evaluateQuality(input) {
938
1136
  const firstLine = caption.split("\n")[0]?.trim() ?? "";
939
1137
  const hashtags = countHashtags(caption);
940
1138
  const threshold = Math.min(35, Math.max(0, input.threshold ?? 26));
1139
+ const isShortFormX = platforms.length > 0 && platforms.every((p) => p === "twitter") && caption.length <= 280;
941
1140
  const blockedTerms = [
942
1141
  ...(input.brandAvoidPatterns ?? []).map((t) => t.trim()).filter(Boolean),
943
1142
  ...(input.customBannedTerms ?? []).map((t) => t.trim()).filter(Boolean)
@@ -947,11 +1146,12 @@ function evaluateQuality(input) {
947
1146
  if (firstLine.length >= 20 && firstLine.length <= 120) hookScore += 1;
948
1147
  if (/[!?]/.test(firstLine) || /\b\d+(\.\d+)?\b/.test(firstLine)) hookScore += 1;
949
1148
  if (/\b(how|why|stop|avoid|build|launch|scale|grow|mistake)\b/i.test(firstLine)) hookScore += 1;
1149
+ if (isShortFormX) hookScore = Math.max(hookScore, 3);
950
1150
  categories.push({
951
1151
  name: "Hook Strength",
952
1152
  score: Math.min(5, hookScore),
953
1153
  maxScore: 5,
954
- detail: "First line should create curiosity/value within 120 chars."
1154
+ detail: isShortFormX ? "Short-form X: keyword-hook heuristics neutralized; substance judge arbitrates." : "First line should create curiosity/value within 120 chars."
955
1155
  });
956
1156
  let clarityScore = 2;
957
1157
  if (caption.length >= 80 && caption.length <= 1200) clarityScore += 2;
@@ -977,7 +1177,8 @@ function evaluateQuality(input) {
977
1177
  detail: "Length, title, and hashtag usage should match target platforms."
978
1178
  });
979
1179
  let brandScore = 3;
980
- const brandKeyword = input.brandKeyword ?? process.env.SOCIALNEURON_BRAND_KEYWORD?.trim();
1180
+ const rawBrandKeyword = input.brandKeyword ?? process.env.SOCIALNEURON_BRAND_KEYWORD?.trim();
1181
+ const brandKeyword = rawBrandKeyword ? rawBrandKeyword.slice(0, 80).replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : null;
981
1182
  if (brandKeyword && new RegExp("\\b" + brandKeyword + "\\b", "i").test(title + " " + caption))
982
1183
  brandScore += 1;
983
1184
  if (!/\b(you|your|customer|audience)\b/i.test(caption)) brandScore -= 1;
@@ -988,11 +1189,12 @@ function evaluateQuality(input) {
988
1189
  brandScore -= Math.min(2, matched.length);
989
1190
  }
990
1191
  }
1192
+ if (isShortFormX) brandScore = Math.max(brandScore, 3);
991
1193
  categories.push({
992
1194
  name: "Brand Alignment",
993
1195
  score: Math.max(0, Math.min(5, brandScore)),
994
1196
  maxScore: 5,
995
- detail: "Voice should match brand context and audience focus."
1197
+ detail: isShortFormX ? "Short-form X: second-person-address heuristic neutralized; blocked terms still enforced." : "Voice should match brand context and audience focus."
996
1198
  });
997
1199
  let noveltyScore = 2;
998
1200
  if (/\b(case study|framework|workflow|playbook|breakdown|behind the scenes)\b/i.test(caption))
@@ -1003,21 +1205,23 @@ function evaluateQuality(input) {
1003
1205
  const matched = blockedTerms.filter((term) => lowerCombined.includes(term.toLowerCase()));
1004
1206
  if (matched.length > 0) noveltyScore -= Math.min(2, matched.length);
1005
1207
  }
1208
+ if (isShortFormX) noveltyScore = Math.max(noveltyScore, 3);
1006
1209
  categories.push({
1007
1210
  name: "Novelty",
1008
1211
  score: Math.max(0, Math.min(5, noveltyScore)),
1009
1212
  maxScore: 5,
1010
- detail: "Avoid generic phrasing; include distinct angle."
1213
+ detail: isShortFormX ? "Short-form X: keyword-novelty neutralized; substance judge arbitrates." : "Avoid generic phrasing; include distinct angle."
1011
1214
  });
1012
1215
  let ctaScore = 2;
1013
1216
  if (/\b(comment|reply|share|save|follow|subscribe|click|try|book|download)\b/i.test(caption))
1014
1217
  ctaScore += 2;
1015
1218
  if (/\?$/.test(firstLine)) ctaScore += 1;
1219
+ if (isShortFormX) ctaScore = Math.max(ctaScore, 3);
1016
1220
  categories.push({
1017
1221
  name: "CTA Strength",
1018
1222
  score: Math.min(5, ctaScore),
1019
1223
  maxScore: 5,
1020
- detail: "Should include a clear next action."
1224
+ detail: isShortFormX ? "Short-form X: explicit CTA optional; native posts may close without one." : "Should include a clear next action."
1021
1225
  });
1022
1226
  let safetyScore = 5;
1023
1227
  if (/\b(guarantee|guaranteed|no risk|risk-free|always works|100%)\b/i.test(caption))
@@ -2022,9 +2226,6 @@ function searchTools(query) {
2022
2226
  (t) => t.name.toLowerCase().includes(q) || t.description.toLowerCase().includes(q)
2023
2227
  );
2024
2228
  }
2025
- function getModules() {
2026
- return [...new Set(TOOL_CATALOG.map((t) => t.module))];
2027
- }
2028
2229
  var TOOL_CATALOG;
2029
2230
  var init_tool_catalog = __esm({
2030
2231
  "src/lib/tool-catalog.ts"() {
@@ -2099,6 +2300,26 @@ var init_tool_catalog = __esm({
2099
2300
  module: "carousel",
2100
2301
  scope: "mcp:write"
2101
2302
  },
2303
+ {
2304
+ name: "cancel_async_job",
2305
+ description: "Cancel an owned pending async job and refund an eligible debit",
2306
+ module: "lifecycle",
2307
+ scope: "mcp:write",
2308
+ task_intent: "Stop a queued generation or render before worker execution",
2309
+ use_when: "The user explicitly wants to cancel a pending job and has confirmed the action.",
2310
+ avoid_when: "The job is already processing or terminal; check_status instead.",
2311
+ next_tools: ["check_status", "get_credit_balance"]
2312
+ },
2313
+ {
2314
+ name: "delete_carousel",
2315
+ description: "Delete an owned carousel content record from one project",
2316
+ module: "lifecycle",
2317
+ scope: "mcp:write",
2318
+ task_intent: "Remove an unwanted carousel record from Social Neuron",
2319
+ use_when: "The user explicitly confirms deletion and understands stored media follows normal retention.",
2320
+ avoid_when: "The user expects an already-published social post or retained media object to be removed.",
2321
+ next_tools: ["list_recent_posts"]
2322
+ },
2102
2323
  // media
2103
2324
  {
2104
2325
  name: "upload_media",
@@ -2119,6 +2340,26 @@ var init_tool_catalog = __esm({
2119
2340
  module: "distribution",
2120
2341
  scope: "mcp:distribute"
2121
2342
  },
2343
+ {
2344
+ name: "reschedule_post",
2345
+ description: "Atomically move an unclaimed scheduled post to a new time within its brand project",
2346
+ module: "distribution",
2347
+ scope: "mcp:distribute",
2348
+ task_intent: "Move an already scheduled post without creating a duplicate publication",
2349
+ use_when: "A pending or scheduled post needs a different future publication time.",
2350
+ avoid_when: "The post is already publishing or published; create a new post instead.",
2351
+ next_tools: ["list_recent_posts", "open_content_calendar"]
2352
+ },
2353
+ {
2354
+ name: "cancel_scheduled_post",
2355
+ description: "Cancel an owned scheduled post before publishing begins",
2356
+ module: "lifecycle",
2357
+ scope: "mcp:distribute",
2358
+ task_intent: "Unschedule a post without publishing it",
2359
+ use_when: "The user explicitly confirms cancellation of a draft, pending, or scheduled post.",
2360
+ avoid_when: "The post is already publishing or published.",
2361
+ next_tools: ["list_recent_posts", "open_content_calendar"]
2362
+ },
2122
2363
  {
2123
2364
  name: "list_recent_posts",
2124
2365
  description: "List recently published or scheduled posts",
@@ -2309,6 +2550,16 @@ var init_tool_catalog = __esm({
2309
2550
  module: "planning",
2310
2551
  scope: "mcp:write"
2311
2552
  },
2553
+ {
2554
+ name: "delete_content_plan",
2555
+ description: "Permanently delete an owned content plan from one project",
2556
+ module: "lifecycle",
2557
+ scope: "mcp:write",
2558
+ task_intent: "Remove an obsolete saved content plan",
2559
+ use_when: "The user explicitly confirms permanent deletion of a specific plan.",
2560
+ avoid_when: "The plan has scheduled posts the user also expects to cancel; cancel those posts separately.",
2561
+ next_tools: ["plan_content_week", "list_recent_posts"]
2562
+ },
2312
2563
  {
2313
2564
  name: "submit_content_plan_for_approval",
2314
2565
  description: "Submit a content plan for team approval",
@@ -2403,6 +2654,16 @@ var init_tool_catalog = __esm({
2403
2654
  module: "autopilot",
2404
2655
  scope: "mcp:autopilot"
2405
2656
  },
2657
+ {
2658
+ name: "delete_autopilot_config",
2659
+ description: "Permanently delete an owned autopilot configuration from one project",
2660
+ module: "lifecycle",
2661
+ scope: "mcp:autopilot",
2662
+ task_intent: "Remove an autopilot configuration while retaining historical run records",
2663
+ use_when: "The user explicitly confirms deletion of the configuration.",
2664
+ avoid_when: "The user only wants to pause or edit automation, or expects historical posts to be deleted.",
2665
+ next_tools: ["list_autopilot_configs"]
2666
+ },
2406
2667
  // extraction
2407
2668
  {
2408
2669
  name: "extract_url_content",
@@ -2519,7 +2780,13 @@ var init_tool_catalog = __esm({
2519
2780
  // apps (MCP Apps — interactive UI inside the host)
2520
2781
  {
2521
2782
  name: "open_content_calendar",
2522
- description: "Open an interactive drag-drop calendar of the user's scheduled posts inside the host (Claude Desktop / claude.ai). Renders an MCP App; backed by list_recent_posts, schedule_post, find_next_slots \u2014 no new tools needed.",
2783
+ description: "Open a project-scoped interactive calendar inside MCP App-capable hosts. Uses reschedule_post for conflict-safe drag/drop and schedule_post for new posts.",
2784
+ module: "apps",
2785
+ scope: "mcp:read"
2786
+ },
2787
+ {
2788
+ name: "open_analytics_pulse",
2789
+ description: "Open a project-scoped interactive performance dashboard with views, engagement, platform mix, and top posts.",
2523
2790
  module: "apps",
2524
2791
  scope: "mcp:read"
2525
2792
  },
@@ -2634,6 +2901,12 @@ var init_tool_catalog = __esm({
2634
2901
  module: "skills",
2635
2902
  scope: "mcp:read"
2636
2903
  },
2904
+ {
2905
+ name: "get_skill",
2906
+ description: "Fetch the full body and current compiled guidance for one Social Neuron skill by slug.",
2907
+ module: "skills",
2908
+ scope: "mcp:read"
2909
+ },
2637
2910
  {
2638
2911
  name: "run_skill",
2639
2912
  description: "Run a Social Neuron workflow skill end-to-end (brand-locked content production). Returns a structured run preview with the step plan, credit cost, and a deep-link to launch in the SN dashboard.",
@@ -2665,17 +2938,21 @@ __export(discovery_exports, {
2665
2938
  handleInfo: () => handleInfo,
2666
2939
  handleTools: () => handleTools
2667
2940
  });
2941
+ function publicStdioTools() {
2942
+ return TOOL_CATALOG.filter(
2943
+ (t) => !t.internal && !t.hiddenFromPublicCount && t.module !== "apps"
2944
+ );
2945
+ }
2668
2946
  async function handleTools(args, asJson) {
2669
- let tools = TOOL_CATALOG;
2947
+ let tools = publicStdioTools();
2670
2948
  const scope = args.scope;
2671
2949
  if (typeof scope === "string") {
2672
- tools = getToolsByScope(scope);
2950
+ tools = tools.filter((tool) => tool.scope === scope);
2673
2951
  }
2674
2952
  const module = args.module;
2675
2953
  if (typeof module === "string") {
2676
- tools = getToolsByModule(module);
2954
+ tools = tools.filter((tool) => tool.module === module);
2677
2955
  }
2678
- tools = tools.filter((t) => !t.internal);
2679
2956
  if (asJson) {
2680
2957
  emitSnResult({ ok: true, command: "tools", toolCount: tools.length, tools }, true);
2681
2958
  return;
@@ -2704,27 +2981,31 @@ Module: ${moduleName} (${moduleTools.length} tools)`);
2704
2981
  process.exit(0);
2705
2982
  }
2706
2983
  async function handleInfo(args, asJson) {
2984
+ const stdioTools = publicStdioTools();
2707
2985
  const info = {
2708
2986
  version: MCP_VERSION,
2709
- toolCount: TOOL_CATALOG.filter((t) => !t.internal).length,
2710
- modules: getModules()
2987
+ toolCount: stdioTools.length,
2988
+ modules: Array.from(new Set(stdioTools.map((tool) => tool.module))).sort(),
2989
+ auth: null
2711
2990
  };
2712
- try {
2713
- const { loadApiKey: loadApiKey2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
2714
- const { validateApiKey: validateApiKey2 } = await Promise.resolve().then(() => (init_api_keys(), api_keys_exports));
2715
- const apiKey = await loadApiKey2();
2716
- if (apiKey) {
2717
- const result = await validateApiKey2(apiKey);
2718
- if (result.valid) {
2719
- info.auth = {
2720
- email: result.email || null,
2721
- scopes: result.scopes || [],
2722
- expiresAt: result.expiresAt || null
2723
- };
2991
+ if (!isEnabledFlag(args.offline)) {
2992
+ try {
2993
+ const { loadApiKey: loadApiKey2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
2994
+ const { validateApiKey: validateApiKey2 } = await Promise.resolve().then(() => (init_api_keys(), api_keys_exports));
2995
+ const apiKey = await loadApiKey2();
2996
+ if (apiKey) {
2997
+ const result = await validateApiKey2(apiKey);
2998
+ if (result.valid) {
2999
+ info.auth = {
3000
+ email: result.email || null,
3001
+ scopes: result.scopes || [],
3002
+ expiresAt: result.expiresAt || null
3003
+ };
3004
+ }
2724
3005
  }
3006
+ } catch {
3007
+ info.auth = null;
2725
3008
  }
2726
- } catch {
2727
- info.auth = null;
2728
3009
  }
2729
3010
  if (info.auth) {
2730
3011
  try {
@@ -2838,6 +3119,9 @@ var init_scopes = __esm({
2838
3119
  generate_carousel: "mcp:write",
2839
3120
  create_carousel: "mcp:write",
2840
3121
  upload_media: "mcp:write",
3122
+ cancel_async_job: "mcp:write",
3123
+ delete_carousel: "mcp:write",
3124
+ delete_content_plan: "mcp:write",
2841
3125
  // mcp:read (media)
2842
3126
  get_media_url: "mcp:read",
2843
3127
  // F4 Hyperframes — HTML composition runtime
@@ -2845,6 +3129,8 @@ var init_scopes = __esm({
2845
3129
  render_hyperframes: "mcp:write",
2846
3130
  // mcp:distribute
2847
3131
  schedule_post: "mcp:distribute",
3132
+ reschedule_post: "mcp:distribute",
3133
+ cancel_scheduled_post: "mcp:distribute",
2848
3134
  start_platform_connection: "mcp:distribute",
2849
3135
  // mcp:analytics
2850
3136
  refresh_platform_analytics: "mcp:analytics",
@@ -2859,6 +3145,7 @@ var init_scopes = __esm({
2859
3145
  list_autopilot_configs: "mcp:autopilot",
2860
3146
  update_autopilot_config: "mcp:autopilot",
2861
3147
  get_autopilot_status: "mcp:autopilot",
3148
+ delete_autopilot_config: "mcp:autopilot",
2862
3149
  // Recipes
2863
3150
  list_recipes: "mcp:read",
2864
3151
  get_recipe_details: "mcp:read",
@@ -2902,6 +3189,7 @@ var init_scopes = __esm({
2902
3189
  detect_anomalies: "mcp:analytics",
2903
3190
  // mcp:read (Apps — entry tool for the Content Calendar MCP App; reads recent posts)
2904
3191
  open_content_calendar: "mcp:read",
3192
+ open_analytics_pulse: "mcp:read",
2905
3193
  // mcp:write (Agentic harness — learning loop write-back)
2906
3194
  write_agent_reflection: "mcp:write",
2907
3195
  record_outcome: "mcp:write",
@@ -2917,6 +3205,7 @@ var init_scopes = __esm({
2917
3205
  get_active_campaigns: "mcp:read",
2918
3206
  // mcp:read / mcp:write (Skills)
2919
3207
  list_skills: "mcp:read",
3208
+ get_skill: "mcp:read",
2920
3209
  run_skill: "mcp:write",
2921
3210
  // mcp:read (Loop observability — growth-loop KPIs + content learning state)
2922
3211
  get_loop_pulse: "mcp:read",
@@ -3043,6 +3332,11 @@ var init_tool_annotations = __esm({
3043
3332
  OVERRIDES = {
3044
3333
  // Destructive or overwrite tools
3045
3334
  delete_comment: { destructiveHint: true },
3335
+ cancel_async_job: { destructiveHint: true, idempotentHint: true },
3336
+ cancel_scheduled_post: { destructiveHint: true, idempotentHint: true, openWorldHint: true },
3337
+ delete_carousel: { destructiveHint: true, idempotentHint: true },
3338
+ delete_content_plan: { destructiveHint: true, idempotentHint: true },
3339
+ delete_autopilot_config: { destructiveHint: true, idempotentHint: true },
3046
3340
  moderate_comment: { destructiveHint: true },
3047
3341
  save_brand_profile: { destructiveHint: true, idempotentHint: true },
3048
3342
  update_platform_voice: { destructiveHint: true, idempotentHint: true },
@@ -3207,6 +3501,7 @@ var init_tool_profile = __esm({
3207
3501
  "list_hyperframes_blocks",
3208
3502
  "render_hyperframes",
3209
3503
  "list_skills",
3504
+ "get_skill",
3210
3505
  "run_skill"
3211
3506
  ]);
3212
3507
  }
@@ -3240,7 +3535,8 @@ var init_constants = __esm({
3240
3535
  ssn: "\\d{3}-\\d{2}-\\d{4}",
3241
3536
  ip: "(?<![\\d.])(?:\\d{1,3}\\.){3}\\d{1,3}(?![\\d.])"
3242
3537
  },
3243
- MAX_LENGTH: 1e4
3538
+ MAX_LENGTH: 1e4,
3539
+ MAX_OUTPUT_LENGTH: 1e6
3244
3540
  };
3245
3541
  }
3246
3542
  });
@@ -3337,7 +3633,8 @@ var init_pii = __esm({
3337
3633
  function scan(text, options) {
3338
3634
  const flagged = /* @__PURE__ */ new Set();
3339
3635
  let risk = 0;
3340
- if (text.length > CONSTANTS.MAX_LENGTH) {
3636
+ const maxLength = options.source === "mcp_tool_output" ? CONSTANTS.MAX_OUTPUT_LENGTH : CONSTANTS.MAX_LENGTH;
3637
+ if (text.length > maxLength) {
3341
3638
  return {
3342
3639
  passed: false,
3343
3640
  risk_score: 1,
@@ -3412,6 +3709,7 @@ var CATEGORY_CONFIGS, RateLimiter, limiters;
3412
3709
  var init_rate_limit = __esm({
3413
3710
  "src/lib/rate-limit.ts"() {
3414
3711
  "use strict";
3712
+ init_scopes();
3415
3713
  CATEGORY_CONFIGS = {
3416
3714
  posting: { maxTokens: 30, refillRate: 30 / 60 },
3417
3715
  // 30 req/min — publish/schedule/comment
@@ -3502,8 +3800,8 @@ function registerIdeationTools(server) {
3502
3800
  brand_voice: z.string().max(500).optional().describe(
3503
3801
  'Tone directive (e.g. "direct, no jargon, second person" or "witty Gen-Z energy with emoji"). Leave blank to auto-load from project brand profile if project_id is set.'
3504
3802
  ),
3505
- model: z.enum(["gemini-2.0-flash", "gemini-2.5-flash", "gemini-2.5-pro"]).optional().describe(
3506
- "AI model to use. Defaults to gemini-2.5-flash. Use gemini-2.5-pro for highest quality."
3803
+ model: z.enum(["gemini-2.5-flash", "gemini-2.5-pro"]).optional().describe(
3804
+ "AI model to use. Defaults to gemini-2.5-flash. Use gemini-2.5-pro for highest quality. Retired provider models are intentionally not accepted."
3507
3805
  ),
3508
3806
  project_id: z.string().uuid().optional().describe(
3509
3807
  "Project ID to auto-load brand profile and performance context for prompt enrichment."
@@ -3512,7 +3810,7 @@ function registerIdeationTools(server) {
3512
3810
  async ({ prompt: prompt2, content_type, platform: platform3, brand_voice, model, project_id }) => {
3513
3811
  try {
3514
3812
  const userId = await getDefaultUserId();
3515
- const rl = checkRateLimit("posting", userId);
3813
+ const rl = checkRateLimit("generation", `generate_content:${userId}`);
3516
3814
  if (!rl.allowed) {
3517
3815
  return {
3518
3816
  content: [
@@ -3769,7 +4067,7 @@ Content Type: ${content_type}`;
3769
4067
  async ({ content, source_platform, target_platform, brand_voice, project_id }) => {
3770
4068
  try {
3771
4069
  const userId = await getDefaultUserId();
3772
- const rl = checkRateLimit("posting", userId);
4070
+ const rl = checkRateLimit("generation", `adapt_content:${userId}`);
3773
4071
  if (!rl.allowed) {
3774
4072
  return {
3775
4073
  content: [
@@ -3881,11 +4179,12 @@ function buildCheckStatusPayload(job, liveStatus) {
3881
4179
  const progress = liveStatus?.progress ?? null;
3882
4180
  const resultUrl = liveStatus?.resultUrl ?? job.result_url ?? null;
3883
4181
  const r2Key = resultUrl && !resultUrl.startsWith("http") ? resultUrl : null;
3884
- const errorMessage = liveStatus?.error ?? job.error_message ?? null;
4182
+ const rawError = liveStatus?.error ?? job.error_message ?? null;
4183
+ const errorMessage = rawError && status === "failed" ? "Generation failed. Retry or choose another model." : rawError && (status === "cancelled" || status === "canceled") ? "Cancelled by user." : null;
3885
4184
  const allUrls = job.result_metadata?.all_urls ?? null;
3886
4185
  const modelRequested = job.result_metadata?.model_requested ?? null;
3887
4186
  const modelDelivered = job.result_metadata?.model_delivered ?? null;
3888
- const fallbackReason = job.result_metadata?.fallback_reason ?? null;
4187
+ const fallbackReason = job.result_metadata?.fallback_reason ? "Requested model was unavailable; a fallback model was used." : null;
3889
4188
  return {
3890
4189
  job_id: job.id,
3891
4190
  job_type: job.job_type,
@@ -3897,6 +4196,11 @@ function buildCheckStatusPayload(job, liveStatus) {
3897
4196
  all_urls: allUrls,
3898
4197
  error: errorMessage,
3899
4198
  credits_cost: job.credits_cost,
4199
+ credits_reserved: job.credits_reserved ?? null,
4200
+ credits_charged: job.credits_charged ?? null,
4201
+ credits_refunded: job.credits_refunded ?? null,
4202
+ billing_status: job.billing_status ?? "unknown",
4203
+ failure_reason: job.failure_reason ?? null,
3900
4204
  created_at: job.created_at,
3901
4205
  completed_at: job.completed_at,
3902
4206
  model_requested: modelRequested,
@@ -3998,6 +4302,13 @@ var init_budget = __esm({
3998
4302
 
3999
4303
  // src/tools/content.ts
4000
4304
  import { z as z2 } from "zod";
4305
+ function buildFallbackDisclosureLine(meta) {
4306
+ const requested = meta?.model_requested;
4307
+ const delivered = meta?.model_delivered;
4308
+ if (!requested || !delivered || requested === delivered) return null;
4309
+ const reason = meta?.fallback_reason ? " because the requested model was unavailable" : "";
4310
+ return `Note: requested "${requested}" but delivered "${delivered}"${reason} \u2014 cost never exceeds the requested model's price.`;
4311
+ }
4001
4312
  function asEnvelope2(data) {
4002
4313
  return {
4003
4314
  _meta: {
@@ -4066,7 +4377,7 @@ function registerContentTools(server) {
4066
4377
  isError: true
4067
4378
  };
4068
4379
  }
4069
- const rateLimit = checkRateLimit("posting", `generate_video:${userId}`);
4380
+ const rateLimit = checkRateLimit("generation", `generate_video:${userId}`);
4070
4381
  if (!rateLimit.allowed) {
4071
4382
  return {
4072
4383
  content: [
@@ -4207,7 +4518,7 @@ function registerContentTools(server) {
4207
4518
  isError: true
4208
4519
  };
4209
4520
  }
4210
- const rateLimit = checkRateLimit("posting", `generate_image:${userId}`);
4521
+ const rateLimit = checkRateLimit("generation", `generate_image:${userId}`);
4211
4522
  if (!rateLimit.allowed) {
4212
4523
  return {
4213
4524
  content: [
@@ -4347,20 +4658,28 @@ function registerContentTools(server) {
4347
4658
  }
4348
4659
  );
4349
4660
  if (liveStatus) {
4661
+ const livePayload = buildCheckStatusPayload(job, liveStatus);
4350
4662
  const lines2 = [
4351
4663
  `Job: ${job.id}`,
4352
4664
  `Type: ${job.job_type}`,
4353
4665
  `Model: ${job.model}`,
4354
- `Status: ${liveStatus.status}`,
4355
- `Progress: ${liveStatus.progress}%`
4666
+ `Status: ${livePayload.status}`,
4667
+ `Progress: ${livePayload.progress}%`
4356
4668
  ];
4357
- if (liveStatus.resultUrl) {
4358
- lines2.push(`Result URL: ${liveStatus.resultUrl}`);
4669
+ if (livePayload.result_url) {
4670
+ lines2.push(`Result URL: ${livePayload.result_url}`);
4359
4671
  }
4360
- if (liveStatus.error) {
4361
- lines2.push(`Error: ${liveStatus.error}`);
4672
+ if (livePayload.error) {
4673
+ lines2.push(`Error: ${livePayload.error}`);
4362
4674
  }
4675
+ const fallbackDisclosure2 = buildFallbackDisclosureLine(job.result_metadata);
4676
+ if (fallbackDisclosure2) lines2.push(fallbackDisclosure2);
4363
4677
  lines2.push(`Credits: ${job.credits_cost}`);
4678
+ if (job.billing_status) {
4679
+ lines2.push(
4680
+ `Billing: ${job.billing_status} (charged ${job.credits_charged ?? 0}, refunded ${job.credits_refunded ?? 0})`
4681
+ );
4682
+ }
4364
4683
  lines2.push(`Created: ${job.created_at}`);
4365
4684
  if (format === "json") {
4366
4685
  return {
@@ -4378,7 +4697,7 @@ function registerContentTools(server) {
4378
4697
  // `job` + `liveStatus`; do not duplicate them here.
4379
4698
  asEnvelope2({
4380
4699
  ...liveStatus,
4381
- ...buildCheckStatusPayload(job, liveStatus)
4700
+ ...livePayload
4382
4701
  }),
4383
4702
  null,
4384
4703
  2
@@ -4421,7 +4740,14 @@ function registerContentTools(server) {
4421
4740
  if (job.error_message) {
4422
4741
  lines.push(`Error: ${job.error_message}`);
4423
4742
  }
4743
+ const fallbackDisclosure = buildFallbackDisclosureLine(job.result_metadata);
4744
+ if (fallbackDisclosure) lines.push(fallbackDisclosure);
4424
4745
  lines.push(`Credits: ${job.credits_cost}`);
4746
+ if (job.billing_status) {
4747
+ lines.push(
4748
+ `Billing: ${job.billing_status} (charged ${job.credits_charged ?? 0}, refunded ${job.credits_refunded ?? 0})`
4749
+ );
4750
+ }
4425
4751
  lines.push(`Created: ${job.created_at}`);
4426
4752
  if (job.completed_at) {
4427
4753
  lines.push(`Completed: ${job.completed_at}`);
@@ -4651,7 +4977,7 @@ Return ONLY valid JSON in this exact format:
4651
4977
  };
4652
4978
  }
4653
4979
  const rateLimit = checkRateLimit(
4654
- "posting",
4980
+ "generation",
4655
4981
  `generate_voiceover:${userId}`
4656
4982
  );
4657
4983
  if (!rateLimit.allowed) {
@@ -4818,7 +5144,7 @@ Return ONLY valid JSON in this exact format:
4818
5144
  }
4819
5145
  const userId = await getDefaultUserId();
4820
5146
  const rateLimit = checkRateLimit(
4821
- "posting",
5147
+ "generation",
4822
5148
  `generate_carousel:${userId}`
4823
5149
  );
4824
5150
  if (!rateLimit.allowed) {
@@ -4972,9 +5298,6 @@ var init_content2 = __esm({
4972
5298
  // src/lib/sanitize-error.ts
4973
5299
  function sanitizeError(error) {
4974
5300
  const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
4975
- if (process.env.NODE_ENV !== "production") {
4976
- console.error("[Error]", msg);
4977
- }
4978
5301
  for (const [pattern, userMessage] of ERROR_PATTERNS) {
4979
5302
  if (pattern.test(msg)) {
4980
5303
  return userMessage;
@@ -5178,7 +5501,7 @@ var init_ssrf = __esm({
5178
5501
 
5179
5502
  // src/tools/distribution.ts
5180
5503
  import { z as z3 } from "zod";
5181
- import { createHash as createHash3 } from "node:crypto";
5504
+ import { createHash as createHash2 } from "node:crypto";
5182
5505
  function snakeToCamel(obj) {
5183
5506
  const result = {};
5184
5507
  for (const [key, value] of Object.entries(obj)) {
@@ -5204,6 +5527,87 @@ function asEnvelope3(data) {
5204
5527
  data
5205
5528
  };
5206
5529
  }
5530
+ function publicConnectedAccount(value) {
5531
+ if (!value || typeof value !== "object") return null;
5532
+ const row = value;
5533
+ if (typeof row.id !== "string" || typeof row.platform !== "string" || typeof row.status !== "string" || typeof row.created_at !== "string") {
5534
+ return null;
5535
+ }
5536
+ return {
5537
+ id: row.id,
5538
+ platform: row.platform,
5539
+ status: row.status,
5540
+ ...typeof row.effective_status === "string" ? { effective_status: row.effective_status } : {},
5541
+ username: typeof row.username === "string" ? row.username : null,
5542
+ created_at: row.created_at,
5543
+ ...typeof row.updated_at === "string" || row.updated_at === null ? { updated_at: row.updated_at } : {},
5544
+ ...typeof row.expires_at === "string" || row.expires_at === null ? { expires_at: row.expires_at } : {},
5545
+ ...typeof row.has_refresh_token === "boolean" ? { has_refresh_token: row.has_refresh_token } : {},
5546
+ ...typeof row.project_id === "string" || row.project_id === null ? { project_id: row.project_id } : {}
5547
+ };
5548
+ }
5549
+ function publicPostRecord(value) {
5550
+ if (!value || typeof value !== "object") return null;
5551
+ const row = value;
5552
+ if (typeof row.id !== "string" || typeof row.platform !== "string" || typeof row.status !== "string" || typeof row.created_at !== "string") {
5553
+ return null;
5554
+ }
5555
+ const nullableString = (field) => typeof row[field] === "string" ? row[field] : null;
5556
+ return {
5557
+ id: row.id,
5558
+ platform: row.platform,
5559
+ status: row.status,
5560
+ title: nullableString("title"),
5561
+ external_post_id: nullableString("external_post_id"),
5562
+ published_at: nullableString("published_at"),
5563
+ scheduled_at: nullableString("scheduled_at"),
5564
+ created_at: row.created_at
5565
+ };
5566
+ }
5567
+ function mediaTypeFromPath(value) {
5568
+ try {
5569
+ const pathname = value.startsWith("http") ? new URL(value).pathname : value;
5570
+ const extension = pathname.split(".").pop()?.toLowerCase();
5571
+ if (!extension) return null;
5572
+ if (VIDEO_FILE_EXTENSIONS.has(extension)) return "VIDEO";
5573
+ if (IMAGE_FILE_EXTENSIONS.has(extension)) return "IMAGE";
5574
+ } catch {
5575
+ return null;
5576
+ }
5577
+ return null;
5578
+ }
5579
+ function inferScheduleMediaType(explicit, singleCandidates, collectionCandidates) {
5580
+ if (explicit) return explicit;
5581
+ const populatedCollection = collectionCandidates.find(
5582
+ (values) => Array.isArray(values) && values.length > 0
5583
+ );
5584
+ if (populatedCollection) {
5585
+ if (populatedCollection.length > 1) return "CAROUSEL_ALBUM";
5586
+ for (const values of collectionCandidates) {
5587
+ if (!values || values.length !== 1) continue;
5588
+ const inferred = mediaTypeFromPath(values[0]);
5589
+ if (inferred) return inferred;
5590
+ }
5591
+ return null;
5592
+ }
5593
+ for (const value of singleCandidates) {
5594
+ if (typeof value !== "string" || value.length === 0) continue;
5595
+ const inferred = mediaTypeFromPath(value);
5596
+ if (inferred) return inferred;
5597
+ }
5598
+ return null;
5599
+ }
5600
+ async function validatePublishMediaUrl(url) {
5601
+ try {
5602
+ if (new URL(url).protocol !== "https:") {
5603
+ return "Media URLs must use HTTPS.";
5604
+ }
5605
+ } catch {
5606
+ return "Media URL is invalid.";
5607
+ }
5608
+ const check = await validateUrlForSSRF(url);
5609
+ return check.isValid ? null : check.error || "Media URL failed safety validation.";
5610
+ }
5207
5611
  function accountEffectiveStatus(account) {
5208
5612
  return account.effective_status || account.status;
5209
5613
  }
@@ -5222,22 +5626,11 @@ function requestedAccountIdForPlatform(accountId, accountIds, platform3) {
5222
5626
  if (!accountIds) return void 0;
5223
5627
  return accountIds[platform3] || accountIds[platform3.toLowerCase()];
5224
5628
  }
5225
- function isAlreadyR2Signed(url) {
5226
- try {
5227
- const u = new URL(url);
5228
- return u.searchParams.has("X-Amz-Signature");
5229
- } catch {
5230
- return false;
5231
- }
5232
- }
5233
5629
  async function rehostExternalUrl(mediaUrl, projectId) {
5234
5630
  const ssrf = await validateUrlForSSRF(mediaUrl);
5235
5631
  if (!ssrf.isValid) {
5236
5632
  return { error: ssrf.error ?? "URL rejected by SSRF check" };
5237
5633
  }
5238
- if (isAlreadyR2Signed(mediaUrl)) {
5239
- return { signedUrl: mediaUrl, r2Key: "" };
5240
- }
5241
5634
  const { data, error } = await callEdgeFunction(
5242
5635
  "upload-to-r2",
5243
5636
  { url: ssrf.sanitizedUrl ?? mediaUrl, projectId },
@@ -5256,19 +5649,19 @@ function registerDistributionTools(server) {
5256
5649
  media_url: z3.string().optional().describe(
5257
5650
  "URL of the media file to post. Any public HTTPS URL works \u2014 including ephemeral generator URLs (Replicate, OpenAI, DALL-E). The server persists non-R2 URLs into R2 before posting so scheduled posts and byte-upload platforms (X, LinkedIn, YouTube, Bluesky) do not 404 when the source URL expires. Set auto_rehost=false to skip. Not needed if media_urls, r2_key, or job_id is provided."
5258
5651
  ),
5259
- media_urls: z3.array(z3.string()).optional().describe(
5652
+ media_urls: z3.array(z3.string().url()).min(2).max(10).optional().describe(
5260
5653
  "Array of 2-10 image URLs for carousel posts. Same rehosting rules as media_url \u2014 ephemeral URLs are persisted automatically. Use with media_type=CAROUSEL_ALBUM."
5261
5654
  ),
5262
5655
  r2_key: z3.string().optional().describe(
5263
5656
  "R2 object key from upload_media. Signed on demand at post time \u2014 survives scheduling delays. Alternative to media_url."
5264
5657
  ),
5265
- r2_keys: z3.array(z3.string()).optional().describe(
5658
+ r2_keys: z3.array(z3.string()).min(2).max(10).optional().describe(
5266
5659
  "Array of R2 object keys for carousel posts. Each is signed on demand. Alternative to media_urls."
5267
5660
  ),
5268
5661
  job_id: z3.string().optional().describe(
5269
5662
  "Async job ID from generate_image/generate_video. Resolves the completed job's R2 key and signs it. Alternative to media_url/r2_key."
5270
5663
  ),
5271
- job_ids: z3.array(z3.string()).optional().describe(
5664
+ job_ids: z3.array(z3.string()).min(2).max(10).optional().describe(
5272
5665
  "Array of async job IDs for carousel posts. Each resolved to its R2 key. Alternative to media_urls/r2_keys."
5273
5666
  ),
5274
5667
  platform_metadata: z3.object({
@@ -5298,7 +5691,10 @@ function registerDistributionTools(server) {
5298
5691
  category_id: z3.string().optional(),
5299
5692
  tags: z3.array(z3.string()).optional(),
5300
5693
  made_for_kids: z3.boolean().optional(),
5301
- notify_subscribers: z3.boolean().optional()
5694
+ notify_subscribers: z3.boolean().optional(),
5695
+ contains_synthetic_media: z3.boolean().optional().describe(
5696
+ "YouTube altered-or-synthetic-content disclosure. Defaults to true for MCP posts; set false explicitly for verified non-AI media."
5697
+ )
5302
5698
  }).optional(),
5303
5699
  facebook: z3.object({
5304
5700
  page_id: z3.string().optional().describe("Facebook Page ID to post to."),
@@ -5381,14 +5777,8 @@ function registerDistributionTools(server) {
5381
5777
  auto_rehost: z3.boolean().optional().describe(
5382
5778
  "Whether to persist non-R2 media_url/media_urls into R2 before posting. Default: true. Set to false only if you know the source URL will outlive the scheduling window and every target platform supports URL ingest."
5383
5779
  ),
5384
- visual_gate_result: z3.object({ passed: z3.boolean() }).passthrough().optional().describe(
5385
- "Visual QA gate verdict from the carousel/image generation pipeline. Required when mediaType is CAROUSEL_ALBUM, IMAGE, or VIDEO and VISUAL_GATE_ENFORCE is enabled on the EF. Produce with visual_quality_check tool. Must have passed=true for the gate to allow the publish."
5386
- ),
5387
- origin: z3.enum(["human", "hermes", "user"]).optional().describe(
5388
- "Originator lineage for the post. Marks who scheduled it; use the agent value when calling from an autonomous workflow, otherwise default 'human'."
5389
- ),
5390
- hermes_run_id: z3.string().optional().describe(
5391
- "Optional agent run identifier for traceability between a draft and the run that produced it."
5780
+ idempotency_key: z3.string().regex(/^[a-zA-Z0-9_-]{8,128}$/).optional().describe(
5781
+ "Stable 8-128 character retry key (letters, numbers, underscore, hyphen). Reuse the same key when retrying the same publish request to prevent duplicate posts."
5392
5782
  )
5393
5783
  },
5394
5784
  async ({
@@ -5411,9 +5801,7 @@ function registerDistributionTools(server) {
5411
5801
  platform_metadata,
5412
5802
  account_id,
5413
5803
  account_ids,
5414
- visual_gate_result,
5415
- origin,
5416
- hermes_run_id
5804
+ idempotency_key
5417
5805
  }) => {
5418
5806
  const format = response_format ?? "text";
5419
5807
  if ((!caption || caption.trim().length === 0) && (!title || title.trim().length === 0)) {
@@ -5442,6 +5830,8 @@ function registerDistributionTools(server) {
5442
5830
  }
5443
5831
  let resolvedMediaUrl = media_url;
5444
5832
  let resolvedMediaUrls = media_urls;
5833
+ let resolvedMediaUrlIsTrustedR2 = false;
5834
+ let resolvedMediaUrlsAreTrustedR2 = (media_urls ?? []).map(() => false);
5445
5835
  const signR2Key = async (key) => {
5446
5836
  const cleanKey = key.startsWith("r2://") ? key.slice(5) : key;
5447
5837
  const { data: signData } = await callEdgeFunction(
@@ -5459,8 +5849,11 @@ function registerDistributionTools(server) {
5459
5849
  );
5460
5850
  const resultUrl = jobData?.job?.result_url;
5461
5851
  if (!resultUrl) return null;
5462
- if (!resultUrl.startsWith("http")) return signR2Key(resultUrl);
5463
- return resultUrl;
5852
+ if (!resultUrl.startsWith("http")) {
5853
+ const signed = await signR2Key(resultUrl);
5854
+ return signed ? { url: signed, trustedR2: true } : null;
5855
+ }
5856
+ return { url: resultUrl, trustedR2: false };
5464
5857
  };
5465
5858
  try {
5466
5859
  if (r2_key && !resolvedMediaUrl) {
@@ -5477,6 +5870,7 @@ function registerDistributionTools(server) {
5477
5870
  };
5478
5871
  }
5479
5872
  resolvedMediaUrl = signed;
5873
+ resolvedMediaUrlIsTrustedR2 = true;
5480
5874
  } else if (job_id && !resolvedMediaUrl && !r2_key) {
5481
5875
  const resolved = await resolveJobId(job_id);
5482
5876
  if (!resolved) {
@@ -5490,7 +5884,8 @@ function registerDistributionTools(server) {
5490
5884
  isError: true
5491
5885
  };
5492
5886
  }
5493
- resolvedMediaUrl = resolved;
5887
+ resolvedMediaUrl = resolved.url;
5888
+ resolvedMediaUrlIsTrustedR2 = resolved.trustedR2;
5494
5889
  }
5495
5890
  if (r2_keys && r2_keys.length > 0 && !resolvedMediaUrls) {
5496
5891
  const signed = await Promise.all(r2_keys.map(signR2Key));
@@ -5507,6 +5902,7 @@ function registerDistributionTools(server) {
5507
5902
  };
5508
5903
  }
5509
5904
  resolvedMediaUrls = signed;
5905
+ resolvedMediaUrlsAreTrustedR2 = signed.map(() => true);
5510
5906
  } else if (job_ids && job_ids.length > 0 && !resolvedMediaUrls && !r2_keys) {
5511
5907
  const resolved = await Promise.all(job_ids.map(resolveJobId));
5512
5908
  const failIdx = resolved.findIndex((r) => !r);
@@ -5521,10 +5917,12 @@ function registerDistributionTools(server) {
5521
5917
  isError: true
5522
5918
  };
5523
5919
  }
5524
- resolvedMediaUrls = resolved;
5920
+ const resolvedJobs = resolved;
5921
+ resolvedMediaUrls = resolvedJobs.map((item) => item.url);
5922
+ resolvedMediaUrlsAreTrustedR2 = resolvedJobs.map((item) => item.trustedR2);
5525
5923
  }
5526
5924
  const shouldRehost = auto_rehost !== false;
5527
- if (shouldRehost && resolvedMediaUrl) {
5925
+ if (shouldRehost && resolvedMediaUrl && !resolvedMediaUrlIsTrustedR2) {
5528
5926
  const rehost = await rehostExternalUrl(resolvedMediaUrl, project_id);
5529
5927
  if ("error" in rehost) {
5530
5928
  return {
@@ -5538,10 +5936,13 @@ function registerDistributionTools(server) {
5538
5936
  };
5539
5937
  }
5540
5938
  resolvedMediaUrl = rehost.signedUrl;
5939
+ resolvedMediaUrlIsTrustedR2 = true;
5541
5940
  }
5542
5941
  if (shouldRehost && resolvedMediaUrls && resolvedMediaUrls.length > 0) {
5543
5942
  const rehosted = await Promise.all(
5544
- resolvedMediaUrls.map((u) => rehostExternalUrl(u, project_id))
5943
+ resolvedMediaUrls.map(
5944
+ (u, index) => resolvedMediaUrlsAreTrustedR2[index] ? Promise.resolve({ signedUrl: u, r2Key: "" }) : rehostExternalUrl(u, project_id)
5945
+ )
5545
5946
  );
5546
5947
  const failIdx = rehosted.findIndex((r) => "error" in r);
5547
5948
  if (failIdx !== -1) {
@@ -5557,6 +5958,7 @@ function registerDistributionTools(server) {
5557
5958
  };
5558
5959
  }
5559
5960
  resolvedMediaUrls = rehosted.map((r) => r.signedUrl);
5961
+ resolvedMediaUrlsAreTrustedR2 = resolvedMediaUrls.map(() => true);
5560
5962
  }
5561
5963
  } catch (resolveErr) {
5562
5964
  return {
@@ -5569,6 +5971,42 @@ function registerDistributionTools(server) {
5569
5971
  isError: true
5570
5972
  };
5571
5973
  }
5974
+ const hasResolvedMedia = Boolean(
5975
+ resolvedMediaUrl || resolvedMediaUrls && resolvedMediaUrls.length > 0
5976
+ );
5977
+ const resolvedMediaType = inferScheduleMediaType(
5978
+ media_type,
5979
+ [resolvedMediaUrl, r2_key],
5980
+ [resolvedMediaUrls, r2_keys]
5981
+ );
5982
+ if (hasResolvedMedia && !resolvedMediaType) {
5983
+ return {
5984
+ content: [
5985
+ {
5986
+ type: "text",
5987
+ text: "media_type is required when the media format cannot be inferred from a file extension. Set IMAGE, VIDEO, or CAROUSEL_ALBUM explicitly."
5988
+ }
5989
+ ],
5990
+ isError: true
5991
+ };
5992
+ }
5993
+ const finalUrls = [resolvedMediaUrl, ...resolvedMediaUrls ?? []].filter(
5994
+ (value) => typeof value === "string" && value.length > 0
5995
+ );
5996
+ for (const url of finalUrls) {
5997
+ const validationError = await validatePublishMediaUrl(url);
5998
+ if (validationError) {
5999
+ return {
6000
+ content: [
6001
+ {
6002
+ type: "text",
6003
+ text: `Media URL blocked: ${validationError}`
6004
+ }
6005
+ ],
6006
+ isError: true
6007
+ };
6008
+ }
6009
+ }
5572
6010
  const normalizedPlatforms = platforms.map(
5573
6011
  (p) => PLATFORM_CASE_MAP[p.toLowerCase()] || p
5574
6012
  );
@@ -5694,12 +6132,32 @@ Created with Social Neuron`;
5694
6132
  };
5695
6133
  return true;
5696
6134
  })();
6135
+ const defaultAiDisclosure = (platformKey, field) => {
6136
+ const existing = normalizedPlatformMetadata?.[platformKey];
6137
+ if (existing && existing[field] !== void 0) return;
6138
+ normalizedPlatformMetadata = {
6139
+ ...normalizedPlatformMetadata ?? {},
6140
+ [platformKey]: {
6141
+ ...existing ?? {},
6142
+ [field]: true
6143
+ }
6144
+ };
6145
+ };
6146
+ if (normalizedPlatforms.includes("TikTok")) {
6147
+ defaultAiDisclosure("tiktok", "is_ai_generated");
6148
+ }
6149
+ if (normalizedPlatforms.includes("Instagram")) {
6150
+ defaultAiDisclosure("instagram", "is_ai_generated");
6151
+ }
6152
+ if (normalizedPlatforms.includes("YouTube")) {
6153
+ defaultAiDisclosure("youtube", "contains_synthetic_media");
6154
+ }
5697
6155
  const { data, error } = await callEdgeFunction(
5698
6156
  "schedule-post",
5699
6157
  {
5700
6158
  mediaUrl: resolvedMediaUrl,
5701
6159
  mediaUrls: resolvedMediaUrls,
5702
- mediaType: media_type,
6160
+ mediaType: resolvedMediaType ?? void 0,
5703
6161
  caption: finalCaption,
5704
6162
  platforms: normalizedPlatforms,
5705
6163
  title,
@@ -5712,14 +6170,10 @@ Created with Social Neuron`;
5712
6170
  normalizedPlatformMetadata
5713
6171
  )
5714
6172
  } : {},
5715
- // Forward visual gate verdict + source so the EF can enforce on media posts.
5716
- ...visual_gate_result ? { visualGateResult: visual_gate_result } : {},
5717
- visualGateSource: "mcp",
5718
- // Originator lineage (Hermes integration, 2026-05-22). EF validates origin
5719
- // against the posts.origin CHECK constraint and persists hermesRunId when
5720
- // origin === 'hermes'.
5721
- ...origin ? { origin } : {},
5722
- ...hermes_run_id ? { hermesRunId: hermes_run_id } : {}
6173
+ ...idempotency_key ? { idempotencyKey: idempotency_key } : {}
6174
+ // Attribution is assigned by the authenticated gateway. Visual QA
6175
+ // attestations are server-produced evidence and are deliberately not
6176
+ // accepted from an MCP caller.
5723
6177
  },
5724
6178
  { timeoutMs: 3e4 }
5725
6179
  );
@@ -5785,6 +6239,99 @@ Created with Social Neuron`;
5785
6239
  };
5786
6240
  }
5787
6241
  );
6242
+ server.tool(
6243
+ "reschedule_post",
6244
+ "Move an existing pending or scheduled post to a new future time without creating a duplicate. Pass project_id for the post's brand. expected_scheduled_at is recommended: it prevents overwriting a change made in another client after the calendar was loaded.",
6245
+ {
6246
+ post_id: z3.string().uuid().describe("Post ID returned by list_recent_posts."),
6247
+ project_id: z3.string().uuid().optional().describe(
6248
+ "Brand/project ID that owns the post. Defaults to the authenticated key's project or the account default."
6249
+ ),
6250
+ scheduled_at: z3.string().datetime({ offset: true }).describe("New future publish time as an ISO 8601 datetime with timezone."),
6251
+ expected_scheduled_at: z3.string().datetime({ offset: true }).optional().describe(
6252
+ "Optional current schedule timestamp. If it changed since you read it, the update is rejected instead of silently overwriting it."
6253
+ ),
6254
+ response_format: z3.enum(["text", "json"]).default("text")
6255
+ },
6256
+ async ({
6257
+ post_id,
6258
+ project_id,
6259
+ scheduled_at,
6260
+ expected_scheduled_at,
6261
+ response_format
6262
+ }) => {
6263
+ const next = new Date(scheduled_at);
6264
+ if (!Number.isFinite(next.getTime()) || next.getTime() <= Date.now()) {
6265
+ return {
6266
+ content: [
6267
+ {
6268
+ type: "text",
6269
+ text: "scheduled_at must be a valid future ISO datetime with timezone."
6270
+ }
6271
+ ],
6272
+ isError: true
6273
+ };
6274
+ }
6275
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
6276
+ if (!resolvedProjectId) {
6277
+ return {
6278
+ content: [
6279
+ {
6280
+ type: "text",
6281
+ text: "No project_id was provided and no default project is configured."
6282
+ }
6283
+ ],
6284
+ isError: true
6285
+ };
6286
+ }
6287
+ const userId = await getDefaultUserId();
6288
+ const rateLimit = checkRateLimit("posting", `reschedule_post:${userId}`);
6289
+ if (!rateLimit.allowed) {
6290
+ return {
6291
+ content: [
6292
+ {
6293
+ type: "text",
6294
+ text: `Rate limit exceeded. Retry in ~${rateLimit.retryAfter}s.`
6295
+ }
6296
+ ],
6297
+ isError: true
6298
+ };
6299
+ }
6300
+ const { data: result, error } = await callEdgeFunction("mcp-data", {
6301
+ action: "reschedule-scheduled-post",
6302
+ post_id,
6303
+ projectId: resolvedProjectId,
6304
+ project_id: resolvedProjectId,
6305
+ scheduled_at: next.toISOString(),
6306
+ ...expected_scheduled_at ? { expected_scheduled_at: new Date(expected_scheduled_at).toISOString() } : {}
6307
+ });
6308
+ if (error || !result?.success) {
6309
+ const code = result?.error ?? error ?? "reschedule_failed";
6310
+ const recovery = code === "publishing_in_progress" ? "The worker has already started publishing this post." : code === "schedule_conflict" ? `The schedule changed in another client${result?.current_scheduled_at ? ` to ${result.current_scheduled_at}` : ""}; refresh the calendar before retrying.` : code === "not_found" ? "The post was not found in this project." : code === "not_reschedulable" ? `This post can no longer be rescheduled${result?.status ? ` (status: ${result.status})` : ""}.` : "The post could not be rescheduled.";
6311
+ return {
6312
+ content: [{ type: "text", text: recovery }],
6313
+ isError: true
6314
+ };
6315
+ }
6316
+ const publicResult = {
6317
+ success: true,
6318
+ post_id: result.post_id ?? post_id,
6319
+ project_id: result.project_id ?? resolvedProjectId,
6320
+ previous_scheduled_at: result.previous_scheduled_at ?? null,
6321
+ scheduled_at: result.scheduled_at ?? next.toISOString()
6322
+ };
6323
+ const structuredContent = asEnvelope3(publicResult);
6324
+ return {
6325
+ structuredContent,
6326
+ content: [
6327
+ {
6328
+ type: "text",
6329
+ text: response_format === "json" ? JSON.stringify(structuredContent, null, 2) : `Post ${publicResult.post_id} rescheduled to ${publicResult.scheduled_at}.`
6330
+ }
6331
+ ]
6332
+ };
6333
+ }
6334
+ );
5788
6335
  server.tool(
5789
6336
  "list_connected_accounts",
5790
6337
  "Check which social platforms have active OAuth connections for posting. Call this before schedule_post to verify credentials. Pass project_id to list the accounts for a specific brand/project, then pass the returned account id as account_id/account_ids when posting. If a platform is missing or expired, the user needs to reconnect at socialneuron.com/settings/connections.",
@@ -5813,14 +6360,16 @@ Created with Social Neuron`;
5813
6360
  isError: true
5814
6361
  };
5815
6362
  }
5816
- const accounts = result.accounts ?? [];
6363
+ const accounts = (result.accounts ?? []).map(publicConnectedAccount).filter((account) => account !== null);
5817
6364
  if (accounts.length === 0) {
5818
6365
  if (format === "json") {
6366
+ const structuredContent = asEnvelope3({ accounts: [] });
5819
6367
  return {
6368
+ structuredContent,
5820
6369
  content: [
5821
6370
  {
5822
6371
  type: "text",
5823
- text: JSON.stringify(asEnvelope3({ accounts: [] }), null, 2)
6372
+ text: JSON.stringify(structuredContent, null, 2)
5824
6373
  }
5825
6374
  ]
5826
6375
  };
@@ -5848,11 +6397,13 @@ Created with Social Neuron`;
5848
6397
  );
5849
6398
  }
5850
6399
  if (format === "json") {
6400
+ const structuredContent = asEnvelope3({ accounts });
5851
6401
  return {
6402
+ structuredContent,
5852
6403
  content: [
5853
6404
  {
5854
6405
  type: "text",
5855
- text: JSON.stringify(asEnvelope3({ accounts }), null, 2)
6406
+ text: JSON.stringify(structuredContent, null, 2)
5856
6407
  }
5857
6408
  ]
5858
6409
  };
@@ -5866,6 +6417,9 @@ Created with Social Neuron`;
5866
6417
  "list_recent_posts",
5867
6418
  "List recent published and scheduled posts with status, platform, title, and timestamps. Use to check what has been posted before planning new content, or to find post IDs for fetch_analytics. Filter by platform or status to narrow results.",
5868
6419
  {
6420
+ project_id: z3.string().uuid().optional().describe(
6421
+ "Brand/project ID to scope posts. Defaults to the authenticated key's project or the account default."
6422
+ ),
5869
6423
  platform: z3.enum([
5870
6424
  "youtube",
5871
6425
  "tiktok",
@@ -5881,13 +6435,27 @@ Created with Social Neuron`;
5881
6435
  limit: z3.number().min(1).max(50).optional().describe("Maximum number of posts to return. Defaults to 20."),
5882
6436
  response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
5883
6437
  },
5884
- async ({ platform: platform3, status, days, limit, response_format }) => {
6438
+ async ({ project_id, platform: platform3, status, days, limit, response_format }) => {
5885
6439
  const format = response_format ?? "text";
5886
6440
  const lookbackDays = days ?? 7;
5887
- const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
5888
- action: "recent-posts",
5889
- days: lookbackDays,
6441
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
6442
+ if (!resolvedProjectId) {
6443
+ return {
6444
+ content: [
6445
+ {
6446
+ type: "text",
6447
+ text: "No project_id was provided and no default project is configured."
6448
+ }
6449
+ ],
6450
+ isError: true
6451
+ };
6452
+ }
6453
+ const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
6454
+ action: "recent-posts",
6455
+ days: lookbackDays,
5890
6456
  limit: limit ?? 20,
6457
+ projectId: resolvedProjectId,
6458
+ project_id: resolvedProjectId,
5891
6459
  ...platform3 ? { platform: platform3 } : {},
5892
6460
  ...status ? { status } : {}
5893
6461
  });
@@ -5902,7 +6470,7 @@ Created with Social Neuron`;
5902
6470
  isError: true
5903
6471
  };
5904
6472
  }
5905
- const rows = result.posts ?? [];
6473
+ const rows = (result.posts ?? []).map(publicPostRecord).filter((post) => post !== null);
5906
6474
  if (rows.length === 0) {
5907
6475
  if (format === "json") {
5908
6476
  const structuredContent2 = asEnvelope3({ posts: [] });
@@ -5977,8 +6545,11 @@ Created with Social Neuron`;
5977
6545
  };
5978
6546
  server.tool(
5979
6547
  "find_next_slots",
5980
- "Find optimal posting time slots based on best posting times and existing schedule. Returns non-conflicting slots sorted by engagement score.",
6548
+ "Find optimal posting time slots for one brand/project based on preferred posting times and that project's existing schedule. Returns non-conflicting slots sorted by engagement score.",
5981
6549
  {
6550
+ project_id: z3.string().uuid().optional().describe(
6551
+ "Brand/project ID used for conflict detection. Defaults to the authenticated key's project or the account default."
6552
+ ),
5982
6553
  platforms: z3.array(
5983
6554
  z3.enum([
5984
6555
  "youtube",
@@ -5997,6 +6568,7 @@ Created with Social Neuron`;
5997
6568
  response_format: z3.enum(["text", "json"]).default("text")
5998
6569
  },
5999
6570
  async ({
6571
+ project_id,
6000
6572
  platforms,
6001
6573
  count,
6002
6574
  start_after,
@@ -6004,13 +6576,35 @@ Created with Social Neuron`;
6004
6576
  response_format
6005
6577
  }) => {
6006
6578
  try {
6579
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
6580
+ if (!resolvedProjectId) {
6581
+ return {
6582
+ content: [
6583
+ {
6584
+ type: "text",
6585
+ text: "No project_id was provided and no default project is configured."
6586
+ }
6587
+ ],
6588
+ isError: true
6589
+ };
6590
+ }
6007
6591
  const startDate = start_after ? new Date(start_after) : /* @__PURE__ */ new Date();
6592
+ if (!Number.isFinite(startDate.getTime())) {
6593
+ return {
6594
+ content: [
6595
+ { type: "text", text: "start_after must be a valid ISO datetime." }
6596
+ ],
6597
+ isError: true
6598
+ };
6599
+ }
6008
6600
  const endDate = new Date(startDate.getTime() + 7 * 24 * 60 * 60 * 1e3);
6009
6601
  const { data: postsResult, error: postsError } = await callEdgeFunction("mcp-data", {
6010
6602
  action: "scheduled-posts",
6011
6603
  start_date: startDate.toISOString(),
6012
6604
  end_date: endDate.toISOString(),
6013
- statuses: ["scheduled", "draft"]
6605
+ statuses: ["pending", "scheduled", "draft"],
6606
+ projectId: resolvedProjectId,
6607
+ project_id: resolvedProjectId
6014
6608
  });
6015
6609
  const existingPosts = postsError ? [] : postsResult?.posts ?? [];
6016
6610
  const gapMs = min_gap_hours * 60 * 60 * 1e3;
@@ -6050,19 +6644,17 @@ Created with Social Neuron`;
6050
6644
  const slots = candidates.filter((s) => !s.conflict).sort((a, b) => b.engagement_score - a.engagement_score).slice(0, count);
6051
6645
  const conflictsAvoided = candidates.filter((s) => s.conflict).length;
6052
6646
  if (response_format === "json") {
6647
+ const structuredContent = asEnvelope3({
6648
+ slots,
6649
+ total_candidates: candidates.length,
6650
+ conflicts_avoided: conflictsAvoided
6651
+ });
6053
6652
  return {
6653
+ structuredContent,
6054
6654
  content: [
6055
6655
  {
6056
6656
  type: "text",
6057
- text: JSON.stringify(
6058
- asEnvelope3({
6059
- slots,
6060
- total_candidates: candidates.length,
6061
- conflicts_avoided: conflictsAvoided
6062
- }),
6063
- null,
6064
- 2
6065
- )
6657
+ text: JSON.stringify(structuredContent, null, 2)
6066
6658
  }
6067
6659
  ],
6068
6660
  isError: false
@@ -6427,7 +7019,7 @@ Created with Social Neuron`;
6427
7019
  const results = [];
6428
7020
  const buildIdempotencyKey = (post) => {
6429
7021
  const planId = effectivePlanId ?? (typeof workingPlan.plan_id === "string" ? String(workingPlan.plan_id) : "inline");
6430
- const captionHash = createHash3("sha256").update(post.caption).digest("hex").slice(0, 16);
7022
+ const captionHash = createHash2("sha256").update(post.caption).digest("hex").slice(0, 16);
6431
7023
  const raw = [
6432
7024
  "schedule_content_plan",
6433
7025
  planId,
@@ -6437,7 +7029,7 @@ Created with Social Neuron`;
6437
7029
  captionHash,
6438
7030
  idempotency_seed ?? ""
6439
7031
  ].join(":");
6440
- return `plan-${createHash3("sha256").update(raw).digest("hex").slice(0, 48)}`;
7032
+ return `plan-${createHash2("sha256").update(raw).digest("hex").slice(0, 48)}`;
6441
7033
  };
6442
7034
  const scheduleOne = async (post) => {
6443
7035
  if (!post.schedule_at) {
@@ -6623,7 +7215,7 @@ Created with Social Neuron`;
6623
7215
  }
6624
7216
  );
6625
7217
  }
6626
- var PLATFORM_CASE_MAP, TIKTOK_AUDIT_APPROVED, MCP_NOT_LIVE_FOR_POSTING;
7218
+ var PLATFORM_CASE_MAP, TIKTOK_AUDIT_APPROVED, MCP_NOT_LIVE_FOR_POSTING, VIDEO_FILE_EXTENSIONS, IMAGE_FILE_EXTENSIONS;
6627
7219
  var init_distribution = __esm({
6628
7220
  "src/tools/distribution.ts"() {
6629
7221
  "use strict";
@@ -6644,8 +7236,29 @@ var init_distribution = __esm({
6644
7236
  threads: "Threads",
6645
7237
  bluesky: "Bluesky"
6646
7238
  };
6647
- TIKTOK_AUDIT_APPROVED = false;
7239
+ TIKTOK_AUDIT_APPROVED = !["false", "0", "no"].includes(
7240
+ (process.env.TIKTOK_AUDIT_APPROVED ?? "").toLowerCase()
7241
+ );
6648
7242
  MCP_NOT_LIVE_FOR_POSTING = /* @__PURE__ */ new Set(["LinkedIn", "Pinterest", "Reddit"]);
7243
+ VIDEO_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
7244
+ "mp4",
7245
+ "mov",
7246
+ "m4v",
7247
+ "webm",
7248
+ "avi",
7249
+ "mkv",
7250
+ "mpeg",
7251
+ "mpg"
7252
+ ]);
7253
+ IMAGE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
7254
+ "jpg",
7255
+ "jpeg",
7256
+ "png",
7257
+ "webp",
7258
+ "gif",
7259
+ "avif",
7260
+ "heic"
7261
+ ]);
6649
7262
  }
6650
7263
  });
6651
7264
 
@@ -6926,7 +7539,10 @@ function registerMediaTools(server) {
6926
7539
  content: [
6927
7540
  {
6928
7541
  type: "text",
6929
- text: `R2 upload failed (HTTP ${putResp.status}): ${await putResp.text().catch(() => "Unknown error")}`
7542
+ // Presigned-store bodies may include provider request IDs,
7543
+ // bucket names, or signed URL fragments. Status is enough
7544
+ // for user recovery and safe diagnostics.
7545
+ text: `R2 upload failed (HTTP ${putResp.status}). Please retry.`
6930
7546
  }
6931
7547
  ],
6932
7548
  isError: true
@@ -7169,7 +7785,11 @@ function registerAnalyticsTools(server) {
7169
7785
  action: "analytics",
7170
7786
  platform: platform3,
7171
7787
  days: lookbackDays,
7172
- limit: maxPosts,
7788
+ // Fetch extra snapshots, then deduplicate to the requested post count.
7789
+ // The backend understands latestOnly after the paired application
7790
+ // deployment; older deployments safely ignore the hint.
7791
+ limit: Math.min(maxPosts * 5, 100),
7792
+ latestOnly: true,
7173
7793
  contentId: content_id,
7174
7794
  ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
7175
7795
  });
@@ -7179,7 +7799,14 @@ function registerAnalyticsTools(server) {
7179
7799
  isError: true
7180
7800
  };
7181
7801
  }
7182
- const rows = result?.rows ?? [];
7802
+ const snapshotRows = result?.rows ?? [];
7803
+ const seenSnapshots = /* @__PURE__ */ new Set();
7804
+ const rows = [...snapshotRows].sort((a, b) => b.captured_at.localeCompare(a.captured_at)).filter((row) => {
7805
+ const key = `${row.post_id}:${row.platform.toLowerCase()}`;
7806
+ if (seenSnapshots.has(key)) return false;
7807
+ seenSnapshots.add(key);
7808
+ return true;
7809
+ }).slice(0, maxPosts);
7183
7810
  if (rows.length === 0) {
7184
7811
  if (format === "json") {
7185
7812
  const structuredContent = asEnvelope4({
@@ -7358,6 +7985,84 @@ var init_analytics2 = __esm({
7358
7985
  }
7359
7986
  });
7360
7987
 
7988
+ // src/lib/brandUrlInput.ts
7989
+ function canonicalizeHost(hostname) {
7990
+ return hostname.replace(UNICODE_DOT_VARIANTS, ".").replace(/\.+$/, "");
7991
+ }
7992
+ function hostLooksLikeDomain(hostname) {
7993
+ return /\S+\.\S+/.test(canonicalizeHost(hostname));
7994
+ }
7995
+ function resolveHandle(handle, platform3) {
7996
+ const normalized = handle.replace(/^@/, "").trim();
7997
+ if (!normalized) return { url: null, handle: null, platform: null, ambiguous: false };
7998
+ if (!platform3) return { url: null, handle: normalized, platform: null, ambiguous: true };
7999
+ return {
8000
+ url: PROFILE_URL_BUILDERS[platform3](normalized),
8001
+ handle: normalized,
8002
+ platform: platform3,
8003
+ ambiguous: false
8004
+ };
8005
+ }
8006
+ function normalizeBrandUrlInput(raw, explicitPlatform) {
8007
+ const input = (raw ?? "").trim();
8008
+ if (!input) return { url: null, handle: null, platform: null, ambiguous: false };
8009
+ if (/^https?:\/\//i.test(input)) {
8010
+ let parsed;
8011
+ try {
8012
+ parsed = new URL(input);
8013
+ } catch {
8014
+ return { url: null, handle: null, platform: null, ambiguous: false, invalidUrl: true };
8015
+ }
8016
+ if (hostLooksLikeDomain(parsed.hostname)) {
8017
+ return { url: parsed.toString(), handle: null, platform: null, ambiguous: false };
8018
+ }
8019
+ return resolveHandle(canonicalizeHost(parsed.hostname), explicitPlatform);
8020
+ }
8021
+ const qualified = input.match(/^([a-z]{1,10}):\s*@?(.+)$/i);
8022
+ if (qualified) {
8023
+ const platform3 = PLATFORM_ALIASES[qualified[1].toLowerCase()];
8024
+ if (platform3) return resolveHandle(qualified[2], platform3);
8025
+ }
8026
+ if (input.startsWith("@")) return resolveHandle(input, explicitPlatform);
8027
+ if (!/\s/.test(input)) {
8028
+ try {
8029
+ const parsed = new URL(`https://${input}`);
8030
+ if (hostLooksLikeDomain(parsed.hostname)) {
8031
+ return { url: parsed.toString(), handle: null, platform: null, ambiguous: false };
8032
+ }
8033
+ } catch {
8034
+ }
8035
+ }
8036
+ return resolveHandle(input.replace(UNICODE_DOT_VARIANTS, ".").replace(/\.+$/, ""), explicitPlatform);
8037
+ }
8038
+ var PLATFORM_ALIASES, PROFILE_URL_BUILDERS, UNICODE_DOT_VARIANTS;
8039
+ var init_brandUrlInput = __esm({
8040
+ "src/lib/brandUrlInput.ts"() {
8041
+ "use strict";
8042
+ PLATFORM_ALIASES = {
8043
+ instagram: "instagram",
8044
+ ig: "instagram",
8045
+ insta: "instagram",
8046
+ tiktok: "tiktok",
8047
+ tt: "tiktok",
8048
+ twitter: "twitter",
8049
+ x: "twitter",
8050
+ linkedin: "linkedin",
8051
+ li: "linkedin",
8052
+ youtube: "youtube",
8053
+ yt: "youtube"
8054
+ };
8055
+ PROFILE_URL_BUILDERS = {
8056
+ instagram: (handle) => `https://instagram.com/${encodeURIComponent(handle)}`,
8057
+ tiktok: (handle) => `https://tiktok.com/@${encodeURIComponent(handle)}`,
8058
+ twitter: (handle) => `https://x.com/${encodeURIComponent(handle)}`,
8059
+ linkedin: (handle) => `https://linkedin.com/company/${encodeURIComponent(handle)}`,
8060
+ youtube: (handle) => `https://youtube.com/@${encodeURIComponent(handle)}`
8061
+ };
8062
+ UNICODE_DOT_VARIANTS = /[。.。]/g;
8063
+ }
8064
+ });
8065
+
7361
8066
  // src/tools/brand.ts
7362
8067
  import { z as z6 } from "zod";
7363
8068
  function asEnvelope5(data) {
@@ -7374,12 +8079,31 @@ function registerBrandTools(server) {
7374
8079
  "extract_brand",
7375
8080
  "Analyze a website URL and extract brand identity data including brand name, colors, voice/tone, target audience, and logo. Uses AI-powered analysis of the page HTML. Useful for understanding a brand before generating content for it.",
7376
8081
  {
7377
- url: z6.string().url().describe(
7378
- 'The website URL to analyze for brand identity (e.g. "https://example.com").'
8082
+ url: z6.string().min(1).max(2048).describe(
8083
+ 'The website URL to analyze for brand identity (e.g. "https://example.com"). Bare handles are rejected; supply a full profile URL or use "platform:handle" shorthand (e.g. "instagram:acmefoods").'
7379
8084
  ),
7380
8085
  response_format: z6.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
7381
8086
  },
7382
- async ({ url, response_format }) => {
8087
+ async ({ url: rawUrl, response_format }) => {
8088
+ const normalized = normalizeBrandUrlInput(rawUrl);
8089
+ if (normalized.invalidUrl) {
8090
+ return {
8091
+ content: [{ type: "text", text: `URL blocked: "${rawUrl}" is not a valid URL.` }],
8092
+ isError: true
8093
+ };
8094
+ }
8095
+ if (!normalized.url) {
8096
+ return {
8097
+ content: [
8098
+ {
8099
+ type: "text",
8100
+ text: `"${rawUrl}" looks like a handle, not a full URL. Provide a profile URL or use "platform:handle" shorthand (for example "instagram:acmefoods").`
8101
+ }
8102
+ ],
8103
+ isError: true
8104
+ };
8105
+ }
8106
+ const url = normalized.url;
7383
8107
  const ssrfCheck = await validateUrlForSSRF(url);
7384
8108
  if (!ssrfCheck.isValid) {
7385
8109
  return {
@@ -7687,6 +8411,7 @@ var init_brand = __esm({
7687
8411
  init_supabase();
7688
8412
  init_ssrf();
7689
8413
  init_version();
8414
+ init_brandUrlInput();
7690
8415
  }
7691
8416
  });
7692
8417
 
@@ -8062,7 +8787,7 @@ function registerRemotionTools(server) {
8062
8787
  },
8063
8788
  async ({ composition_id, output_format, props }) => {
8064
8789
  const userId = await getDefaultUserId();
8065
- const rateLimit = checkRateLimit("screenshot", `render_demo_video:${userId}`);
8790
+ const rateLimit = checkRateLimit("generation", `render_demo_video:${userId}`);
8066
8791
  if (!rateLimit.allowed) {
8067
8792
  return {
8068
8793
  content: [
@@ -11896,7 +12621,9 @@ function toolKnowledgeDocument(tool) {
11896
12621
  function getKnowledgeDocuments() {
11897
12622
  return [
11898
12623
  ...STATIC_KNOWLEDGE_DOCUMENTS,
11899
- ...TOOL_CATALOG.filter((t) => !t.internal).map(toolKnowledgeDocument)
12624
+ ...TOOL_CATALOG.filter((t) => !t.internal && !t.hiddenFromPublicCount).map(
12625
+ toolKnowledgeDocument
12626
+ )
11900
12627
  ];
11901
12628
  }
11902
12629
  function tokenize(input) {
@@ -12013,7 +12740,7 @@ function registerDiscoveryTools(server) {
12013
12740
  if (query) {
12014
12741
  results = searchTools(query);
12015
12742
  }
12016
- results = results.filter((t) => !t.internal);
12743
+ results = results.filter((t) => !t.internal && !t.hiddenFromPublicCount);
12017
12744
  if (module) {
12018
12745
  const moduleTools = getToolsByModule(module);
12019
12746
  results = results.filter((t) => moduleTools.some((mt) => mt.name === t.name));
@@ -13944,11 +14671,11 @@ function hexToLab(hex) {
13944
14671
  const lb = srgbToLinear(b);
13945
14672
  const x = lr * 0.4124564 + lg * 0.3575761 + lb * 0.1804375;
13946
14673
  const y = lr * 0.2126729 + lg * 0.7151522 + lb * 0.072175;
13947
- const z38 = lr * 0.0193339 + lg * 0.119192 + lb * 0.9503041;
14674
+ const z40 = lr * 0.0193339 + lg * 0.119192 + lb * 0.9503041;
13948
14675
  const f = (t) => t > 8856e-6 ? Math.cbrt(t) : 7.787 * t + 16 / 116;
13949
14676
  const fx = f(x / 0.95047);
13950
14677
  const fy = f(y / 1);
13951
- const fz = f(z38 / 1.08883);
14678
+ const fz = f(z40 / 1.08883);
13952
14679
  return { L: 116 * fy - 16, a: 500 * (fx - fy), b: 200 * (fy - fz) };
13953
14680
  }
13954
14681
  function deltaE2000(lab1, lab2) {
@@ -14188,10 +14915,10 @@ function registerBrandRuntimeTools(server) {
14188
14915
  pagesScraped: meta.pagesScraped || 0
14189
14916
  }
14190
14917
  };
14191
- const envelope = asEnvelope23(runtime);
14918
+ const envelope2 = asEnvelope23(runtime);
14192
14919
  return {
14193
- structuredContent: envelope,
14194
- content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
14920
+ structuredContent: envelope2,
14921
+ content: [{ type: "text", text: JSON.stringify(envelope2, null, 2) }]
14195
14922
  };
14196
14923
  }
14197
14924
  );
@@ -14331,9 +15058,9 @@ function registerBrandRuntimeTools(server) {
14331
15058
  }
14332
15059
  const profile = row.profile_data;
14333
15060
  const checkResult = computeBrandConsistency(content, profile);
14334
- const envelope = asEnvelope23(checkResult);
15061
+ const envelope2 = asEnvelope23(checkResult);
14335
15062
  return {
14336
- content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
15063
+ content: [{ type: "text", text: JSON.stringify(envelope2, null, 2) }]
14337
15064
  };
14338
15065
  }
14339
15066
  );
@@ -14365,9 +15092,9 @@ function registerBrandRuntimeTools(server) {
14365
15092
  content_colors,
14366
15093
  threshold ?? 10
14367
15094
  );
14368
- const envelope = asEnvelope23(auditResult);
15095
+ const envelope2 = asEnvelope23(auditResult);
14369
15096
  return {
14370
- content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
15097
+ content: [{ type: "text", text: JSON.stringify(envelope2, null, 2) }]
14371
15098
  };
14372
15099
  }
14373
15100
  );
@@ -14400,9 +15127,9 @@ function registerBrandRuntimeTools(server) {
14400
15127
  row.profile_data.typography,
14401
15128
  format
14402
15129
  );
14403
- const envelope = asEnvelope23({ format, tokens: output });
15130
+ const envelope2 = asEnvelope23({ format, tokens: output });
14404
15131
  return {
14405
- content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
15132
+ content: [{ type: "text", text: JSON.stringify(envelope2, null, 2) }]
14406
15133
  };
14407
15134
  }
14408
15135
  );
@@ -14556,7 +15283,7 @@ function registerCarouselTools(server) {
14556
15283
  };
14557
15284
  }
14558
15285
  const userId = await getDefaultUserId();
14559
- const rateLimit = checkRateLimit("posting", `create_carousel:${userId}`);
15286
+ const rateLimit = checkRateLimit("generation", `create_carousel:${userId}`);
14560
15287
  if (!rateLimit.allowed) {
14561
15288
  return {
14562
15289
  content: [
@@ -14628,7 +15355,12 @@ function registerCarouselTools(server) {
14628
15355
  slideNumber: slide.slideNumber,
14629
15356
  jobId: null,
14630
15357
  model: image_model,
14631
- error: error ?? "No job ID returned"
15358
+ error: error ?? "No job ID returned",
15359
+ creditsReserved: data?.credits_reserved ?? null,
15360
+ creditsCharged: data?.credits_charged ?? null,
15361
+ creditsRefunded: data?.credits_refunded ?? null,
15362
+ billingStatus: data?.billing_status ?? "unknown",
15363
+ failureReason: data?.failure_reason ?? null
14632
15364
  };
14633
15365
  }
14634
15366
  const jobId = data.asyncJobId ?? data.taskId ?? null;
@@ -14640,14 +15372,24 @@ function registerCarouselTools(server) {
14640
15372
  slideNumber: slide.slideNumber,
14641
15373
  jobId,
14642
15374
  model: image_model,
14643
- error: null
15375
+ error: null,
15376
+ creditsReserved: 0,
15377
+ creditsCharged: data.creditsDeducted ?? perImageCost,
15378
+ creditsRefunded: 0,
15379
+ billingStatus: "charged",
15380
+ failureReason: null
14644
15381
  };
14645
15382
  } catch (err) {
14646
15383
  return {
14647
15384
  slideNumber: slide.slideNumber,
14648
15385
  jobId: null,
14649
15386
  model: image_model,
14650
- error: sanitizeError(err)
15387
+ error: sanitizeError(err),
15388
+ creditsReserved: null,
15389
+ creditsCharged: null,
15390
+ creditsRefunded: null,
15391
+ billingStatus: "unknown",
15392
+ failureReason: null
14651
15393
  };
14652
15394
  }
14653
15395
  })
@@ -14672,7 +15414,8 @@ function registerCarouselTools(server) {
14672
15414
  return {
14673
15415
  ...s,
14674
15416
  imageJobId: job?.jobId ?? null,
14675
- imageError: job?.error ?? null
15417
+ imageError: job?.error ?? null,
15418
+ imageBillingStatus: job?.billingStatus ?? "unknown"
14676
15419
  };
14677
15420
  }),
14678
15421
  imageModel: image_model,
@@ -14684,11 +15427,19 @@ function registerCarouselTools(server) {
14684
15427
  jobIds: successfulJobs.map((j) => j.jobId),
14685
15428
  failedSlides: failedJobs.map((j) => ({
14686
15429
  slideNumber: j.slideNumber,
14687
- error: j.error
15430
+ error: j.error,
15431
+ credits_reserved: j.creditsReserved,
15432
+ credits_charged: j.creditsCharged,
15433
+ credits_refunded: j.creditsRefunded,
15434
+ billing_status: j.billingStatus,
15435
+ failure_reason: j.failureReason
14688
15436
  })),
14689
15437
  credits: {
14690
15438
  textGeneration: textCredits,
14691
15439
  imagesEstimated: successfulJobs.length * perImageCost,
15440
+ imagesCharged: imageJobs.reduce((sum, job) => sum + (job.creditsCharged ?? 0), 0),
15441
+ imagesRefunded: imageJobs.reduce((sum, job) => sum + (job.creditsRefunded ?? 0), 0),
15442
+ billingUnknownSlides: imageJobs.filter((job) => job.billingStatus === "unknown").length,
14692
15443
  totalEstimated: textCredits + successfulJobs.length * perImageCost
14693
15444
  }
14694
15445
  }
@@ -14884,7 +15635,7 @@ function registerHyperframesTools(server) {
14884
15635
  );
14885
15636
  server.tool(
14886
15637
  "render_hyperframes",
14887
- "Render an HTML video composition (Hyperframes) to MP4. Author the composition as HTML with data-* timing attributes and GSAP timelines \u2014 frame-accurate, no React build step. Use list_hyperframes_blocks to see the pre-built block catalog. Pass project_id to keep the render with the correct brand/project. Returns a job ID \u2014 poll with check_status.",
15638
+ "Render an HTML video composition (Hyperframes) to MP4 \u2014 frame-accurate, no React build step. The page MUST expose window.__hf = { duration: <seconds>, seek: (t) => void }; the renderer calls seek(t) per frame (GSAP timelines work when driven from seek). Missing window.__hf causes a terminal timeout; poll check_status and verify its reported billing/refund outcome. Use list_hyperframes_blocks for the pre-built block catalog. Pass project_id to keep the render with the correct brand/project. Returns a job ID \u2014 poll with check_status.",
14888
15639
  {
14889
15640
  composition_html: z30.string().max(5e5).optional().describe(
14890
15641
  "Inline HTML composition (full <html>...</html>). Max 500KB. Use composition_url for larger."
@@ -14897,7 +15648,8 @@ function registerHyperframesTools(server) {
14897
15648
  duration_sec: z30.number().min(0.1).max(600).describe("Output duration in seconds. Required. Max 600 (10 min)."),
14898
15649
  fps: z30.union([z30.literal(24), z30.literal(30), z30.literal(60)]).optional().describe("Frames per second. Default 30."),
14899
15650
  quality: z30.enum(["draft", "standard", "high"]).optional().describe("draft: fast iteration; standard: production; high: final master."),
14900
- project_id: z30.string().optional().describe("Project ID to associate the Hyperframes render with.")
15651
+ project_id: z30.string().optional().describe("Project ID to associate the Hyperframes render with."),
15652
+ response_format: z30.enum(["text", "json"]).optional().describe("Response format. Use json for a stable job_id handoff.")
14901
15653
  },
14902
15654
  async ({
14903
15655
  composition_html,
@@ -14907,7 +15659,8 @@ function registerHyperframesTools(server) {
14907
15659
  duration_sec,
14908
15660
  fps,
14909
15661
  quality,
14910
- project_id
15662
+ project_id,
15663
+ response_format
14911
15664
  }) => {
14912
15665
  const userId = await getDefaultUserId();
14913
15666
  const rateLimit = checkRateLimit("generation", `render_hyperframes:${userId}`);
@@ -14958,11 +15711,23 @@ function registerHyperframesTools(server) {
14958
15711
  if (error || !data?.jobId) {
14959
15712
  throw new Error(error || data?.error || "Failed to create Hyperframes render job");
14960
15713
  }
15714
+ const payload = {
15715
+ job_id: data.jobId,
15716
+ jobId: data.jobId,
15717
+ status: data.status,
15718
+ credits_cost: data.creditsCost,
15719
+ credits: data.creditsCost,
15720
+ duration_sec,
15721
+ fps: fps || 30,
15722
+ aspect_ratio: aspect_ratio || "9:16",
15723
+ quality: quality || "standard",
15724
+ project_id: project_id || null
15725
+ };
14961
15726
  return {
14962
15727
  content: [
14963
15728
  {
14964
15729
  type: "text",
14965
- text: [
15730
+ text: response_format === "json" ? JSON.stringify({ data: payload }) : [
14966
15731
  `Hyperframes render job queued.`,
14967
15732
  ` Job ID: ${data.jobId}`,
14968
15733
  ` Credits: ${data.creditsCost}`,
@@ -14979,7 +15744,7 @@ function registerHyperframesTools(server) {
14979
15744
  content: [
14980
15745
  {
14981
15746
  type: "text",
14982
- text: `Failed to queue Hyperframes render: ${err instanceof Error ? err.message : "Unknown error"}`
15747
+ text: `Failed to queue Hyperframes render: ${sanitizeError(err)}`
14983
15748
  }
14984
15749
  ],
14985
15750
  isError: true
@@ -14995,6 +15760,7 @@ var init_hyperframes = __esm({
14995
15760
  init_rate_limit();
14996
15761
  init_supabase();
14997
15762
  init_edge_function();
15763
+ init_sanitize_error();
14998
15764
  HYPERFRAMES_BLOCKS = [
14999
15765
  // Transitions
15000
15766
  {
@@ -15112,49 +15878,122 @@ import {
15112
15878
  import { z as z31 } from "zod";
15113
15879
  import fs from "node:fs/promises";
15114
15880
  import path from "node:path";
15881
+ import { fileURLToPath } from "node:url";
15115
15882
  function startOfCurrentWeekMonday() {
15116
15883
  const now = /* @__PURE__ */ new Date();
15117
- const day = now.getDay();
15884
+ const day = now.getUTCDay();
15118
15885
  const monday = new Date(now);
15119
- monday.setUTCDate(now.getUTCDate() - day + (day === 0 ? -6 : 1));
15886
+ monday.setUTCDate(now.getUTCDate() - (day + 6) % 7);
15887
+ monday.setUTCHours(0, 0, 0, 0);
15120
15888
  return monday.toISOString().split("T")[0];
15121
15889
  }
15890
+ function endOfWeek(startDate) {
15891
+ const end = /* @__PURE__ */ new Date(`${startDate}T00:00:00.000Z`);
15892
+ end.setUTCDate(end.getUTCDate() + 7);
15893
+ end.setUTCMilliseconds(end.getUTCMilliseconds() - 1);
15894
+ return end.toISOString();
15895
+ }
15896
+ function isStrictIsoDate(value) {
15897
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
15898
+ const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
15899
+ return Number.isFinite(parsed.getTime()) && parsed.toISOString().slice(0, 10) === value;
15900
+ }
15901
+ function publicRecentPost(value) {
15902
+ if (!value || typeof value !== "object") return null;
15903
+ const row = value;
15904
+ if (typeof row.id !== "string" || typeof row.platform !== "string" || typeof row.status !== "string" || typeof row.created_at !== "string") {
15905
+ return null;
15906
+ }
15907
+ const optional = (name) => typeof row[name] === "string" ? row[name] : null;
15908
+ return {
15909
+ id: row.id,
15910
+ platform: row.platform,
15911
+ status: row.status,
15912
+ title: optional("title"),
15913
+ external_post_id: optional("external_post_id"),
15914
+ published_at: optional("published_at"),
15915
+ scheduled_at: optional("scheduled_at"),
15916
+ created_at: row.created_at
15917
+ };
15918
+ }
15919
+ function calendarHtmlCandidates() {
15920
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
15921
+ return [
15922
+ // Bundled HTTP entry: import.meta.url is dist/http.js.
15923
+ path.join(moduleDir, "apps/content-calendar/mcp-app.html"),
15924
+ // Source/tsx/vitest: import.meta.url is src/apps/content-calendar.ts.
15925
+ path.resolve(moduleDir, "../../dist/apps/content-calendar/mcp-app.html"),
15926
+ // Local development fallback; never the sole package path.
15927
+ path.resolve(process.cwd(), "dist/apps/content-calendar/mcp-app.html")
15928
+ ];
15929
+ }
15930
+ async function readCalendarHtml() {
15931
+ for (const candidate of calendarHtmlCandidates()) {
15932
+ try {
15933
+ return await fs.readFile(candidate, "utf-8");
15934
+ } catch (error) {
15935
+ if (error.code !== "ENOENT") throw error;
15936
+ }
15937
+ }
15938
+ throw new Error("calendar_bundle_missing");
15939
+ }
15122
15940
  function registerContentCalendarApp(server) {
15123
15941
  registerAppTool(
15124
15942
  server,
15125
15943
  "open_content_calendar",
15126
15944
  {
15127
15945
  title: "Content Calendar",
15128
- description: "Open an interactive drag-drop calendar showing the user's scheduled posts for the current week. Users can reschedule via drag, filter by platform, drill into any post, or quick-create a new post. Backed by list_recent_posts, schedule_post, and find_next_slots \u2014 no new tools needed.",
15946
+ description: "Open a project-scoped interactive calendar for the current week. Users can filter, inspect, quick-create, suggest a slot, and reschedule pending posts with optimistic conflict protection.",
15129
15947
  inputSchema: {
15130
- start_date: z31.string().optional().describe(
15948
+ project_id: z31.string().uuid().optional().describe(
15949
+ "Brand/project ID. Defaults to the authenticated key's project or the account default."
15950
+ ),
15951
+ start_date: z31.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe(
15131
15952
  "ISO date for the week start (YYYY-MM-DD); defaults to the current week's Monday."
15132
15953
  )
15133
15954
  },
15134
15955
  outputSchema: {
15135
15956
  start_date: z31.string(),
15957
+ project_id: z31.string(),
15136
15958
  posts: z31.array(RecentPostOutputSchema),
15137
15959
  scopes: z31.array(z31.string())
15138
15960
  },
15139
15961
  _meta: {
15140
15962
  ui: {
15141
- resourceUri: CALENDAR_URI,
15142
- csp: {
15143
- "img-src": ["'self'", "https://*.r2.cloudflarestorage.com", "data:"],
15144
- "connect-src": ["'self'"]
15145
- }
15963
+ resourceUri: CALENDAR_URI
15146
15964
  }
15147
15965
  }
15148
15966
  },
15149
- async ({ start_date }, extra) => {
15150
- const userScopes = extra.authInfo?.scopes ?? [];
15967
+ async ({ project_id, start_date }) => {
15968
+ const userScopes = getRequestScopes() ?? getAuthenticatedScopes();
15969
+ const resolvedProjectId = project_id ?? await getDefaultProjectId();
15970
+ if (!resolvedProjectId) {
15971
+ return {
15972
+ content: [
15973
+ {
15974
+ type: "text",
15975
+ text: "No project_id was provided and no default project is configured."
15976
+ }
15977
+ ],
15978
+ isError: true
15979
+ };
15980
+ }
15151
15981
  const fromDate = start_date ?? startOfCurrentWeekMonday();
15982
+ if (!isStrictIsoDate(fromDate)) {
15983
+ return {
15984
+ content: [{ type: "text", text: "start_date must be a valid YYYY-MM-DD date." }],
15985
+ isError: true
15986
+ };
15987
+ }
15152
15988
  const { data: result, error } = await callEdgeFunction(
15153
15989
  "mcp-data",
15154
15990
  {
15155
- action: "recent-posts",
15156
- days: 14,
15157
- limit: 50
15991
+ action: "scheduled-posts",
15992
+ start_date: `${fromDate}T00:00:00.000Z`,
15993
+ end_date: endOfWeek(fromDate),
15994
+ statuses: ["pending", "scheduled", "draft"],
15995
+ projectId: resolvedProjectId,
15996
+ project_id: resolvedProjectId
15158
15997
  },
15159
15998
  { timeoutMs: 15e3 }
15160
15999
  );
@@ -15163,19 +16002,16 @@ function registerContentCalendarApp(server) {
15163
16002
  content: [
15164
16003
  {
15165
16004
  type: "text",
15166
- text: `Failed to load posts: ${error || result?.error || "Unknown error"}`
16005
+ text: "The content calendar could not load posts. Please retry."
15167
16006
  }
15168
16007
  ],
15169
16008
  isError: true
15170
16009
  };
15171
16010
  }
15172
- const posts = (result.posts ?? []).filter((p) => {
15173
- const ts = p.scheduled_at ?? p.published_at ?? p.created_at;
15174
- if (!ts) return false;
15175
- return ts.split("T")[0] >= fromDate;
15176
- });
16011
+ const posts = (result.posts ?? []).map(publicRecentPost).filter((post) => post !== null);
15177
16012
  const structuredContent = {
15178
16013
  start_date: fromDate,
16014
+ project_id: resolvedProjectId,
15179
16015
  posts,
15180
16016
  scopes: userScopes
15181
16017
  };
@@ -15194,37 +16030,60 @@ function registerContentCalendarApp(server) {
15194
16030
  server,
15195
16031
  CALENDAR_URI,
15196
16032
  CALENDAR_URI,
15197
- { mimeType: RESOURCE_MIME_TYPE },
16033
+ {
16034
+ mimeType: RESOURCE_MIME_TYPE,
16035
+ description: "Self-contained Social Neuron project content calendar.",
16036
+ _meta: { ui: { csp: CALENDAR_CSP } }
16037
+ },
15198
16038
  async () => {
15199
- const htmlPath = path.join(process.cwd(), "apps/content-calendar/dist/mcp-app.html");
15200
16039
  try {
15201
- const html = await fs.readFile(htmlPath, "utf-8");
16040
+ const html = await readCalendarHtml();
15202
16041
  return {
15203
- contents: [{ uri: CALENDAR_URI, mimeType: RESOURCE_MIME_TYPE, text: html }]
16042
+ contents: [
16043
+ {
16044
+ uri: CALENDAR_URI,
16045
+ mimeType: RESOURCE_MIME_TYPE,
16046
+ text: html,
16047
+ _meta: { ui: { csp: CALENDAR_CSP } }
16048
+ }
16049
+ ]
15204
16050
  };
15205
- } catch (err) {
16051
+ } catch {
15206
16052
  const errorHtml = `<!DOCTYPE html>
15207
16053
  <html><head><title>Content Calendar \u2014 unavailable</title></head>
15208
16054
  <body style="font-family:sans-serif;padding:24px;color:#444;">
15209
16055
  <h2>Content Calendar app bundle missing</h2>
15210
- <p>The server registered <code>open_content_calendar</code> but
15211
- <code>apps/content-calendar/dist/mcp-app.html</code> is not built.
15212
- Run <code>npm run build:app</code> in the mcp-server directory and redeploy.</p>
15213
- <p style="color:#999;font-size:12px;">${err.message}</p>
16056
+ <p>The interactive bundle is unavailable on this deployment. Please contact Social Neuron support.</p>
15214
16057
  </body></html>`;
15215
16058
  return {
15216
- contents: [{ uri: CALENDAR_URI, mimeType: RESOURCE_MIME_TYPE, text: errorHtml }]
16059
+ contents: [
16060
+ {
16061
+ uri: CALENDAR_URI,
16062
+ mimeType: RESOURCE_MIME_TYPE,
16063
+ text: errorHtml,
16064
+ _meta: { ui: { csp: CALENDAR_CSP } }
16065
+ }
16066
+ ]
15217
16067
  };
15218
16068
  }
15219
16069
  }
15220
16070
  );
15221
16071
  }
15222
- var CALENDAR_URI, RecentPostOutputSchema;
16072
+ var CALENDAR_URI, CALENDAR_CSP, RecentPostOutputSchema;
15223
16073
  var init_content_calendar = __esm({
15224
16074
  "src/apps/content-calendar.ts"() {
15225
16075
  "use strict";
15226
16076
  init_edge_function();
15227
- CALENDAR_URI = "ui://content-calendar/mcp-app.html";
16077
+ init_request_context();
16078
+ init_supabase();
16079
+ CALENDAR_URI = "ui://content-calendar/v1/mcp-app.html";
16080
+ CALENDAR_CSP = {
16081
+ // The HTML is fully self-contained. Tool calls travel over the host bridge,
16082
+ // not fetch/XHR, so the secure default is no network or remote resources.
16083
+ connectDomains: [],
16084
+ resourceDomains: [],
16085
+ frameDomains: []
16086
+ };
15228
16087
  RecentPostOutputSchema = z31.object({
15229
16088
  id: z31.string(),
15230
16089
  platform: z31.string(),
@@ -15238,8 +16097,257 @@ var init_content_calendar = __esm({
15238
16097
  }
15239
16098
  });
15240
16099
 
15241
- // src/tools/connections.ts
16100
+ // src/apps/analytics-pulse.ts
16101
+ import {
16102
+ registerAppResource as registerAppResource2,
16103
+ registerAppTool as registerAppTool2,
16104
+ RESOURCE_MIME_TYPE as RESOURCE_MIME_TYPE2
16105
+ } from "@modelcontextprotocol/ext-apps/server";
15242
16106
  import { z as z32 } from "zod";
16107
+ import fs2 from "node:fs/promises";
16108
+ import path2 from "node:path";
16109
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
16110
+ function finiteMetric(value) {
16111
+ const parsed = Number(value ?? 0);
16112
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
16113
+ }
16114
+ function publicAnalyticsPost(value) {
16115
+ if (!value || typeof value !== "object") return null;
16116
+ const row = value;
16117
+ const post = row.posts && typeof row.posts === "object" ? row.posts : {};
16118
+ const platform3 = typeof row.platform === "string" ? row.platform : typeof post.platform === "string" ? post.platform : null;
16119
+ const capturedAt = typeof row.captured_at === "string" ? row.captured_at : null;
16120
+ if (!platform3 || !capturedAt) return null;
16121
+ const views = finiteMetric(row.views);
16122
+ const likes = finiteMetric(row.likes);
16123
+ const comments = finiteMetric(row.comments);
16124
+ const shares = finiteMetric(row.shares);
16125
+ const engagement = likes + comments + shares;
16126
+ return {
16127
+ platform: platform3,
16128
+ title: typeof post.title === "string" ? post.title : null,
16129
+ views,
16130
+ likes,
16131
+ comments,
16132
+ shares,
16133
+ engagement_rate: views > 0 ? Number((engagement / views * 100).toFixed(2)) : 0,
16134
+ captured_at: capturedAt,
16135
+ published_at: typeof post.published_at === "string" ? post.published_at : null
16136
+ };
16137
+ }
16138
+ function latestAnalyticsRows(values) {
16139
+ const rows = values.filter((value) => Boolean(value) && typeof value === "object").sort((a, b) => String(b.captured_at ?? "").localeCompare(String(a.captured_at ?? "")));
16140
+ const seen = /* @__PURE__ */ new Set();
16141
+ const latest = [];
16142
+ for (const row of rows) {
16143
+ if (typeof row.post_id !== "string" || typeof row.platform !== "string") continue;
16144
+ const key = `${row.post_id}:${row.platform.toLowerCase()}`;
16145
+ if (seen.has(key)) continue;
16146
+ seen.add(key);
16147
+ latest.push(row);
16148
+ }
16149
+ return latest;
16150
+ }
16151
+ function analyticsHtmlCandidates() {
16152
+ const moduleDir = path2.dirname(fileURLToPath2(import.meta.url));
16153
+ return [
16154
+ path2.join(moduleDir, "apps/analytics-pulse/mcp-app.html"),
16155
+ path2.resolve(moduleDir, "../../dist/apps/analytics-pulse/mcp-app.html"),
16156
+ path2.resolve(process.cwd(), "dist/apps/analytics-pulse/mcp-app.html")
16157
+ ];
16158
+ }
16159
+ async function readAnalyticsHtml() {
16160
+ for (const candidate of analyticsHtmlCandidates()) {
16161
+ try {
16162
+ return await fs2.readFile(candidate, "utf-8");
16163
+ } catch (error) {
16164
+ if (error.code !== "ENOENT") throw error;
16165
+ }
16166
+ }
16167
+ throw new Error("analytics_bundle_missing");
16168
+ }
16169
+ function registerAnalyticsPulseApp(server) {
16170
+ registerAppTool2(
16171
+ server,
16172
+ "open_analytics_pulse",
16173
+ {
16174
+ title: "Analytics Pulse",
16175
+ description: "Open a project-scoped performance dashboard with views, engagement, platform mix, and top-post metrics. Use it to review results visually before planning the next content cycle.",
16176
+ inputSchema: {
16177
+ project_id: z32.string().uuid().optional().describe("Brand/project ID. Defaults to the authenticated key's project or account default."),
16178
+ platform: z32.enum(PLATFORM_VALUES).optional().describe("Optional platform filter."),
16179
+ days: z32.number().int().min(1).max(365).optional().describe("Lookback window. Defaults to 30 days.")
16180
+ },
16181
+ outputSchema: {
16182
+ project_id: z32.string(),
16183
+ platform: z32.string().nullable(),
16184
+ days: z32.number(),
16185
+ summary: z32.object({
16186
+ views: z32.number(),
16187
+ engagement: z32.number(),
16188
+ engagement_rate: z32.number(),
16189
+ posts: z32.number()
16190
+ }),
16191
+ platform_totals: z32.array(
16192
+ z32.object({
16193
+ platform: z32.string(),
16194
+ views: z32.number(),
16195
+ engagement: z32.number(),
16196
+ posts: z32.number()
16197
+ })
16198
+ ),
16199
+ posts: z32.array(AnalyticsPostOutputSchema)
16200
+ },
16201
+ _meta: { ui: { resourceUri: ANALYTICS_URI } }
16202
+ },
16203
+ async ({ project_id, platform: platform3, days }) => {
16204
+ const resolvedProjectId = project_id ?? await getDefaultProjectId();
16205
+ if (!resolvedProjectId) {
16206
+ return {
16207
+ content: [{ type: "text", text: "No project_id was provided and no default project is configured." }],
16208
+ isError: true
16209
+ };
16210
+ }
16211
+ const lookbackDays = days ?? 30;
16212
+ const { data: result, error } = await callEdgeFunction(
16213
+ "mcp-data",
16214
+ {
16215
+ action: "analytics",
16216
+ projectId: resolvedProjectId,
16217
+ project_id: resolvedProjectId,
16218
+ days: lookbackDays,
16219
+ // The table stores cumulative snapshots. Request a wider window and
16220
+ // deduplicate below so refresh frequency cannot inflate the totals.
16221
+ limit: 100,
16222
+ latestOnly: true,
16223
+ ...platform3 ? { platform: platform3 } : {}
16224
+ },
16225
+ { timeoutMs: 15e3 }
16226
+ );
16227
+ if (error || !result?.success) {
16228
+ return {
16229
+ content: [{ type: "text", text: "The analytics dashboard could not load data. Please retry." }],
16230
+ isError: true
16231
+ };
16232
+ }
16233
+ const posts = latestAnalyticsRows(result.rows ?? []).map(publicAnalyticsPost).filter((post) => post !== null).sort((a, b) => b.views - a.views);
16234
+ const totals = /* @__PURE__ */ new Map();
16235
+ let totalViews = 0;
16236
+ let totalEngagement = 0;
16237
+ for (const post of posts) {
16238
+ const engagement = post.likes + post.comments + post.shares;
16239
+ totalViews += post.views;
16240
+ totalEngagement += engagement;
16241
+ const aggregate = totals.get(post.platform) ?? {
16242
+ platform: post.platform,
16243
+ views: 0,
16244
+ engagement: 0,
16245
+ posts: 0
16246
+ };
16247
+ aggregate.views += post.views;
16248
+ aggregate.engagement += engagement;
16249
+ aggregate.posts += 1;
16250
+ totals.set(post.platform, aggregate);
16251
+ }
16252
+ const structuredContent = {
16253
+ project_id: resolvedProjectId,
16254
+ platform: platform3 ?? null,
16255
+ days: lookbackDays,
16256
+ summary: {
16257
+ views: totalViews,
16258
+ engagement: totalEngagement,
16259
+ engagement_rate: totalViews > 0 ? Number((totalEngagement / totalViews * 100).toFixed(2)) : 0,
16260
+ posts: posts.length
16261
+ },
16262
+ platform_totals: [...totals.values()].sort((a, b) => b.views - a.views),
16263
+ posts
16264
+ };
16265
+ return {
16266
+ structuredContent,
16267
+ content: [
16268
+ {
16269
+ type: "text",
16270
+ text: `Loaded ${posts.length} analytics record${posts.length === 1 ? "" : "s"} for the last ${lookbackDays} days.`
16271
+ }
16272
+ ]
16273
+ };
16274
+ }
16275
+ );
16276
+ registerAppResource2(
16277
+ server,
16278
+ ANALYTICS_URI,
16279
+ ANALYTICS_URI,
16280
+ {
16281
+ mimeType: RESOURCE_MIME_TYPE2,
16282
+ description: "Self-contained Social Neuron project analytics dashboard.",
16283
+ _meta: { ui: { csp: ANALYTICS_CSP } }
16284
+ },
16285
+ async () => {
16286
+ try {
16287
+ const html = await readAnalyticsHtml();
16288
+ return {
16289
+ contents: [
16290
+ {
16291
+ uri: ANALYTICS_URI,
16292
+ mimeType: RESOURCE_MIME_TYPE2,
16293
+ text: html,
16294
+ _meta: { ui: { csp: ANALYTICS_CSP } }
16295
+ }
16296
+ ]
16297
+ };
16298
+ } catch {
16299
+ return {
16300
+ contents: [
16301
+ {
16302
+ uri: ANALYTICS_URI,
16303
+ mimeType: RESOURCE_MIME_TYPE2,
16304
+ text: "<!doctype html><html><body><h2>Analytics Pulse unavailable</h2><p>The interactive bundle is unavailable on this deployment. Please contact Social Neuron support.</p></body></html>",
16305
+ _meta: { ui: { csp: ANALYTICS_CSP } }
16306
+ }
16307
+ ]
16308
+ };
16309
+ }
16310
+ }
16311
+ );
16312
+ }
16313
+ var ANALYTICS_URI, ANALYTICS_CSP, PLATFORM_VALUES, AnalyticsPostOutputSchema;
16314
+ var init_analytics_pulse = __esm({
16315
+ "src/apps/analytics-pulse.ts"() {
16316
+ "use strict";
16317
+ init_edge_function();
16318
+ init_supabase();
16319
+ ANALYTICS_URI = "ui://analytics-pulse/v1/mcp-app.html";
16320
+ ANALYTICS_CSP = {
16321
+ connectDomains: [],
16322
+ resourceDomains: [],
16323
+ frameDomains: []
16324
+ };
16325
+ PLATFORM_VALUES = [
16326
+ "youtube",
16327
+ "tiktok",
16328
+ "instagram",
16329
+ "twitter",
16330
+ "linkedin",
16331
+ "facebook",
16332
+ "threads",
16333
+ "bluesky"
16334
+ ];
16335
+ AnalyticsPostOutputSchema = z32.object({
16336
+ platform: z32.string(),
16337
+ title: z32.string().nullable(),
16338
+ views: z32.number(),
16339
+ likes: z32.number(),
16340
+ comments: z32.number(),
16341
+ shares: z32.number(),
16342
+ engagement_rate: z32.number(),
16343
+ captured_at: z32.string(),
16344
+ published_at: z32.string().nullable()
16345
+ });
16346
+ }
16347
+ });
16348
+
16349
+ // src/tools/connections.ts
16350
+ import { z as z33 } from "zod";
15243
16351
  function findActiveAccount(accounts, platform3) {
15244
16352
  const target = platform3.toLowerCase();
15245
16353
  return accounts.find((a) => {
@@ -15252,16 +16360,16 @@ function registerConnectionTools(server) {
15252
16360
  "start_platform_connection",
15253
16361
  "Begin connecting a social platform (Instagram, TikTok, YouTube, etc.). Returns a single-use deep link the user opens in a browser to complete the one-time OAuth handshake on socialneuron.com. This is NOT another OAuth in Claude \u2014 platform connections require a browser session because the social platforms (Meta, Google, TikTok) only accept callbacks on socialneuron.com. After the user clicks the link and approves on the platform, call `wait_for_connection` to detect completion. Link expires in 2 minutes; mint a new one if needed. Use `list_connected_accounts` first to check whether the platform is already connected before calling this.",
15254
16362
  {
15255
- platform: z32.enum(PLATFORM_ENUM4).describe("Platform to connect. Lower-case: instagram, tiktok, youtube, etc."),
15256
- project_id: z32.string().optional().describe(
16363
+ platform: z33.enum(PLATFORM_ENUM4).describe("Platform to connect. Lower-case: instagram, tiktok, youtube, etc."),
16364
+ project_id: z33.string().optional().describe(
15257
16365
  "Brand/project ID to bind the new social account to. Use this when connecting a brand-specific account."
15258
16366
  ),
15259
- response_format: z32.enum(["text", "json"]).optional().describe("Response format. Default: text.")
16367
+ response_format: z33.enum(["text", "json"]).optional().describe("Response format. Default: text.")
15260
16368
  },
15261
16369
  async ({ platform: platform3, project_id, response_format }) => {
15262
16370
  const format = response_format ?? "text";
15263
16371
  const userId = await getDefaultUserId();
15264
- const rl = checkRateLimit("read", `start_platform_connection:${userId}`);
16372
+ const rl = checkRateLimit("posting", `start_platform_connection:${userId}`);
15265
16373
  if (!rl.allowed) {
15266
16374
  return {
15267
16375
  content: [
@@ -15341,13 +16449,13 @@ function registerConnectionTools(server) {
15341
16449
  "wait_for_connection",
15342
16450
  "Poll until a platform connection becomes active. Use after `start_platform_connection` while the user completes the browser OAuth flow. Returns when the account row appears with status=active, or when the timeout elapses. Default timeout 120s, max 600s.",
15343
16451
  {
15344
- platform: z32.enum(PLATFORM_ENUM4).describe("Platform to wait for."),
15345
- project_id: z32.string().optional().describe(
16452
+ platform: z33.enum(PLATFORM_ENUM4).describe("Platform to wait for."),
16453
+ project_id: z33.string().optional().describe(
15346
16454
  "Brand/project ID to scope the connection poll. Use the same project_id passed to start_platform_connection."
15347
16455
  ),
15348
- timeout_s: z32.number().min(5).max(600).optional().describe("How long to wait, in seconds. Default 120."),
15349
- poll_interval_s: z32.number().min(2).max(30).optional().describe("Poll interval in seconds. Default 5."),
15350
- response_format: z32.enum(["text", "json"]).optional().describe("Response format. Default: text.")
16456
+ timeout_s: z33.number().min(5).max(600).optional().describe("How long to wait, in seconds. Default 120."),
16457
+ poll_interval_s: z33.number().min(2).max(30).optional().describe("Poll interval in seconds. Default 5."),
16458
+ response_format: z33.enum(["text", "json"]).optional().describe("Response format. Default: text.")
15351
16459
  },
15352
16460
  async ({ platform: platform3, project_id, timeout_s, poll_interval_s, response_format }) => {
15353
16461
  const format = response_format ?? "text";
@@ -15503,7 +16611,7 @@ var init_connections = __esm({
15503
16611
  });
15504
16612
 
15505
16613
  // src/tools/harness.ts
15506
- import { z as z33 } from "zod";
16614
+ import { z as z34 } from "zod";
15507
16615
  function registerHarnessTools(server, _ctx) {
15508
16616
  server.tool(
15509
16617
  "write_agent_reflection",
@@ -15608,38 +16716,38 @@ var init_harness = __esm({
15608
16716
  "engager"
15609
16717
  ];
15610
16718
  writeReflectionSchema = {
15611
- reflection_text: z33.string().min(1).max(4e3).describe("The verbal reflection text produced by the agent. 1\u20134000 characters."),
15612
- generated_by_agent: z33.enum(ALLOWED_AGENTS).describe(
16719
+ reflection_text: z34.string().min(1).max(4e3).describe("The verbal reflection text produced by the agent. 1\u20134000 characters."),
16720
+ generated_by_agent: z34.enum(ALLOWED_AGENTS).describe(
15613
16721
  "Which agent produced this reflection. Must be one of: conductor, brand-brain, drafter, publisher, analyst, engager."
15614
16722
  ),
15615
- provenance: z33.object({
15616
- content_history_id: z33.string().optional().describe("Related content_history row UUID."),
15617
- outcome_event_id: z33.string().optional().describe("Related outcome event UUID."),
15618
- prm_score_ids: z33.array(z33.string()).optional().describe("PRM score IDs that informed this reflection."),
15619
- handoff_ids: z33.array(z33.string()).optional().describe("Handoff event IDs that triggered this reflection.")
16723
+ provenance: z34.object({
16724
+ content_history_id: z34.string().optional().describe("Related content_history row UUID."),
16725
+ outcome_event_id: z34.string().optional().describe("Related outcome event UUID."),
16726
+ prm_score_ids: z34.array(z34.string()).optional().describe("PRM score IDs that informed this reflection."),
16727
+ handoff_ids: z34.array(z34.string()).optional().describe("Handoff event IDs that triggered this reflection.")
15620
16728
  }).strict().describe(
15621
16729
  "Source evidence for this reflection. Only these four keys are accepted \u2014 unknown keys are rejected to prevent spurious provenance claims."
15622
16730
  ),
15623
- brand_id: z33.string().describe("Brand profile UUID this reflection belongs to."),
15624
- pipeline_id: z33.string().optional().describe("Optional: pipeline run UUID."),
15625
- post_id: z33.string().optional().describe("Optional: post UUID if reflection targets a post.")
16731
+ brand_id: z34.string().describe("Brand profile UUID this reflection belongs to."),
16732
+ pipeline_id: z34.string().optional().describe("Optional: pipeline run UUID."),
16733
+ post_id: z34.string().optional().describe("Optional: post UUID if reflection targets a post.")
15626
16734
  };
15627
16735
  readReflectionSchema = {
15628
- brand_id: z33.string().describe("Brand profile UUID to read reflections for."),
15629
- generated_by_agent: z33.enum(ALLOWED_AGENTS).optional().describe(
16736
+ brand_id: z34.string().describe("Brand profile UUID to read reflections for."),
16737
+ generated_by_agent: z34.enum(ALLOWED_AGENTS).optional().describe(
15630
16738
  "Optional filter: only return reflections produced by this agent. One of: conductor, brand-brain, drafter, publisher, analyst, engager."
15631
16739
  ),
15632
- limit: z33.number().int().min(1).max(100).optional().describe(
16740
+ limit: z34.number().int().min(1).max(100).optional().describe(
15633
16741
  "Maximum number of reflections to return. Clamped to [1, 100]. Default: 50. Results are ordered by created_at DESC, then id ASC (deterministic tiebreak)."
15634
16742
  )
15635
16743
  };
15636
16744
  recordOutcomeSchema = {
15637
- decision_event_id: z33.string().describe("UUID of the decision_events row to record an outcome for."),
15638
- horizon: z33.enum(["1h", "6h", "24h"]).describe(
16745
+ decision_event_id: z34.string().describe("UUID of the decision_events row to record an outcome for."),
16746
+ horizon: z34.enum(["1h", "6h", "24h"]).describe(
15639
16747
  "Observation horizon. Only horizon=24h with a non-null reward triggers a learning-loop update; 1h/6h are stored but inert for learning."
15640
16748
  ),
15641
- reward: z33.number().min(0).max(1).describe("Normalised reward signal in [0, 1]. Higher = better outcome."),
15642
- outcome_metrics: z33.record(z33.string(), z33.number()).optional().describe(
16749
+ reward: z34.number().min(0).max(1).describe("Normalised reward signal in [0, 1]. Higher = better outcome."),
16750
+ outcome_metrics: z34.record(z34.string(), z34.number()).optional().describe(
15643
16751
  'Optional map of raw metric name \u2192 value (e.g. {"likes": 42, "reach": 1200}). Stored verbatim; not used for learning directly.'
15644
16752
  )
15645
16753
  };
@@ -15647,7 +16755,7 @@ var init_harness = __esm({
15647
16755
  });
15648
16756
 
15649
16757
  // src/tools/hermes.ts
15650
- import { z as z34 } from "zod";
16758
+ import { z as z35 } from "zod";
15651
16759
  function asEnvelope25(data) {
15652
16760
  return {
15653
16761
  _meta: {
@@ -15663,12 +16771,12 @@ function registerHermesTools(server) {
15663
16771
  "Save a draft post to the SN content library. Use when an autonomous agent wants to persist a draft for review before publishing. Lands in the content library with status='draft'. The draft can then be approved/edited in the SN UI.",
15664
16772
  {
15665
16773
  platform: PLATFORM.describe("Target platform for the draft."),
15666
- copy: z34.string().min(1).max(8e3).describe("The draft post body."),
15667
- project_id: z34.string().optional().describe("SN project UUID. Optional but recommended."),
15668
- media_url: z34.string().url().optional().describe("Optional cover/media URL (R2 signed URL)."),
15669
- hermes_run_id: z34.string().optional().describe("Agent run id, for traceability."),
15670
- source_intel_ids: z34.array(z34.string()).optional().describe("Optional list of intel_signals.id rows that informed this draft."),
15671
- response_format: z34.enum(["text", "json"]).optional()
16774
+ copy: z35.string().min(1).max(8e3).describe("The draft post body."),
16775
+ project_id: z35.string().optional().describe("SN project UUID. Optional but recommended."),
16776
+ media_url: z35.string().url().optional().describe("Optional cover/media URL (R2 signed URL)."),
16777
+ hermes_run_id: z35.string().optional().describe("Agent run id, for traceability."),
16778
+ source_intel_ids: z35.array(z35.string()).optional().describe("Optional list of intel_signals.id rows that informed this draft."),
16779
+ response_format: z35.enum(["text", "json"]).optional()
15672
16780
  },
15673
16781
  async ({ platform: platform3, copy, project_id, media_url, hermes_run_id, response_format }) => {
15674
16782
  const { data, error } = await callEdgeFunction(
@@ -15706,15 +16814,15 @@ function registerHermesTools(server) {
15706
16814
  "record_voice_lesson",
15707
16815
  'Persist a learned voice lesson to brand_profiles.platform_voice.voice_lessons. Use after weekly reflection identifies a hook/format/CTA pattern that beats median engagement by \u226530%. Appears in SN Brand > BrandBrainPreview > "Voice lessons (auto)".',
15708
16816
  {
15709
- project_id: z34.string().describe("SN project UUID."),
15710
- lesson: z34.string().min(1).max(500).describe('One-sentence rule (e.g. "lowercase IG hooks beat title-case").'),
15711
- evidence: z34.object({
15712
- engagement_lift_pct: z34.number().describe("Engagement lift vs baseline, in percentage points."),
15713
- sample_size: z34.number().int().min(1).describe("Number of posts behind this lesson."),
15714
- top_examples: z34.array(z34.string()).optional().describe("Up to 3 hook examples from the top quartile.")
16817
+ project_id: z35.string().describe("SN project UUID."),
16818
+ lesson: z35.string().min(1).max(500).describe('One-sentence rule (e.g. "lowercase IG hooks beat title-case").'),
16819
+ evidence: z35.object({
16820
+ engagement_lift_pct: z35.number().describe("Engagement lift vs baseline, in percentage points."),
16821
+ sample_size: z35.number().int().min(1).describe("Number of posts behind this lesson."),
16822
+ top_examples: z35.array(z35.string()).optional().describe("Up to 3 hook examples from the top quartile.")
15715
16823
  }).describe("Quantitative evidence backing the lesson."),
15716
- applies_to: z34.array(PLATFORM).min(1).describe("Platforms this lesson applies to."),
15717
- response_format: z34.enum(["text", "json"]).optional()
16824
+ applies_to: z35.array(PLATFORM).min(1).describe("Platforms this lesson applies to."),
16825
+ response_format: z35.enum(["text", "json"]).optional()
15718
16826
  },
15719
16827
  async ({ project_id, lesson, evidence, applies_to, response_format }) => {
15720
16828
  const { data, error } = await callEdgeFunction("mcp-data", {
@@ -15750,10 +16858,10 @@ function registerHermesTools(server) {
15750
16858
  "record_observation",
15751
16859
  'Record an agent observation (e.g. "topic X engagement up 23% this week"). Surfaces in the analytics Playbook. Use for weekly reflection digests and mid-campaign pulse summaries.',
15752
16860
  {
15753
- summary: z34.string().min(1).max(2e3).describe("One-paragraph summary, shown to the account owner."),
15754
- deltas: z34.record(z34.string(), z34.union([z34.number(), z34.string(), z34.boolean()])).optional().describe("Optional structured key/value payload (e.g. {topic_x_er_pct: 23})."),
15755
- run_id: z34.string().optional().describe("Agent run id for traceability."),
15756
- response_format: z34.enum(["text", "json"]).optional()
16861
+ summary: z35.string().min(1).max(2e3).describe("One-paragraph summary, shown to the account owner."),
16862
+ deltas: z35.record(z35.string(), z35.union([z35.number(), z35.string(), z35.boolean()])).optional().describe("Optional structured key/value payload (e.g. {topic_x_er_pct: 23})."),
16863
+ run_id: z35.string().optional().describe("Agent run id for traceability."),
16864
+ response_format: z35.enum(["text", "json"]).optional()
15757
16865
  },
15758
16866
  async ({ summary, deltas, run_id, response_format }) => {
15759
16867
  const { data, error } = await callEdgeFunction("mcp-data", { action: "record-observation", summary, deltas, run_id });
@@ -15780,13 +16888,13 @@ function registerHermesTools(server) {
15780
16888
  "record_intel_signal",
15781
16889
  "Record a research/trend signal (news, HN post, competitor change, arxiv paper). Surfaces in SN Brand > Niche Intelligence. Dedupes by URL per user \u2014 safe to call multiple times for the same source.",
15782
16890
  {
15783
- source: z34.string().min(1).max(100).describe('Watcher name (e.g. "news-watch", "hackernews-watch").'),
15784
- url: z34.string().url().max(2e3).describe("Canonical URL of the source. Used for dedupe."),
15785
- topic: z34.string().max(200).optional().describe("Best-fit topic key."),
15786
- title: z34.string().max(500).optional(),
15787
- summary: z34.string().max(4e3).optional().describe("One-paragraph summary of the signal."),
15788
- score: z34.number().optional().describe("Relevance score from the watcher (0\u201310 typical)."),
15789
- response_format: z34.enum(["text", "json"]).optional()
16891
+ source: z35.string().min(1).max(100).describe('Watcher name (e.g. "news-watch", "hackernews-watch").'),
16892
+ url: z35.string().url().max(2e3).describe("Canonical URL of the source. Used for dedupe."),
16893
+ topic: z35.string().max(200).optional().describe("Best-fit topic key."),
16894
+ title: z35.string().max(500).optional(),
16895
+ summary: z35.string().max(4e3).optional().describe("One-paragraph summary of the signal."),
16896
+ score: z35.number().optional().describe("Relevance score from the watcher (0\u201310 typical)."),
16897
+ response_format: z35.enum(["text", "json"]).optional()
15790
16898
  },
15791
16899
  async ({ source, url, topic, title, summary, score, response_format }) => {
15792
16900
  const { data, error } = await callEdgeFunction("mcp-data", {
@@ -15824,16 +16932,16 @@ function registerHermesTools(server) {
15824
16932
  "record_campaign_spend",
15825
16933
  "Log a campaign cost line. Use when an autonomous agent incurs spend that should be attributed to a campaign \u2014 drafts, renders, paid amp. Read aggregate via get_active_campaigns.",
15826
16934
  {
15827
- campaign_id: z34.string().min(1).max(200).describe("Campaign identifier (slug)."),
15828
- category: z34.enum([
16935
+ campaign_id: z35.string().min(1).max(200).describe("Campaign identifier (slug)."),
16936
+ category: z35.enum([
15829
16937
  "hermes_drafts",
15830
16938
  "carousel_renders",
15831
16939
  "analytics_pulls",
15832
16940
  "paid_amplification",
15833
16941
  "other"
15834
16942
  ]).describe("Cost category."),
15835
- amount_usd: z34.number().min(0).describe("Amount in USD; supports 4 decimal places."),
15836
- response_format: z34.enum(["text", "json"]).optional()
16943
+ amount_usd: z35.number().min(0).describe("Amount in USD; supports 4 decimal places."),
16944
+ response_format: z35.enum(["text", "json"]).optional()
15837
16945
  },
15838
16946
  async ({ campaign_id, category, amount_usd, response_format }) => {
15839
16947
  const { data, error } = await callEdgeFunction("mcp-data", {
@@ -15868,7 +16976,7 @@ function registerHermesTools(server) {
15868
16976
  "get_active_campaigns",
15869
16977
  "List currently-running campaigns with thesis, budget, hero format, and current spend. Use to bias drafts toward active campaign themes.",
15870
16978
  {
15871
- response_format: z34.enum(["text", "json"]).optional()
16979
+ response_format: z35.enum(["text", "json"]).optional()
15872
16980
  },
15873
16981
  async ({ response_format }) => {
15874
16982
  const { data, error } = await callEdgeFunction("mcp-data", { action: "get-active-campaigns" });
@@ -15917,7 +17025,7 @@ var init_hermes = __esm({
15917
17025
  "use strict";
15918
17026
  init_edge_function();
15919
17027
  init_version();
15920
- PLATFORM = z34.enum([
17028
+ PLATFORM = z35.enum([
15921
17029
  "instagram",
15922
17030
  "twitter",
15923
17031
  "linkedin",
@@ -15963,7 +17071,20 @@ var init_skills_manifest = __esm({
15963
17071
  });
15964
17072
 
15965
17073
  // src/tools/skills.ts
15966
- import { z as z35 } from "zod";
17074
+ import { z as z36 } from "zod";
17075
+ function renderCatalogRow(row) {
17076
+ const lines = [];
17077
+ const platform3 = row.platform ? ` \xB7 ${row.platform}` : "";
17078
+ lines.push(`${row.slug} (${row.kind}${platform3})`);
17079
+ const desc = row.frontmatter && typeof row.frontmatter.description === "string" ? row.frontmatter.description : null;
17080
+ if (desc) lines.push(` ${desc}`);
17081
+ const lock = row.locked ? ` \u{1F512} (upgrade to unlock)` : "";
17082
+ lines.push(` Tier: ${row.tier_minimum}${lock}`);
17083
+ lines.push(
17084
+ ` Model: ${row.model_id ?? "n/a"} \xB7 ${row.body_chars} chars` + (row.updated_at ? ` \xB7 updated ${row.updated_at}` : "")
17085
+ );
17086
+ return lines.join("\n");
17087
+ }
15967
17088
  function asEnvelope26(data) {
15968
17089
  return {
15969
17090
  _meta: {
@@ -15991,19 +17112,88 @@ function registerSkillsTools(server) {
15991
17112
  "list_skills",
15992
17113
  'List Social Neuron content workflow skills available to the authenticated user. A skill is a brand-locked multi-step pipeline (research \u2192 hook \u2192 script \u2192 visuals \u2192 voice \u2192 captions \u2192 assembly \u2192 quality gate \u2192 schedule) inspired by documented viral patterns (MrBeast 3-second hook, Hormozi pattern interrupt, etc.). Use this tool when the user asks "what can SN do", "what skills are available", "show me viral templates", or before calling run_skill so you can pick the right one.',
15993
17114
  {
15994
- studio: z35.enum(STUDIO_VALUES).optional().describe("Filter to one studio (video, avatar, carousel, voice, caption, edit)."),
15995
- featured_only: z35.boolean().optional().describe("Return only featured (recommended) skills. Defaults to false."),
15996
- response_format: z35.enum(["text", "json"]).optional().describe("Response format. Defaults to text \u2014 human-readable summary.")
17115
+ studio: z36.enum(STUDIO_VALUES).optional().describe(
17116
+ "Filter to one studio (video, avatar, carousel, voice, caption, edit)."
17117
+ ),
17118
+ featured_only: z36.boolean().optional().describe(
17119
+ "Return only featured (recommended) skills. Defaults to false."
17120
+ ),
17121
+ response_format: z36.enum(["text", "json"]).optional().describe(
17122
+ "Response format. Defaults to text \u2014 human-readable summary."
17123
+ )
15997
17124
  },
15998
17125
  async ({ studio, featured_only, response_format }) => {
15999
- const skills = listSkills({ studio, featuredOnly: featured_only });
16000
17126
  const format = response_format ?? "text";
17127
+ const { data: efData, error: efError } = await callEdgeFunction("mcp-data", { action: "get-skills" });
17128
+ const catalogRows = efData?.skills;
17129
+ if (!efError && Array.isArray(catalogRows) && catalogRows.length > 0) {
17130
+ const workflows = listSkills({ studio, featuredOnly: featured_only });
17131
+ const filteredGuideRows = catalogRows.filter(
17132
+ (row) => !studio && (!featured_only || row.frontmatter?.featured === true)
17133
+ );
17134
+ const guides = filteredGuideRows.map((row) => ({
17135
+ ...row,
17136
+ use_with: "get_skill"
17137
+ }));
17138
+ const total = guides.length + workflows.length;
17139
+ if (format === "json") {
17140
+ return {
17141
+ content: [
17142
+ {
17143
+ type: "text",
17144
+ text: JSON.stringify(
17145
+ asEnvelope26({
17146
+ count: total,
17147
+ guides,
17148
+ workflows: workflows.map((w) => ({
17149
+ ...w,
17150
+ use_with: "run_skill"
17151
+ }))
17152
+ }),
17153
+ null,
17154
+ 2
17155
+ )
17156
+ }
17157
+ ]
17158
+ };
17159
+ }
17160
+ const guideBlocks = filteredGuideRows.map(
17161
+ (row) => `${renderCatalogRow(row)}
17162
+ \u2192 Read it: get_skill(slug: "${row.slug}")`
17163
+ ).join("\n\n");
17164
+ const workflowBlocks = workflows.map(renderSkillSummary).join("\n\n");
17165
+ const header2 = `${total} skill${total === 1 ? "" : "s"} available (${guides.length} guide${guides.length === 1 ? "" : "s"} \xB7 ${workflows.length} workflow${workflows.length === 1 ? "" : "s"})
17166
+ ${"=".repeat(40)}`;
17167
+ const sections = [
17168
+ guides.length > 0 ? `GUIDES \u2014 living how-to documents. Fetch with get_skill(slug).
17169
+
17170
+ ${guideBlocks}` : "",
17171
+ workflows.length > 0 ? `WORKFLOWS \u2014 executable content pipelines. Launch with run_skill(skill_id).
17172
+
17173
+ ${workflowBlocks}` : ""
17174
+ ].filter(Boolean);
17175
+ return {
17176
+ content: [
17177
+ {
17178
+ type: "text",
17179
+ text: `${header2}
17180
+
17181
+ ${sections.join("\n\n")}`
17182
+ }
17183
+ ]
17184
+ };
17185
+ }
17186
+ const skills = listSkills({ studio, featuredOnly: featured_only });
16001
17187
  if (format === "json") {
16002
17188
  return {
16003
17189
  content: [
16004
17190
  {
16005
17191
  type: "text",
16006
- text: JSON.stringify(asEnvelope26({ count: skills.length, skills }), null, 2)
17192
+ text: JSON.stringify(
17193
+ asEnvelope26({ count: skills.length, skills }),
17194
+ null,
17195
+ 2
17196
+ )
16007
17197
  }
16008
17198
  ]
16009
17199
  };
@@ -16013,7 +17203,9 @@ function registerSkillsTools(server) {
16013
17203
  content: [
16014
17204
  {
16015
17205
  type: "text",
16016
- text: "No skills match the filter." + (studio ? ` Studio "${studio}" has no skills yet.` : "") + "\n\nAvailable studios with skills: " + Array.from(new Set(SKILLS_MANIFEST.map((s) => s.studio))).join(", ")
17206
+ text: "No skills match the filter." + (studio ? ` Studio "${studio}" has no skills yet.` : "") + "\n\nAvailable studios with skills: " + Array.from(new Set(SKILLS_MANIFEST.map((s) => s.studio))).join(
17207
+ ", "
17208
+ )
16017
17209
  }
16018
17210
  ]
16019
17211
  };
@@ -16032,18 +17224,22 @@ ${blocks}` }]
16032
17224
  "run_skill",
16033
17225
  "Run a Social Neuron workflow skill end-to-end (brand-locked content production). Returns a structured run preview with the exact step plan, credit cost, and a deep-link to launch the run in the SN dashboard. A future release executes in-process so you can stream step-by-step progress back to the user. Call list_skills first to discover available skill ids.",
16034
17226
  {
16035
- skill_id: z35.string().describe(
17227
+ skill_id: z36.string().describe(
16036
17228
  'The id of the skill to run (e.g. "skill-brand-locked-viral-hook-reel"). Use list_skills to discover available ids.'
16037
17229
  ),
16038
- topic: z35.string().min(1).describe('What the content is about (e.g. "why we built Social Neuron").'),
16039
- audience: z35.string().optional().describe(
17230
+ topic: z36.string().min(1).describe(
17231
+ 'What the content is about (e.g. "why we built Social Neuron").'
17232
+ ),
17233
+ audience: z36.string().optional().describe(
16040
17234
  'Who the content is for (e.g. "first-time founders"). Defaults to brand persona.'
16041
17235
  ),
16042
- hook: z35.string().optional().describe(
17236
+ hook: z36.string().optional().describe(
16043
17237
  "Optional explicit hook. If omitted, the skill drafts and scores 3-5 candidates."
16044
17238
  ),
16045
- cta: z35.string().optional().describe("Optional call-to-action override. Defaults to brand standard CTA."),
16046
- response_format: z35.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
17239
+ cta: z36.string().optional().describe(
17240
+ "Optional call-to-action override. Defaults to brand standard CTA."
17241
+ ),
17242
+ response_format: z36.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
16047
17243
  },
16048
17244
  async ({ skill_id, topic, audience, hook, cta, response_format }) => {
16049
17245
  const skill = getSkill(skill_id);
@@ -16111,19 +17307,96 @@ ${blocks}` }]
16111
17307
  };
16112
17308
  }
16113
17309
  );
17310
+ server.tool(
17311
+ "get_skill",
17312
+ `Fetch the full body of a single Social Neuron skill by slug \u2014 the hand-maintained strategy/specs plus the machine-maintained "what's working now" compiled section. Use this after list_skills when the user wants the actual playbook for a platform (e.g. "show me the TikTok skill"). Returns the skill body, compiled section, tier, and linked recipe slug (if any).`,
17313
+ {
17314
+ slug: z36.string().min(1).max(100).regex(/^[a-z0-9][a-z0-9-]*$/).describe(
17315
+ 'The skill slug (e.g. "tiktok-content"). Use list_skills to discover slugs.'
17316
+ ),
17317
+ response_format: z36.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
17318
+ },
17319
+ async ({ slug, response_format }) => {
17320
+ const format = response_format ?? "text";
17321
+ const { data, error } = await callEdgeFunction("mcp-data", {
17322
+ action: "get-skill",
17323
+ slug
17324
+ });
17325
+ if (error) {
17326
+ return toolError(
17327
+ "upstream_error",
17328
+ "The skill catalogue could not be loaded. Please retry."
17329
+ );
17330
+ }
17331
+ const skill = data?.skill;
17332
+ if (!skill) {
17333
+ return toolError("not_found", `No skill found with slug "${slug}".`, {
17334
+ recover_with: ["Call list_skills to see available skills."]
17335
+ });
17336
+ }
17337
+ if (skill.locked) {
17338
+ return toolError(
17339
+ "permission_denied",
17340
+ "This skill requires a higher plan tier.",
17341
+ {
17342
+ details: { slug: skill.slug, tier_minimum: skill.tier_minimum },
17343
+ recover_with: [
17344
+ "Upgrade the account or choose an unlocked skill from list_skills."
17345
+ ]
17346
+ }
17347
+ );
17348
+ }
17349
+ if (format === "json") {
17350
+ return {
17351
+ content: [
17352
+ {
17353
+ type: "text",
17354
+ text: JSON.stringify(asEnvelope26(skill), null, 2)
17355
+ }
17356
+ ]
17357
+ };
17358
+ }
17359
+ const platform3 = skill.platform ? ` \xB7 ${skill.platform}` : "";
17360
+ const lock = skill.locked ? " \u{1F512} (upgrade to unlock)" : "";
17361
+ const header = [
17362
+ `${skill.slug} (${skill.kind}${platform3})`,
17363
+ "=".repeat(40),
17364
+ `Tier: ${skill.tier_minimum}${lock} \xB7 version ${skill.version}` + (skill.recipe_slug ? ` \xB7 recipe: ${skill.recipe_slug}` : ""),
17365
+ ""
17366
+ ].join("\n");
17367
+ const compiled = skill.compiled_section ? `
17368
+
17369
+ --- What's working now ---
17370
+ ${skill.compiled_section}` : "";
17371
+ return {
17372
+ content: [
17373
+ { type: "text", text: `${header}${skill.body}${compiled}` }
17374
+ ]
17375
+ };
17376
+ }
17377
+ );
16114
17378
  }
16115
17379
  var STUDIO_VALUES;
16116
17380
  var init_skills = __esm({
16117
17381
  "src/tools/skills.ts"() {
16118
17382
  "use strict";
16119
17383
  init_version();
17384
+ init_tool_error();
17385
+ init_edge_function();
16120
17386
  init_skills_manifest();
16121
- STUDIO_VALUES = ["video", "avatar", "carousel", "voice", "caption", "edit"];
17387
+ STUDIO_VALUES = [
17388
+ "video",
17389
+ "avatar",
17390
+ "carousel",
17391
+ "voice",
17392
+ "caption",
17393
+ "edit"
17394
+ ];
16122
17395
  }
16123
17396
  });
16124
17397
 
16125
17398
  // src/tools/loopPulse.ts
16126
- import { z as z36 } from "zod";
17399
+ import { z as z37 } from "zod";
16127
17400
  function asEnvelope27(data) {
16128
17401
  return {
16129
17402
  _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
@@ -16135,7 +17408,7 @@ function registerLoopPulseTools(server) {
16135
17408
  "get_loop_pulse",
16136
17409
  'Read the dynamic loop-health KPIs for the Social Neuron growth loop over the last 7 days. Returns reflection coverage, decision coverage, visual gate pass rate, learning-update application rate, per-platform learning uptake, autopilot lag, and pattern aggregation counts \u2014 each with a status ("ok" / "warn" / "bad") and a why-line explaining what the metric measures. Use this to decide whether the loop is closing or where it is stuck before recommending next moves.',
16137
17410
  {
16138
- response_format: z36.enum(["text", "json"]).optional().describe("Output format. Defaults to text.")
17411
+ response_format: z37.enum(["text", "json"]).optional().describe("Output format. Defaults to text.")
16139
17412
  },
16140
17413
  async ({ response_format }) => {
16141
17414
  const format = response_format ?? "text";
@@ -16185,7 +17458,7 @@ var init_loopPulse = __esm({
16185
17458
  });
16186
17459
 
16187
17460
  // src/tools/banditState.ts
16188
- import { z as z37 } from "zod";
17461
+ import { z as z38 } from "zod";
16189
17462
  function asEnvelope28(data) {
16190
17463
  return {
16191
17464
  _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
@@ -16197,8 +17470,8 @@ function registerBanditStateTools(server) {
16197
17470
  "get_bandit_state",
16198
17471
  "Read the current content learning state for a project. Returns top-K arms per (arm_type, platform) with expected performance and uncertainty. Use this to reason about which hook family / format / timing slot currently performs best on each platform before recommending next moves. Arm types: hook_family, length_bucket (xs/s/m/l/xl by platform), posting_time_bucket (morning/midday/evening/late), content_format (video/carousel/image/caption/text/avatar/storyboard).",
16199
17472
  {
16200
- project_id: z37.string().uuid().optional().describe("Project UUID. Defaults to the authenticated user default project."),
16201
- platform: z37.enum([
17473
+ project_id: z38.string().uuid().optional().describe("Project UUID. Defaults to the authenticated user default project."),
17474
+ platform: z38.enum([
16202
17475
  "instagram",
16203
17476
  "tiktok",
16204
17477
  "youtube",
@@ -16210,7 +17483,7 @@ function registerBanditStateTools(server) {
16210
17483
  ]).optional().describe(
16211
17484
  "Lowercase platform name. Omit to return both platform-scoped and legacy platform-agnostic arms."
16212
17485
  ),
16213
- arm_type: z37.enum([
17486
+ arm_type: z38.enum([
16214
17487
  "hook_family",
16215
17488
  "hook_type",
16216
17489
  "format",
@@ -16224,8 +17497,8 @@ function registerBanditStateTools(server) {
16224
17497
  "posting_time_bucket",
16225
17498
  "content_format"
16226
17499
  ]).optional().describe("Arm dimension. Omit to return all types grouped."),
16227
- top_k: z37.number().min(1).max(50).optional().describe("Max arms per (arm_type) group. Default 5."),
16228
- response_format: z37.enum(["text", "json"]).optional().describe("Output format. Defaults to text.")
17500
+ top_k: z38.number().min(1).max(50).optional().describe("Max arms per (arm_type) group. Default 5."),
17501
+ response_format: z38.enum(["text", "json"]).optional().describe("Output format. Defaults to text.")
16229
17502
  },
16230
17503
  async ({ project_id, platform: platform3, arm_type, top_k, response_format }) => {
16231
17504
  const format = response_format ?? "text";
@@ -16288,12 +17561,134 @@ var init_banditState = __esm({
16288
17561
  }
16289
17562
  });
16290
17563
 
17564
+ // src/tools/lifecycle.ts
17565
+ import { z as z39 } from "zod";
17566
+ function envelope(data) {
17567
+ return {
17568
+ _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
17569
+ data
17570
+ };
17571
+ }
17572
+ function lifecycleFailure(error) {
17573
+ if (/not_found|not found/i.test(error)) {
17574
+ return toolError("not_found", "The requested object was not found in this project.");
17575
+ }
17576
+ if (/not_cancellable|publishing_in_progress|schedule_conflict|post_in_progress/i.test(error)) {
17577
+ return toolError("validation_error", "The object can no longer be cancelled in its current state.", {
17578
+ recover_with: ["Refresh its status before deciding the next action."]
17579
+ });
17580
+ }
17581
+ return toolError("upstream_error", "The lifecycle operation could not be completed. Please retry.");
17582
+ }
17583
+ async function projectContext(projectId) {
17584
+ return projectId ?? await getDefaultProjectId();
17585
+ }
17586
+ async function invokeLifecycle(action, projectId, identifiers) {
17587
+ const resolvedProjectId = await projectContext(projectId);
17588
+ if (!resolvedProjectId) {
17589
+ return toolError("validation_error", "A project_id is required because no default project is configured.");
17590
+ }
17591
+ const { data, error } = await callEdgeFunction("mcp-data", {
17592
+ action,
17593
+ projectId: resolvedProjectId,
17594
+ project_id: resolvedProjectId,
17595
+ ...identifiers
17596
+ });
17597
+ if (error) return lifecycleFailure(error);
17598
+ if (!data?.success) return toolError("upstream_error", "The lifecycle operation returned no result.");
17599
+ const result = envelope(data);
17600
+ return {
17601
+ structuredContent: result,
17602
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
17603
+ };
17604
+ }
17605
+ function registerLifecycleTools(server) {
17606
+ server.tool(
17607
+ "cancel_async_job",
17608
+ "Cancel an owned pending async generation/render job before a worker starts it. If credits were already debited, the backend attempts an idempotent refund and reports the result. Processing or terminal jobs are not cancellable.",
17609
+ {
17610
+ job_id: z39.string().uuid().describe("Owned async_jobs ID returned by a generation tool."),
17611
+ project_id: PROJECT_ID,
17612
+ confirm: CONFIRM
17613
+ },
17614
+ async ({ job_id, project_id }) => invokeLifecycle("cancel-async-job", project_id, { job_id })
17615
+ );
17616
+ server.tool(
17617
+ "cancel_scheduled_post",
17618
+ "Cancel an owned draft, pending, or scheduled post before publishing starts. This closes its pending schedule job first; it refuses once a worker has claimed the publication.",
17619
+ {
17620
+ post_id: z39.string().uuid().describe("Owned scheduled post ID."),
17621
+ project_id: PROJECT_ID,
17622
+ confirm: CONFIRM
17623
+ },
17624
+ async ({ post_id, project_id }) => invokeLifecycle("cancel-scheduled-post", project_id, { post_id })
17625
+ );
17626
+ server.tool(
17627
+ "delete_carousel",
17628
+ "Delete an owned carousel content-history record from one project. Stored media is retained until the normal retention cleanup; this does not delete already-published platform posts.",
17629
+ {
17630
+ content_id: z39.string().uuid().describe("Owned carousel content_history ID."),
17631
+ project_id: PROJECT_ID,
17632
+ confirm: CONFIRM
17633
+ },
17634
+ async ({ content_id, project_id }) => invokeLifecycle("delete-carousel", project_id, { content_id })
17635
+ );
17636
+ server.tool(
17637
+ "delete_content_plan",
17638
+ "Permanently delete an owned content plan in one project. This does not cancel posts that were already scheduled from the plan.",
17639
+ {
17640
+ plan_id: z39.string().uuid().describe("Owned content plan ID."),
17641
+ project_id: PROJECT_ID,
17642
+ confirm: CONFIRM
17643
+ },
17644
+ async ({ plan_id, project_id }) => invokeLifecycle("delete-content-plan", project_id, { plan_id })
17645
+ );
17646
+ server.tool(
17647
+ "delete_autopilot_config",
17648
+ "Permanently delete an owned autopilot configuration in one project. Historical runs and already-published posts are retained.",
17649
+ {
17650
+ config_id: z39.string().uuid().describe("Owned autopilot configuration ID."),
17651
+ project_id: PROJECT_ID,
17652
+ confirm: CONFIRM
17653
+ },
17654
+ async ({ config_id, project_id }) => invokeLifecycle("delete-autopilot-config", project_id, { config_id })
17655
+ );
17656
+ }
17657
+ var PROJECT_ID, CONFIRM;
17658
+ var init_lifecycle = __esm({
17659
+ "src/tools/lifecycle.ts"() {
17660
+ "use strict";
17661
+ init_edge_function();
17662
+ init_supabase();
17663
+ init_tool_error();
17664
+ init_version();
17665
+ PROJECT_ID = z39.string().uuid().optional().describe("Brand/project ID. Defaults to the authenticated key's project or account default.");
17666
+ CONFIRM = z39.literal(true).describe("Must be true only after the user explicitly confirms this destructive action.");
17667
+ }
17668
+ });
17669
+
16291
17670
  // src/lib/register-tools.ts
17671
+ function redactBase64Payloads(args) {
17672
+ if (typeof args !== "object" || args === null || Array.isArray(args)) return args;
17673
+ let changed = false;
17674
+ const out = { ...args };
17675
+ for (const key of BASE64_PAYLOAD_KEYS) {
17676
+ const value = out[key];
17677
+ if (typeof value !== "string" || value.length < 64) continue;
17678
+ const body = value.replace(DATA_URI_PREFIX2, "");
17679
+ if (STRICT_BASE64.test(body)) {
17680
+ out[key] = `[base64:${body.length} chars omitted from prose scan]`;
17681
+ changed = true;
17682
+ }
17683
+ }
17684
+ return changed ? out : args;
17685
+ }
16292
17686
  function wrapToolWithScanner(toolName, handler) {
16293
17687
  return async function scannerWrappedHandler(...handlerArgs) {
16294
17688
  const args = handlerArgs[0];
16295
17689
  const ctx = handlerArgs[1];
16296
- const inputText = args === void 0 ? "{}" : typeof args === "string" ? args : JSON.stringify(args);
17690
+ const scanArgs = redactBase64Payloads(args);
17691
+ const inputText = scanArgs === void 0 ? "{}" : typeof scanArgs === "string" ? scanArgs : JSON.stringify(scanArgs);
16297
17692
  const inputScan = scan(inputText, {
16298
17693
  mode: "block",
16299
17694
  source: "mcp_tool_input",
@@ -16321,6 +17716,16 @@ function wrapToolWithScanner(toolName, handler) {
16321
17716
  source: "mcp_tool_output",
16322
17717
  user_id: ctx?.userId
16323
17718
  });
17719
+ if (!outputScan.passed) {
17720
+ try {
17721
+ ctx?.logScan?.(toolName, "output", outputScan);
17722
+ } catch {
17723
+ }
17724
+ return toolError(
17725
+ "server_error",
17726
+ "The response exceeded the safe output limit and was not returned."
17727
+ );
17728
+ }
16324
17729
  if (outputScan.sanitized_text !== void 0) {
16325
17730
  try {
16326
17731
  ctx?.logScan?.(toolName, "output", outputScan);
@@ -16329,7 +17734,10 @@ function wrapToolWithScanner(toolName, handler) {
16329
17734
  try {
16330
17735
  return JSON.parse(outputScan.sanitized_text);
16331
17736
  } catch {
16332
- return result;
17737
+ return toolError(
17738
+ "server_error",
17739
+ "The response could not be returned safely. Please retry or contact support."
17740
+ );
16333
17741
  }
16334
17742
  }
16335
17743
  return result;
@@ -16379,8 +17787,7 @@ function applyScopeEnforcement(server, scopeResolver) {
16379
17787
  details: {
16380
17788
  source: "wrapper",
16381
17789
  // A thrown exception escaped the handler — an unclassified fault.
16382
- error_type: "server_error",
16383
- exception: err instanceof Error ? err.message.slice(0, 200) : "unknown"
17790
+ error_type: "server_error"
16384
17791
  }
16385
17792
  });
16386
17793
  throw err;
@@ -16512,12 +17919,14 @@ function registerAllTools(server, options) {
16512
17919
  registerSkillsTools(server);
16513
17920
  registerLoopPulseTools(server);
16514
17921
  registerBanditStateTools(server);
17922
+ registerLifecycleTools(server);
16515
17923
  if (!options?.skipApps) {
16516
17924
  registerContentCalendarApp(server);
17925
+ registerAnalyticsPulseApp(server);
16517
17926
  }
16518
17927
  applyAnnotations(server);
16519
17928
  }
16520
- var RESPONSE_CHAR_LIMIT;
17929
+ var BASE64_PAYLOAD_KEYS, DATA_URI_PREFIX2, STRICT_BASE64, RESPONSE_CHAR_LIMIT;
16521
17930
  var init_register_tools = __esm({
16522
17931
  "src/lib/register-tools.ts"() {
16523
17932
  "use strict";
@@ -16559,12 +17968,17 @@ var init_register_tools = __esm({
16559
17968
  init_niche_research();
16560
17969
  init_hyperframes();
16561
17970
  init_content_calendar();
17971
+ init_analytics_pulse();
16562
17972
  init_connections();
16563
17973
  init_harness();
16564
17974
  init_hermes();
16565
17975
  init_skills();
16566
17976
  init_loopPulse();
16567
17977
  init_banditState();
17978
+ init_lifecycle();
17979
+ BASE64_PAYLOAD_KEYS = /* @__PURE__ */ new Set(["file_data", "fileData"]);
17980
+ DATA_URI_PREFIX2 = /^data:[\w.+-]+\/[\w.+-]+;base64,/;
17981
+ STRICT_BASE64 = /^[A-Za-z0-9+/]+={0,2}$/;
16568
17982
  RESPONSE_CHAR_LIMIT = 1e5;
16569
17983
  }
16570
17984
  });
@@ -16586,7 +18000,11 @@ function getCallHandler() {
16586
18000
  return cachedCallHandler;
16587
18001
  }
16588
18002
  function restToolNames() {
16589
- return new Set(TOOL_CATALOG.filter((t) => !t.localOnly && !t.internal).map((t) => t.name));
18003
+ return new Set(
18004
+ TOOL_CATALOG.filter((t) => !t.localOnly && !t.internal && !t.hiddenFromPublicCount).map(
18005
+ (t) => t.name
18006
+ )
18007
+ );
16590
18008
  }
16591
18009
  async function invokeToolRest(name, args) {
16592
18010
  const handler = getCallHandler();
@@ -17152,7 +18570,7 @@ function printSnUsage() {
17152
18570
  console.error("");
17153
18571
  console.error("Discovery:");
17154
18572
  console.error(" tools [--scope <scope>] [--module <module>] [--json]");
17155
- console.error(" info [--json]");
18573
+ console.error(" info [--offline] [--json]");
17156
18574
  console.error("");
17157
18575
  console.error("Content:");
17158
18576
  console.error(
@@ -17285,10 +18703,11 @@ var setup_exports = {};
17285
18703
  __export(setup_exports, {
17286
18704
  generatePKCE: () => generatePKCE,
17287
18705
  getAppBaseUrl: () => getAppBaseUrl,
18706
+ isValidSetupApiKey: () => isValidSetupApiKey,
17288
18707
  runLogout: () => runLogout,
17289
18708
  runSetup: () => runSetup
17290
18709
  });
17291
- import { createHash as createHash4, randomBytes, randomUUID as randomUUID4 } from "node:crypto";
18710
+ import { createHash as createHash3, randomBytes as randomBytes2, randomUUID as randomUUID4 } from "node:crypto";
17292
18711
  import { createServer } from "node:http";
17293
18712
  import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
17294
18713
  import { homedir as homedir3, platform as platform2 } from "node:os";
@@ -17297,12 +18716,15 @@ function base64url(buffer) {
17297
18716
  return buffer.toString("base64url");
17298
18717
  }
17299
18718
  function generatePKCE() {
17300
- const verifierBytes = randomBytes(32);
18719
+ const verifierBytes = randomBytes2(32);
17301
18720
  const codeVerifier = base64url(verifierBytes);
17302
- const challengeHash = createHash4("sha256").update(codeVerifier).digest();
18721
+ const challengeHash = createHash3("sha256").update(codeVerifier).digest();
17303
18722
  const codeChallenge = base64url(challengeHash);
17304
18723
  return { codeVerifier, codeChallenge };
17305
18724
  }
18725
+ function isValidSetupApiKey(value) {
18726
+ return typeof value === "string" && value.length >= 32 && value.length <= 512 && /^snk_live_[A-Za-z0-9_-]+$/.test(value);
18727
+ }
17306
18728
  function getAppBaseUrl() {
17307
18729
  return process.env.SOCIALNEURON_APP_URL || "https://www.socialneuron.com";
17308
18730
  }
@@ -17364,11 +18786,24 @@ function configureMcpClient(configPath) {
17364
18786
  return false;
17365
18787
  }
17366
18788
  }
17367
- function readBody(req) {
18789
+ function readBody(req, maxBytes = 16 * 1024) {
17368
18790
  return new Promise((resolve3, reject) => {
17369
18791
  const chunks = [];
17370
- req.on("data", (chunk) => chunks.push(chunk));
17371
- req.on("end", () => resolve3(Buffer.concat(chunks).toString("utf-8")));
18792
+ let total = 0;
18793
+ let tooLarge = false;
18794
+ req.on("data", (chunk) => {
18795
+ if (tooLarge) return;
18796
+ total += chunk.length;
18797
+ if (total > maxBytes) {
18798
+ tooLarge = true;
18799
+ reject(new Error("request_too_large"));
18800
+ return;
18801
+ }
18802
+ chunks.push(chunk);
18803
+ });
18804
+ req.on("end", () => {
18805
+ if (!tooLarge) resolve3(Buffer.concat(chunks).toString("utf-8"));
18806
+ });
17372
18807
  req.on("error", reject);
17373
18808
  });
17374
18809
  }
@@ -17381,14 +18816,13 @@ async function completePkceExchange(codeVerifier, state) {
17381
18816
  body: JSON.stringify({ code_verifier: codeVerifier, state })
17382
18817
  });
17383
18818
  if (!response.ok) {
17384
- const text = await response.text();
17385
- console.error(` PKCE exchange failed: ${text}`);
18819
+ console.error(` PKCE exchange failed (HTTP ${response.status}).`);
17386
18820
  return false;
17387
18821
  }
17388
18822
  const data = await response.json();
17389
18823
  return data.success === true;
17390
- } catch (err) {
17391
- console.error(` PKCE exchange error: ${err instanceof Error ? err.message : String(err)}`);
18824
+ } catch {
18825
+ console.error(" PKCE exchange failed because the authentication service was unavailable.");
17392
18826
  return false;
17393
18827
  }
17394
18828
  }
@@ -17399,7 +18833,8 @@ async function runSetup() {
17399
18833
  console.error("");
17400
18834
  console.error(" Privacy Notice:");
17401
18835
  console.error(" - Your API key is stored locally in your OS keychain");
17402
- console.error(" - Tool invocations are logged for usage metering (no content stored)");
18836
+ console.error(" - CLI telemetry records tool name, status, and duration \u2014 not tool input/output");
18837
+ console.error(" - Service-side content processing follows https://socialneuron.com/privacy");
17403
18838
  console.error(" - Set DO_NOT_TRACK=1 to disable telemetry");
17404
18839
  console.error(" - Data export/delete: https://www.socialneuron.com/settings");
17405
18840
  console.error("");
@@ -17423,7 +18858,6 @@ async function runSetup() {
17423
18858
  const open = (await import("open")).default;
17424
18859
  await open(authorizeUrl.toString());
17425
18860
  console.error(" Opening browser for authorization...");
17426
- console.error(` URL: ${authorizeUrl.toString()}`);
17427
18861
  console.error("");
17428
18862
  console.error(" Waiting for authorization (timeout: 120s)...");
17429
18863
  } catch {
@@ -17435,12 +18869,20 @@ async function runSetup() {
17435
18869
  console.error(" Waiting for authorization (timeout: 120s)...");
17436
18870
  }
17437
18871
  const result = await new Promise((resolve3) => {
18872
+ const callbackOrigin = new URL(baseUrl).origin;
17438
18873
  const timeout = setTimeout(() => {
17439
18874
  server.close();
17440
18875
  resolve3({ error: "Authorization timed out after 120 seconds." });
17441
18876
  }, 12e4);
17442
18877
  server.on("request", async (req, res) => {
17443
- res.setHeader("Access-Control-Allow-Origin", "*");
18878
+ const requestOrigin = req.headers.origin;
18879
+ if (requestOrigin !== callbackOrigin) {
18880
+ res.writeHead(403, { "Content-Type": "application/json" });
18881
+ res.end(JSON.stringify({ error: "Origin not allowed" }));
18882
+ return;
18883
+ }
18884
+ res.setHeader("Access-Control-Allow-Origin", callbackOrigin);
18885
+ res.setHeader("Vary", "Origin");
17444
18886
  res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
17445
18887
  res.setHeader("Access-Control-Allow-Headers", "Content-Type");
17446
18888
  res.setHeader("Access-Control-Allow-Private-Network", "true");
@@ -17458,9 +18900,9 @@ async function runSetup() {
17458
18900
  res.end(JSON.stringify({ error: "State mismatch" }));
17459
18901
  return;
17460
18902
  }
17461
- if (!data.api_key) {
18903
+ if (!isValidSetupApiKey(data.api_key)) {
17462
18904
  res.writeHead(400, { "Content-Type": "application/json" });
17463
- res.end(JSON.stringify({ error: "Missing api_key" }));
18905
+ res.end(JSON.stringify({ error: "Invalid api_key" }));
17464
18906
  return;
17465
18907
  }
17466
18908
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -17502,9 +18944,9 @@ async function runSetup() {
17502
18944
  console.error(` Key prefix: ${apiKey.substring(0, 12)}...`);
17503
18945
  const configPaths = getConfigPaths();
17504
18946
  let configured = false;
17505
- for (const { path: path2, name } of configPaths) {
17506
- if (configureMcpClient(path2)) {
17507
- console.error(` Configured ${name}: ${path2}`);
18947
+ for (const { path: path3, name } of configPaths) {
18948
+ if (configureMcpClient(path3)) {
18949
+ console.error(` Configured ${name}: ${path3}`);
17508
18950
  configured = true;
17509
18951
  }
17510
18952
  }
@@ -17543,12 +18985,12 @@ __export(validation_cache_exports, {
17543
18985
  readValidationCache: () => readValidationCache,
17544
18986
  writeValidationCache: () => writeValidationCache
17545
18987
  });
17546
- import { createHash as createHash5 } from "node:crypto";
18988
+ import { createHash as createHash4 } from "node:crypto";
17547
18989
  import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, chmodSync } from "node:fs";
17548
18990
  import { join as join4 } from "node:path";
17549
18991
  import { homedir as homedir4 } from "node:os";
17550
18992
  function keyFingerprint(apiKey) {
17551
- return `sha256:${createHash5("sha256").update(apiKey, "utf8").digest("hex")}`;
18993
+ return `sha256:${createHash4("sha256").update(apiKey, "utf8").digest("hex")}`;
17552
18994
  }
17553
18995
  function readValidationCache(apiKey) {
17554
18996
  try {
@@ -17689,8 +19131,7 @@ async function runLoginDevice() {
17689
19131
  body: JSON.stringify({})
17690
19132
  });
17691
19133
  if (!response.ok) {
17692
- const text = await response.text();
17693
- console.error(` Error: Failed to create device code. ${text}`);
19134
+ console.error(` Error: Failed to create device code (HTTP ${response.status}).`);
17694
19135
  process.exit(1);
17695
19136
  }
17696
19137
  const data = await response.json();
@@ -17940,7 +19381,7 @@ async function runHealthCheck(options) {
17940
19381
  checks.push({
17941
19382
  name: "Connectivity",
17942
19383
  ok: false,
17943
- detail: err instanceof Error ? err.message : "Failed to reach server"
19384
+ detail: "Failed to reach the authentication service."
17944
19385
  });
17945
19386
  }
17946
19387
  }
@@ -17964,7 +19405,7 @@ async function runHealthCheck(options) {
17964
19405
  checks.push({
17965
19406
  name: "Connectivity",
17966
19407
  ok: false,
17967
- detail: err instanceof Error ? err.message : "Network error"
19408
+ detail: "Network request failed."
17968
19409
  });
17969
19410
  }
17970
19411
  if (apiKey) {