@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/index.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
 
@@ -91,6 +91,9 @@ var init_scopes = __esm({
91
91
  generate_carousel: "mcp:write",
92
92
  create_carousel: "mcp:write",
93
93
  upload_media: "mcp:write",
94
+ cancel_async_job: "mcp:write",
95
+ delete_carousel: "mcp:write",
96
+ delete_content_plan: "mcp:write",
94
97
  // mcp:read (media)
95
98
  get_media_url: "mcp:read",
96
99
  // F4 Hyperframes — HTML composition runtime
@@ -98,6 +101,8 @@ var init_scopes = __esm({
98
101
  render_hyperframes: "mcp:write",
99
102
  // mcp:distribute
100
103
  schedule_post: "mcp:distribute",
104
+ reschedule_post: "mcp:distribute",
105
+ cancel_scheduled_post: "mcp:distribute",
101
106
  start_platform_connection: "mcp:distribute",
102
107
  // mcp:analytics
103
108
  refresh_platform_analytics: "mcp:analytics",
@@ -112,6 +117,7 @@ var init_scopes = __esm({
112
117
  list_autopilot_configs: "mcp:autopilot",
113
118
  update_autopilot_config: "mcp:autopilot",
114
119
  get_autopilot_status: "mcp:autopilot",
120
+ delete_autopilot_config: "mcp:autopilot",
115
121
  // Recipes
116
122
  list_recipes: "mcp:read",
117
123
  get_recipe_details: "mcp:read",
@@ -155,6 +161,7 @@ var init_scopes = __esm({
155
161
  detect_anomalies: "mcp:analytics",
156
162
  // mcp:read (Apps — entry tool for the Content Calendar MCP App; reads recent posts)
157
163
  open_content_calendar: "mcp:read",
164
+ open_analytics_pulse: "mcp:read",
158
165
  // mcp:write (Agentic harness — learning loop write-back)
159
166
  write_agent_reflection: "mcp:write",
160
167
  record_outcome: "mcp:write",
@@ -170,6 +177,7 @@ var init_scopes = __esm({
170
177
  get_active_campaigns: "mcp:read",
171
178
  // mcp:read / mcp:write (Skills)
172
179
  list_skills: "mcp:read",
180
+ get_skill: "mcp:read",
173
181
  run_skill: "mcp:write",
174
182
  // mcp:read (Loop observability — growth-loop KPIs + content learning state)
175
183
  get_loop_pulse: "mcp:read",
@@ -296,6 +304,11 @@ var init_tool_annotations = __esm({
296
304
  OVERRIDES = {
297
305
  // Destructive or overwrite tools
298
306
  delete_comment: { destructiveHint: true },
307
+ cancel_async_job: { destructiveHint: true, idempotentHint: true },
308
+ cancel_scheduled_post: { destructiveHint: true, idempotentHint: true, openWorldHint: true },
309
+ delete_carousel: { destructiveHint: true, idempotentHint: true },
310
+ delete_content_plan: { destructiveHint: true, idempotentHint: true },
311
+ delete_autopilot_config: { destructiveHint: true, idempotentHint: true },
299
312
  moderate_comment: { destructiveHint: true },
300
313
  save_brand_profile: { destructiveHint: true, idempotentHint: true },
301
314
  update_platform_voice: { destructiveHint: true, idempotentHint: true },
@@ -381,25 +394,140 @@ __export(credentials_exports, {
381
394
  saveSupabaseUrl: () => saveSupabaseUrl
382
395
  });
383
396
  import { execFileSync } from "node:child_process";
384
- import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
397
+ import {
398
+ closeSync,
399
+ constants,
400
+ existsSync,
401
+ fchmodSync,
402
+ fstatSync,
403
+ ftruncateSync,
404
+ lstatSync,
405
+ mkdirSync,
406
+ openSync,
407
+ readFileSync,
408
+ unlinkSync,
409
+ writeFileSync
410
+ } from "node:fs";
385
411
  import { homedir, platform } from "node:os";
386
412
  import { join } from "node:path";
413
+ function assertSafeCredentialPaths() {
414
+ if (platform() === "win32") return;
415
+ const uid = process.getuid?.();
416
+ if (existsSync(CONFIG_DIR)) {
417
+ const directory = lstatSync(CONFIG_DIR);
418
+ if (directory.isSymbolicLink() || !directory.isDirectory()) {
419
+ throw new Error("Unsafe Social Neuron credential directory. Refusing to use it.");
420
+ }
421
+ if (uid !== void 0 && directory.uid !== uid) {
422
+ throw new Error("Social Neuron credential directory is not owned by the current user.");
423
+ }
424
+ }
425
+ if (existsSync(CREDENTIALS_FILE)) {
426
+ const file = lstatSync(CREDENTIALS_FILE);
427
+ if (file.isSymbolicLink() || !file.isFile()) {
428
+ throw new Error("Unsafe Social Neuron credential file. Refusing to use it.");
429
+ }
430
+ if (uid !== void 0 && file.uid !== uid) {
431
+ throw new Error("Social Neuron credential file is not owned by the current user.");
432
+ }
433
+ }
434
+ }
435
+ function openValidatedCredentialPath(target, kind, flags) {
436
+ let fd;
437
+ try {
438
+ fd = openSync(
439
+ target,
440
+ flags | constants.O_NOFOLLOW | (kind === "directory" ? constants.O_DIRECTORY : 0)
441
+ );
442
+ } catch (error) {
443
+ if (error.code === "ENOENT") return null;
444
+ throw new Error(`Unsafe Social Neuron credential ${kind}. Refusing to use it.`);
445
+ }
446
+ const opened = fstatSync(fd);
447
+ const uid = process.getuid?.();
448
+ if ((kind === "directory" ? !opened.isDirectory() : !opened.isFile()) || uid !== void 0 && opened.uid !== uid || kind === "file" && opened.nlink !== 1) {
449
+ closeSync(fd);
450
+ throw new Error(`Unsafe Social Neuron credential ${kind}. Refusing to use it.`);
451
+ }
452
+ return fd;
453
+ }
454
+ function hardenCredentialPath(target, kind, mode) {
455
+ const fd = openValidatedCredentialPath(target, kind, constants.O_RDONLY);
456
+ if (fd === null) return;
457
+ try {
458
+ const opened = fstatSync(fd);
459
+ if ((opened.mode & 63) !== 0) fchmodSync(fd, mode);
460
+ } finally {
461
+ closeSync(fd);
462
+ }
463
+ }
464
+ function hardenCredentialPermissions() {
465
+ if (platform() === "win32") return;
466
+ hardenCredentialPath(CONFIG_DIR, "directory", 448);
467
+ hardenCredentialPath(CREDENTIALS_FILE, "file", 384);
468
+ }
387
469
  function readCredentialsFile() {
470
+ if (platform() !== "win32") hardenCredentialPath(CONFIG_DIR, "directory", 448);
471
+ const fd = platform() === "win32" ? (() => {
472
+ try {
473
+ return openSync(CREDENTIALS_FILE, constants.O_RDONLY);
474
+ } catch (error) {
475
+ if (error.code === "ENOENT") return null;
476
+ throw error;
477
+ }
478
+ })() : openValidatedCredentialPath(CREDENTIALS_FILE, "file", constants.O_RDONLY);
479
+ if (fd === null) return {};
388
480
  try {
389
- if (!existsSync(CREDENTIALS_FILE)) return {};
390
- const raw = readFileSync(CREDENTIALS_FILE, "utf-8");
391
- return JSON.parse(raw);
392
- } catch {
393
- return {};
481
+ if (platform() !== "win32") fchmodSync(fd, 384);
482
+ const parsed = JSON.parse(readFileSync(fd, "utf-8"));
483
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
484
+ throw new Error("Social Neuron credential file is invalid.");
485
+ }
486
+ return parsed;
487
+ } finally {
488
+ closeSync(fd);
394
489
  }
395
490
  }
396
491
  function writeCredentialsFile(data) {
397
492
  if (!existsSync(CONFIG_DIR)) {
398
493
  mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
399
494
  }
400
- writeFileSync(CREDENTIALS_FILE, JSON.stringify(data, null, 2) + "\n", {
401
- mode: 384
402
- });
495
+ assertSafeCredentialPaths();
496
+ const payload = JSON.stringify(data, null, 2) + "\n";
497
+ if (platform() === "win32") {
498
+ writeFileSync(CREDENTIALS_FILE, payload, { mode: 384 });
499
+ } else {
500
+ let fd;
501
+ try {
502
+ fd = openSync(
503
+ CREDENTIALS_FILE,
504
+ constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,
505
+ 384
506
+ );
507
+ } catch (error) {
508
+ if (error.code !== "EEXIST") {
509
+ throw new Error("Unsafe Social Neuron credential file. Refusing to write it.");
510
+ }
511
+ try {
512
+ fd = openSync(CREDENTIALS_FILE, constants.O_WRONLY | constants.O_NOFOLLOW);
513
+ } catch {
514
+ throw new Error("Unsafe Social Neuron credential file. Refusing to write it.");
515
+ }
516
+ }
517
+ try {
518
+ const opened = fstatSync(fd);
519
+ const uid = process.getuid?.();
520
+ if (!opened.isFile() || opened.nlink !== 1 || uid !== void 0 && opened.uid !== uid) {
521
+ throw new Error("Unsafe Social Neuron credential file. Refusing to write it.");
522
+ }
523
+ fchmodSync(fd, 384);
524
+ ftruncateSync(fd, 0);
525
+ writeFileSync(fd, payload);
526
+ } finally {
527
+ closeSync(fd);
528
+ }
529
+ }
530
+ hardenCredentialPermissions();
403
531
  }
404
532
  function macKeychainRead(service) {
405
533
  try {
@@ -609,7 +737,6 @@ async function validateApiKey(apiKey, _attempt = 0) {
609
737
  }
610
738
  );
611
739
  if (!response.ok) {
612
- const text = await response.text();
613
740
  const retryable = response.status === 429 || response.status >= 500;
614
741
  if (retryable && _attempt < VALIDATE_MAX_RETRIES) {
615
742
  await sleep(300 * (_attempt + 1));
@@ -618,11 +745,19 @@ async function validateApiKey(apiKey, _attempt = 0) {
618
745
  return {
619
746
  valid: false,
620
747
  retryable,
621
- error: `Validation failed (HTTP ${response.status}): ${text}`
748
+ error: `Validation failed (HTTP ${response.status}).`
622
749
  };
623
750
  }
624
- return await response.json();
625
- } catch (err) {
751
+ const result = await response.json();
752
+ if (!result.valid) {
753
+ return {
754
+ valid: false,
755
+ retryable: false,
756
+ error: "API key is invalid, expired, or revoked."
757
+ };
758
+ }
759
+ return result;
760
+ } catch {
626
761
  if (_attempt < VALIDATE_MAX_RETRIES) {
627
762
  await sleep(300 * (_attempt + 1));
628
763
  return validateApiKey(apiKey, _attempt + 1);
@@ -630,7 +765,7 @@ async function validateApiKey(apiKey, _attempt = 0) {
630
765
  return {
631
766
  valid: false,
632
767
  retryable: true,
633
- error: err instanceof Error ? err.message : String(err)
768
+ error: "Authentication service is temporarily unavailable."
634
769
  };
635
770
  }
636
771
  }
@@ -651,10 +786,12 @@ __export(posthog_exports, {
651
786
  initPostHog: () => initPostHog,
652
787
  shutdownPostHog: () => shutdownPostHog
653
788
  });
654
- import { createHash } from "node:crypto";
789
+ import { createHmac, randomBytes } from "node:crypto";
655
790
  import { PostHog } from "posthog-node";
656
791
  function hashUserId(userId) {
657
- return createHash("sha256").update(`${POSTHOG_SALT}:${userId}`).digest("hex").substring(0, 32);
792
+ const hmac = createHmac("sha256", POSTHOG_PSEUDONYMIZATION_KEY);
793
+ hmac.update(userId);
794
+ return hmac.digest("hex").substring(0, 32);
658
795
  }
659
796
  function initPostHog() {
660
797
  if (isTelemetryDisabled()) return;
@@ -690,13 +827,13 @@ async function shutdownPostHog() {
690
827
  client = null;
691
828
  }
692
829
  }
693
- var POSTHOG_SALT, client;
830
+ var POSTHOG_PSEUDONYMIZATION_KEY, client;
694
831
  var init_posthog = __esm({
695
832
  "src/lib/posthog.ts"() {
696
833
  "use strict";
697
834
  init_supabase();
698
835
  init_request_context();
699
- POSTHOG_SALT = "socialneuron-mcp-ph-v1";
836
+ POSTHOG_PSEUDONYMIZATION_KEY = process.env.POSTHOG_PSEUDONYMIZATION_KEY || randomBytes(32).toString("hex");
700
837
  client = null;
701
838
  }
702
839
  });
@@ -1004,9 +1141,6 @@ function searchTools(query) {
1004
1141
  (t) => t.name.toLowerCase().includes(q) || t.description.toLowerCase().includes(q)
1005
1142
  );
1006
1143
  }
1007
- function getModules() {
1008
- return [...new Set(TOOL_CATALOG.map((t) => t.module))];
1009
- }
1010
1144
  var TOOL_CATALOG;
1011
1145
  var init_tool_catalog = __esm({
1012
1146
  "src/lib/tool-catalog.ts"() {
@@ -1081,6 +1215,26 @@ var init_tool_catalog = __esm({
1081
1215
  module: "carousel",
1082
1216
  scope: "mcp:write"
1083
1217
  },
1218
+ {
1219
+ name: "cancel_async_job",
1220
+ description: "Cancel an owned pending async job and refund an eligible debit",
1221
+ module: "lifecycle",
1222
+ scope: "mcp:write",
1223
+ task_intent: "Stop a queued generation or render before worker execution",
1224
+ use_when: "The user explicitly wants to cancel a pending job and has confirmed the action.",
1225
+ avoid_when: "The job is already processing or terminal; check_status instead.",
1226
+ next_tools: ["check_status", "get_credit_balance"]
1227
+ },
1228
+ {
1229
+ name: "delete_carousel",
1230
+ description: "Delete an owned carousel content record from one project",
1231
+ module: "lifecycle",
1232
+ scope: "mcp:write",
1233
+ task_intent: "Remove an unwanted carousel record from Social Neuron",
1234
+ use_when: "The user explicitly confirms deletion and understands stored media follows normal retention.",
1235
+ avoid_when: "The user expects an already-published social post or retained media object to be removed.",
1236
+ next_tools: ["list_recent_posts"]
1237
+ },
1084
1238
  // media
1085
1239
  {
1086
1240
  name: "upload_media",
@@ -1101,6 +1255,26 @@ var init_tool_catalog = __esm({
1101
1255
  module: "distribution",
1102
1256
  scope: "mcp:distribute"
1103
1257
  },
1258
+ {
1259
+ name: "reschedule_post",
1260
+ description: "Atomically move an unclaimed scheduled post to a new time within its brand project",
1261
+ module: "distribution",
1262
+ scope: "mcp:distribute",
1263
+ task_intent: "Move an already scheduled post without creating a duplicate publication",
1264
+ use_when: "A pending or scheduled post needs a different future publication time.",
1265
+ avoid_when: "The post is already publishing or published; create a new post instead.",
1266
+ next_tools: ["list_recent_posts", "open_content_calendar"]
1267
+ },
1268
+ {
1269
+ name: "cancel_scheduled_post",
1270
+ description: "Cancel an owned scheduled post before publishing begins",
1271
+ module: "lifecycle",
1272
+ scope: "mcp:distribute",
1273
+ task_intent: "Unschedule a post without publishing it",
1274
+ use_when: "The user explicitly confirms cancellation of a draft, pending, or scheduled post.",
1275
+ avoid_when: "The post is already publishing or published.",
1276
+ next_tools: ["list_recent_posts", "open_content_calendar"]
1277
+ },
1104
1278
  {
1105
1279
  name: "list_recent_posts",
1106
1280
  description: "List recently published or scheduled posts",
@@ -1291,6 +1465,16 @@ var init_tool_catalog = __esm({
1291
1465
  module: "planning",
1292
1466
  scope: "mcp:write"
1293
1467
  },
1468
+ {
1469
+ name: "delete_content_plan",
1470
+ description: "Permanently delete an owned content plan from one project",
1471
+ module: "lifecycle",
1472
+ scope: "mcp:write",
1473
+ task_intent: "Remove an obsolete saved content plan",
1474
+ use_when: "The user explicitly confirms permanent deletion of a specific plan.",
1475
+ avoid_when: "The plan has scheduled posts the user also expects to cancel; cancel those posts separately.",
1476
+ next_tools: ["plan_content_week", "list_recent_posts"]
1477
+ },
1294
1478
  {
1295
1479
  name: "submit_content_plan_for_approval",
1296
1480
  description: "Submit a content plan for team approval",
@@ -1385,6 +1569,16 @@ var init_tool_catalog = __esm({
1385
1569
  module: "autopilot",
1386
1570
  scope: "mcp:autopilot"
1387
1571
  },
1572
+ {
1573
+ name: "delete_autopilot_config",
1574
+ description: "Permanently delete an owned autopilot configuration from one project",
1575
+ module: "lifecycle",
1576
+ scope: "mcp:autopilot",
1577
+ task_intent: "Remove an autopilot configuration while retaining historical run records",
1578
+ use_when: "The user explicitly confirms deletion of the configuration.",
1579
+ avoid_when: "The user only wants to pause or edit automation, or expects historical posts to be deleted.",
1580
+ next_tools: ["list_autopilot_configs"]
1581
+ },
1388
1582
  // extraction
1389
1583
  {
1390
1584
  name: "extract_url_content",
@@ -1501,7 +1695,13 @@ var init_tool_catalog = __esm({
1501
1695
  // apps (MCP Apps — interactive UI inside the host)
1502
1696
  {
1503
1697
  name: "open_content_calendar",
1504
- 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.",
1698
+ 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.",
1699
+ module: "apps",
1700
+ scope: "mcp:read"
1701
+ },
1702
+ {
1703
+ name: "open_analytics_pulse",
1704
+ description: "Open a project-scoped interactive performance dashboard with views, engagement, platform mix, and top posts.",
1505
1705
  module: "apps",
1506
1706
  scope: "mcp:read"
1507
1707
  },
@@ -1616,6 +1816,12 @@ var init_tool_catalog = __esm({
1616
1816
  module: "skills",
1617
1817
  scope: "mcp:read"
1618
1818
  },
1819
+ {
1820
+ name: "get_skill",
1821
+ description: "Fetch the full body and current compiled guidance for one Social Neuron skill by slug.",
1822
+ module: "skills",
1823
+ scope: "mcp:read"
1824
+ },
1619
1825
  {
1620
1826
  name: "run_skill",
1621
1827
  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.",
@@ -1689,6 +1895,7 @@ var init_tool_profile = __esm({
1689
1895
  "list_hyperframes_blocks",
1690
1896
  "render_hyperframes",
1691
1897
  "list_skills",
1898
+ "get_skill",
1692
1899
  "run_skill"
1693
1900
  ]);
1694
1901
  }
@@ -1722,7 +1929,8 @@ var init_constants = __esm({
1722
1929
  ssn: "\\d{3}-\\d{2}-\\d{4}",
1723
1930
  ip: "(?<![\\d.])(?:\\d{1,3}\\.){3}\\d{1,3}(?![\\d.])"
1724
1931
  },
1725
- MAX_LENGTH: 1e4
1932
+ MAX_LENGTH: 1e4,
1933
+ MAX_OUTPUT_LENGTH: 1e6
1726
1934
  };
1727
1935
  }
1728
1936
  });
@@ -1819,7 +2027,8 @@ var init_pii = __esm({
1819
2027
  function scan(text, options) {
1820
2028
  const flagged = /* @__PURE__ */ new Set();
1821
2029
  let risk = 0;
1822
- if (text.length > CONSTANTS.MAX_LENGTH) {
2030
+ const maxLength = options.source === "mcp_tool_output" ? CONSTANTS.MAX_OUTPUT_LENGTH : CONSTANTS.MAX_LENGTH;
2031
+ if (text.length > maxLength) {
1823
2032
  return {
1824
2033
  passed: false,
1825
2034
  risk_score: 1,
@@ -1875,6 +2084,52 @@ var edge_function_exports = {};
1875
2084
  __export(edge_function_exports, {
1876
2085
  callEdgeFunction: () => callEdgeFunction
1877
2086
  });
2087
+ function safeGatewayError(responseText, status) {
2088
+ try {
2089
+ const parsed = JSON.parse(responseText);
2090
+ const nested = parsed.error && typeof parsed.error === "object" ? parsed.error : null;
2091
+ const candidates = [
2092
+ nested?.error_type,
2093
+ nested?.code,
2094
+ parsed.error_type,
2095
+ parsed.error_code,
2096
+ parsed.code,
2097
+ typeof parsed.error === "string" ? parsed.error : null
2098
+ ];
2099
+ for (const value of candidates) {
2100
+ if (typeof value !== "string") continue;
2101
+ const normalized = value.trim().toLowerCase();
2102
+ if (SAFE_GATEWAY_ERROR_CODES.has(normalized)) return normalized;
2103
+ const embedded = [...SAFE_GATEWAY_ERROR_CODES].find((code) => normalized.includes(code));
2104
+ if (embedded) return embedded;
2105
+ }
2106
+ } catch {
2107
+ }
2108
+ return `Backend request failed (HTTP ${status}).`;
2109
+ }
2110
+ function safeFailureData(responseText) {
2111
+ try {
2112
+ const parsed = JSON.parse(responseText);
2113
+ const metric = (name) => {
2114
+ const value = parsed[name];
2115
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
2116
+ };
2117
+ const billingStatus = typeof parsed.billing_status === "string" && SAFE_BILLING_STATUSES.has(parsed.billing_status) ? parsed.billing_status : void 0;
2118
+ const failureReason = parsed.failure_reason === "generation_failed" || parsed.failure_reason === "authentication_failed" || parsed.failure_reason === "cancelled_by_user" ? parsed.failure_reason : void 0;
2119
+ const jobStatus = typeof parsed.status === "string" && SAFE_JOB_STATUSES.has(parsed.status) ? parsed.status : void 0;
2120
+ const safe = {
2121
+ ...jobStatus ? { status: jobStatus } : {},
2122
+ ...metric("credits_reserved") !== void 0 ? { credits_reserved: metric("credits_reserved") } : {},
2123
+ ...metric("credits_charged") !== void 0 ? { credits_charged: metric("credits_charged") } : {},
2124
+ ...metric("credits_refunded") !== void 0 ? { credits_refunded: metric("credits_refunded") } : {},
2125
+ ...billingStatus ? { billing_status: billingStatus } : {},
2126
+ ...failureReason ? { failure_reason: failureReason } : {}
2127
+ };
2128
+ return Object.keys(safe).length > 0 ? safe : null;
2129
+ } catch {
2130
+ return null;
2131
+ }
2132
+ }
1878
2133
  function getApiKeyOrNull() {
1879
2134
  const envKey = process.env.SOCIALNEURON_API_KEY;
1880
2135
  if (envKey && envKey.trim().length) return envKey.trim();
@@ -1945,33 +2200,28 @@ async function callEdgeFunction(functionName, body, options) {
1945
2200
  clearTimeout(timer);
1946
2201
  const responseText = await response.text();
1947
2202
  if (!response.ok) {
1948
- let errorMessage;
1949
- try {
1950
- const errorJson = JSON.parse(responseText);
1951
- errorMessage = errorJson.error || errorJson.message || responseText;
1952
- } catch {
1953
- errorMessage = responseText || `HTTP ${response.status}`;
1954
- }
2203
+ const errorCode = safeGatewayError(responseText, response.status);
2204
+ const failureData = safeFailureData(responseText);
1955
2205
  if (response.status === 401) {
1956
2206
  return {
1957
- data: null,
2207
+ data: failureData,
1958
2208
  error: `Authentication failed (HTTP 401). Run 'npx @socialneuron/mcp-server login' to re-authenticate.`
1959
2209
  };
1960
2210
  }
1961
2211
  if (response.status === 403) {
1962
2212
  return {
1963
- data: null,
1964
- error: `Forbidden (HTTP 403): ${errorMessage}. This action isn't permitted for your account, plan, or scope \u2014 your connection is still valid.`
2213
+ data: failureData,
2214
+ error: `Forbidden (HTTP 403): ${errorCode}. This action isn't permitted for your account, plan, or scope \u2014 your connection is still valid.`
1965
2215
  };
1966
2216
  }
1967
2217
  if (response.status === 429) {
1968
2218
  const retryAfter = response.headers.get("retry-after") || "60";
1969
2219
  return {
1970
- data: null,
2220
+ data: failureData,
1971
2221
  error: `Rate limit exceeded (HTTP 429). Wait ${retryAfter}s before retrying. Reduce request frequency or upgrade your plan.`
1972
2222
  };
1973
2223
  }
1974
- return { data: null, error: errorMessage };
2224
+ return { data: failureData, error: errorCode };
1975
2225
  }
1976
2226
  try {
1977
2227
  const data = JSON.parse(responseText);
@@ -1987,15 +2237,48 @@ async function callEdgeFunction(functionName, body, options) {
1987
2237
  error: `Edge Function '${functionName}' timed out after ${timeoutMs}ms`
1988
2238
  };
1989
2239
  }
1990
- const message = err instanceof Error ? err.message : String(err);
1991
- return { data: null, error: message };
2240
+ return { data: null, error: "Network request failed. Please retry." };
1992
2241
  }
1993
2242
  }
2243
+ var SAFE_GATEWAY_ERROR_CODES, SAFE_BILLING_STATUSES, SAFE_JOB_STATUSES;
1994
2244
  var init_edge_function = __esm({
1995
2245
  "src/lib/edge-function.ts"() {
1996
2246
  "use strict";
1997
2247
  init_supabase();
1998
2248
  init_request_context();
2249
+ SAFE_GATEWAY_ERROR_CODES = /* @__PURE__ */ new Set([
2250
+ "daily_limit_reached",
2251
+ "insufficient_credits",
2252
+ "project_scope_mismatch",
2253
+ "schedule_conflict",
2254
+ "post_not_found",
2255
+ "post_not_reschedulable",
2256
+ "post_in_progress",
2257
+ "not_cancellable",
2258
+ "publishing_in_progress",
2259
+ "plan_upgrade_required",
2260
+ "rate_limited",
2261
+ "validation_error",
2262
+ "permission_denied",
2263
+ "not_found"
2264
+ ]);
2265
+ SAFE_BILLING_STATUSES = /* @__PURE__ */ new Set([
2266
+ "reserved",
2267
+ "charged",
2268
+ "refunded",
2269
+ "failed_no_charge",
2270
+ "refund_pending",
2271
+ "not_charged"
2272
+ ]);
2273
+ SAFE_JOB_STATUSES = /* @__PURE__ */ new Set([
2274
+ "queued",
2275
+ "pending",
2276
+ "processing",
2277
+ "completed",
2278
+ "failed",
2279
+ "cancelled",
2280
+ "canceled"
2281
+ ]);
1999
2282
  }
2000
2283
  });
2001
2284
 
@@ -2023,6 +2306,7 @@ var CATEGORY_CONFIGS, RateLimiter, limiters;
2023
2306
  var init_rate_limit = __esm({
2024
2307
  "src/lib/rate-limit.ts"() {
2025
2308
  "use strict";
2309
+ init_scopes();
2026
2310
  CATEGORY_CONFIGS = {
2027
2311
  posting: { maxTokens: 30, refillRate: 30 / 60 },
2028
2312
  // 30 req/min — publish/schedule/comment
@@ -2113,8 +2397,8 @@ function registerIdeationTools(server2) {
2113
2397
  brand_voice: z.string().max(500).optional().describe(
2114
2398
  '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.'
2115
2399
  ),
2116
- model: z.enum(["gemini-2.0-flash", "gemini-2.5-flash", "gemini-2.5-pro"]).optional().describe(
2117
- "AI model to use. Defaults to gemini-2.5-flash. Use gemini-2.5-pro for highest quality."
2400
+ model: z.enum(["gemini-2.5-flash", "gemini-2.5-pro"]).optional().describe(
2401
+ "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."
2118
2402
  ),
2119
2403
  project_id: z.string().uuid().optional().describe(
2120
2404
  "Project ID to auto-load brand profile and performance context for prompt enrichment."
@@ -2123,7 +2407,7 @@ function registerIdeationTools(server2) {
2123
2407
  async ({ prompt: prompt2, content_type, platform: platform3, brand_voice, model, project_id }) => {
2124
2408
  try {
2125
2409
  const userId = await getDefaultUserId();
2126
- const rl = checkRateLimit("posting", userId);
2410
+ const rl = checkRateLimit("generation", `generate_content:${userId}`);
2127
2411
  if (!rl.allowed) {
2128
2412
  return {
2129
2413
  content: [
@@ -2380,7 +2664,7 @@ Content Type: ${content_type}`;
2380
2664
  async ({ content, source_platform, target_platform, brand_voice, project_id }) => {
2381
2665
  try {
2382
2666
  const userId = await getDefaultUserId();
2383
- const rl = checkRateLimit("posting", userId);
2667
+ const rl = checkRateLimit("generation", `adapt_content:${userId}`);
2384
2668
  if (!rl.allowed) {
2385
2669
  return {
2386
2670
  content: [
@@ -2492,11 +2776,12 @@ function buildCheckStatusPayload(job, liveStatus) {
2492
2776
  const progress = liveStatus?.progress ?? null;
2493
2777
  const resultUrl = liveStatus?.resultUrl ?? job.result_url ?? null;
2494
2778
  const r2Key = resultUrl && !resultUrl.startsWith("http") ? resultUrl : null;
2495
- const errorMessage = liveStatus?.error ?? job.error_message ?? null;
2779
+ const rawError = liveStatus?.error ?? job.error_message ?? null;
2780
+ const errorMessage = rawError && status === "failed" ? "Generation failed. Retry or choose another model." : rawError && (status === "cancelled" || status === "canceled") ? "Cancelled by user." : null;
2496
2781
  const allUrls = job.result_metadata?.all_urls ?? null;
2497
2782
  const modelRequested = job.result_metadata?.model_requested ?? null;
2498
2783
  const modelDelivered = job.result_metadata?.model_delivered ?? null;
2499
- const fallbackReason = job.result_metadata?.fallback_reason ?? null;
2784
+ const fallbackReason = job.result_metadata?.fallback_reason ? "Requested model was unavailable; a fallback model was used." : null;
2500
2785
  return {
2501
2786
  job_id: job.id,
2502
2787
  job_type: job.job_type,
@@ -2508,6 +2793,11 @@ function buildCheckStatusPayload(job, liveStatus) {
2508
2793
  all_urls: allUrls,
2509
2794
  error: errorMessage,
2510
2795
  credits_cost: job.credits_cost,
2796
+ credits_reserved: job.credits_reserved ?? null,
2797
+ credits_charged: job.credits_charged ?? null,
2798
+ credits_refunded: job.credits_refunded ?? null,
2799
+ billing_status: job.billing_status ?? "unknown",
2800
+ failure_reason: job.failure_reason ?? null,
2511
2801
  created_at: job.created_at,
2512
2802
  completed_at: job.completed_at,
2513
2803
  model_requested: modelRequested,
@@ -2609,6 +2899,13 @@ var init_budget = __esm({
2609
2899
 
2610
2900
  // src/tools/content.ts
2611
2901
  import { z as z2 } from "zod";
2902
+ function buildFallbackDisclosureLine(meta) {
2903
+ const requested = meta?.model_requested;
2904
+ const delivered = meta?.model_delivered;
2905
+ if (!requested || !delivered || requested === delivered) return null;
2906
+ const reason = meta?.fallback_reason ? " because the requested model was unavailable" : "";
2907
+ return `Note: requested "${requested}" but delivered "${delivered}"${reason} \u2014 cost never exceeds the requested model's price.`;
2908
+ }
2612
2909
  function asEnvelope2(data) {
2613
2910
  return {
2614
2911
  _meta: {
@@ -2677,7 +2974,7 @@ function registerContentTools(server2) {
2677
2974
  isError: true
2678
2975
  };
2679
2976
  }
2680
- const rateLimit = checkRateLimit("posting", `generate_video:${userId}`);
2977
+ const rateLimit = checkRateLimit("generation", `generate_video:${userId}`);
2681
2978
  if (!rateLimit.allowed) {
2682
2979
  return {
2683
2980
  content: [
@@ -2818,7 +3115,7 @@ function registerContentTools(server2) {
2818
3115
  isError: true
2819
3116
  };
2820
3117
  }
2821
- const rateLimit = checkRateLimit("posting", `generate_image:${userId}`);
3118
+ const rateLimit = checkRateLimit("generation", `generate_image:${userId}`);
2822
3119
  if (!rateLimit.allowed) {
2823
3120
  return {
2824
3121
  content: [
@@ -2958,20 +3255,28 @@ function registerContentTools(server2) {
2958
3255
  }
2959
3256
  );
2960
3257
  if (liveStatus) {
3258
+ const livePayload = buildCheckStatusPayload(job, liveStatus);
2961
3259
  const lines2 = [
2962
3260
  `Job: ${job.id}`,
2963
3261
  `Type: ${job.job_type}`,
2964
3262
  `Model: ${job.model}`,
2965
- `Status: ${liveStatus.status}`,
2966
- `Progress: ${liveStatus.progress}%`
3263
+ `Status: ${livePayload.status}`,
3264
+ `Progress: ${livePayload.progress}%`
2967
3265
  ];
2968
- if (liveStatus.resultUrl) {
2969
- lines2.push(`Result URL: ${liveStatus.resultUrl}`);
3266
+ if (livePayload.result_url) {
3267
+ lines2.push(`Result URL: ${livePayload.result_url}`);
2970
3268
  }
2971
- if (liveStatus.error) {
2972
- lines2.push(`Error: ${liveStatus.error}`);
3269
+ if (livePayload.error) {
3270
+ lines2.push(`Error: ${livePayload.error}`);
2973
3271
  }
3272
+ const fallbackDisclosure2 = buildFallbackDisclosureLine(job.result_metadata);
3273
+ if (fallbackDisclosure2) lines2.push(fallbackDisclosure2);
2974
3274
  lines2.push(`Credits: ${job.credits_cost}`);
3275
+ if (job.billing_status) {
3276
+ lines2.push(
3277
+ `Billing: ${job.billing_status} (charged ${job.credits_charged ?? 0}, refunded ${job.credits_refunded ?? 0})`
3278
+ );
3279
+ }
2975
3280
  lines2.push(`Created: ${job.created_at}`);
2976
3281
  if (format === "json") {
2977
3282
  return {
@@ -2989,7 +3294,7 @@ function registerContentTools(server2) {
2989
3294
  // `job` + `liveStatus`; do not duplicate them here.
2990
3295
  asEnvelope2({
2991
3296
  ...liveStatus,
2992
- ...buildCheckStatusPayload(job, liveStatus)
3297
+ ...livePayload
2993
3298
  }),
2994
3299
  null,
2995
3300
  2
@@ -3032,7 +3337,14 @@ function registerContentTools(server2) {
3032
3337
  if (job.error_message) {
3033
3338
  lines.push(`Error: ${job.error_message}`);
3034
3339
  }
3340
+ const fallbackDisclosure = buildFallbackDisclosureLine(job.result_metadata);
3341
+ if (fallbackDisclosure) lines.push(fallbackDisclosure);
3035
3342
  lines.push(`Credits: ${job.credits_cost}`);
3343
+ if (job.billing_status) {
3344
+ lines.push(
3345
+ `Billing: ${job.billing_status} (charged ${job.credits_charged ?? 0}, refunded ${job.credits_refunded ?? 0})`
3346
+ );
3347
+ }
3036
3348
  lines.push(`Created: ${job.created_at}`);
3037
3349
  if (job.completed_at) {
3038
3350
  lines.push(`Completed: ${job.completed_at}`);
@@ -3262,7 +3574,7 @@ Return ONLY valid JSON in this exact format:
3262
3574
  };
3263
3575
  }
3264
3576
  const rateLimit = checkRateLimit(
3265
- "posting",
3577
+ "generation",
3266
3578
  `generate_voiceover:${userId}`
3267
3579
  );
3268
3580
  if (!rateLimit.allowed) {
@@ -3429,7 +3741,7 @@ Return ONLY valid JSON in this exact format:
3429
3741
  }
3430
3742
  const userId = await getDefaultUserId();
3431
3743
  const rateLimit = checkRateLimit(
3432
- "posting",
3744
+ "generation",
3433
3745
  `generate_carousel:${userId}`
3434
3746
  );
3435
3747
  if (!rateLimit.allowed) {
@@ -3583,9 +3895,6 @@ var init_content = __esm({
3583
3895
  // src/lib/sanitize-error.ts
3584
3896
  function sanitizeError(error) {
3585
3897
  const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
3586
- if (process.env.NODE_ENV !== "production") {
3587
- console.error("[Error]", msg);
3588
- }
3589
3898
  for (const [pattern, userMessage] of ERROR_PATTERNS) {
3590
3899
  if (pattern.test(msg)) {
3591
3900
  return userMessage;
@@ -3799,6 +4108,7 @@ function evaluateQuality(input) {
3799
4108
  const firstLine = caption.split("\n")[0]?.trim() ?? "";
3800
4109
  const hashtags = countHashtags(caption);
3801
4110
  const threshold = Math.min(35, Math.max(0, input.threshold ?? 26));
4111
+ const isShortFormX = platforms.length > 0 && platforms.every((p) => p === "twitter") && caption.length <= 280;
3802
4112
  const blockedTerms = [
3803
4113
  ...(input.brandAvoidPatterns ?? []).map((t) => t.trim()).filter(Boolean),
3804
4114
  ...(input.customBannedTerms ?? []).map((t) => t.trim()).filter(Boolean)
@@ -3808,11 +4118,12 @@ function evaluateQuality(input) {
3808
4118
  if (firstLine.length >= 20 && firstLine.length <= 120) hookScore += 1;
3809
4119
  if (/[!?]/.test(firstLine) || /\b\d+(\.\d+)?\b/.test(firstLine)) hookScore += 1;
3810
4120
  if (/\b(how|why|stop|avoid|build|launch|scale|grow|mistake)\b/i.test(firstLine)) hookScore += 1;
4121
+ if (isShortFormX) hookScore = Math.max(hookScore, 3);
3811
4122
  categories.push({
3812
4123
  name: "Hook Strength",
3813
4124
  score: Math.min(5, hookScore),
3814
4125
  maxScore: 5,
3815
- detail: "First line should create curiosity/value within 120 chars."
4126
+ detail: isShortFormX ? "Short-form X: keyword-hook heuristics neutralized; substance judge arbitrates." : "First line should create curiosity/value within 120 chars."
3816
4127
  });
3817
4128
  let clarityScore = 2;
3818
4129
  if (caption.length >= 80 && caption.length <= 1200) clarityScore += 2;
@@ -3838,7 +4149,8 @@ function evaluateQuality(input) {
3838
4149
  detail: "Length, title, and hashtag usage should match target platforms."
3839
4150
  });
3840
4151
  let brandScore = 3;
3841
- const brandKeyword = input.brandKeyword ?? process.env.SOCIALNEURON_BRAND_KEYWORD?.trim();
4152
+ const rawBrandKeyword = input.brandKeyword ?? process.env.SOCIALNEURON_BRAND_KEYWORD?.trim();
4153
+ const brandKeyword = rawBrandKeyword ? rawBrandKeyword.slice(0, 80).replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : null;
3842
4154
  if (brandKeyword && new RegExp("\\b" + brandKeyword + "\\b", "i").test(title + " " + caption))
3843
4155
  brandScore += 1;
3844
4156
  if (!/\b(you|your|customer|audience)\b/i.test(caption)) brandScore -= 1;
@@ -3849,11 +4161,12 @@ function evaluateQuality(input) {
3849
4161
  brandScore -= Math.min(2, matched.length);
3850
4162
  }
3851
4163
  }
4164
+ if (isShortFormX) brandScore = Math.max(brandScore, 3);
3852
4165
  categories.push({
3853
4166
  name: "Brand Alignment",
3854
4167
  score: Math.max(0, Math.min(5, brandScore)),
3855
4168
  maxScore: 5,
3856
- detail: "Voice should match brand context and audience focus."
4169
+ detail: isShortFormX ? "Short-form X: second-person-address heuristic neutralized; blocked terms still enforced." : "Voice should match brand context and audience focus."
3857
4170
  });
3858
4171
  let noveltyScore = 2;
3859
4172
  if (/\b(case study|framework|workflow|playbook|breakdown|behind the scenes)\b/i.test(caption))
@@ -3864,21 +4177,23 @@ function evaluateQuality(input) {
3864
4177
  const matched = blockedTerms.filter((term) => lowerCombined.includes(term.toLowerCase()));
3865
4178
  if (matched.length > 0) noveltyScore -= Math.min(2, matched.length);
3866
4179
  }
4180
+ if (isShortFormX) noveltyScore = Math.max(noveltyScore, 3);
3867
4181
  categories.push({
3868
4182
  name: "Novelty",
3869
4183
  score: Math.max(0, Math.min(5, noveltyScore)),
3870
4184
  maxScore: 5,
3871
- detail: "Avoid generic phrasing; include distinct angle."
4185
+ detail: isShortFormX ? "Short-form X: keyword-novelty neutralized; substance judge arbitrates." : "Avoid generic phrasing; include distinct angle."
3872
4186
  });
3873
4187
  let ctaScore = 2;
3874
4188
  if (/\b(comment|reply|share|save|follow|subscribe|click|try|book|download)\b/i.test(caption))
3875
4189
  ctaScore += 2;
3876
4190
  if (/\?$/.test(firstLine)) ctaScore += 1;
4191
+ if (isShortFormX) ctaScore = Math.max(ctaScore, 3);
3877
4192
  categories.push({
3878
4193
  name: "CTA Strength",
3879
4194
  score: Math.min(5, ctaScore),
3880
4195
  maxScore: 5,
3881
- detail: "Should include a clear next action."
4196
+ detail: isShortFormX ? "Short-form X: explicit CTA optional; native posts may close without one." : "Should include a clear next action."
3882
4197
  });
3883
4198
  let safetyScore = 5;
3884
4199
  if (/\b(guarantee|guaranteed|no risk|risk-free|always works|100%)\b/i.test(caption))
@@ -3916,7 +4231,7 @@ var init_quality = __esm({
3916
4231
 
3917
4232
  // src/tools/distribution.ts
3918
4233
  import { z as z3 } from "zod";
3919
- import { createHash as createHash2 } from "node:crypto";
4234
+ import { createHash } from "node:crypto";
3920
4235
  function snakeToCamel(obj) {
3921
4236
  const result = {};
3922
4237
  for (const [key, value] of Object.entries(obj)) {
@@ -3942,6 +4257,87 @@ function asEnvelope3(data) {
3942
4257
  data
3943
4258
  };
3944
4259
  }
4260
+ function publicConnectedAccount(value) {
4261
+ if (!value || typeof value !== "object") return null;
4262
+ const row = value;
4263
+ if (typeof row.id !== "string" || typeof row.platform !== "string" || typeof row.status !== "string" || typeof row.created_at !== "string") {
4264
+ return null;
4265
+ }
4266
+ return {
4267
+ id: row.id,
4268
+ platform: row.platform,
4269
+ status: row.status,
4270
+ ...typeof row.effective_status === "string" ? { effective_status: row.effective_status } : {},
4271
+ username: typeof row.username === "string" ? row.username : null,
4272
+ created_at: row.created_at,
4273
+ ...typeof row.updated_at === "string" || row.updated_at === null ? { updated_at: row.updated_at } : {},
4274
+ ...typeof row.expires_at === "string" || row.expires_at === null ? { expires_at: row.expires_at } : {},
4275
+ ...typeof row.has_refresh_token === "boolean" ? { has_refresh_token: row.has_refresh_token } : {},
4276
+ ...typeof row.project_id === "string" || row.project_id === null ? { project_id: row.project_id } : {}
4277
+ };
4278
+ }
4279
+ function publicPostRecord(value) {
4280
+ if (!value || typeof value !== "object") return null;
4281
+ const row = value;
4282
+ if (typeof row.id !== "string" || typeof row.platform !== "string" || typeof row.status !== "string" || typeof row.created_at !== "string") {
4283
+ return null;
4284
+ }
4285
+ const nullableString = (field) => typeof row[field] === "string" ? row[field] : null;
4286
+ return {
4287
+ id: row.id,
4288
+ platform: row.platform,
4289
+ status: row.status,
4290
+ title: nullableString("title"),
4291
+ external_post_id: nullableString("external_post_id"),
4292
+ published_at: nullableString("published_at"),
4293
+ scheduled_at: nullableString("scheduled_at"),
4294
+ created_at: row.created_at
4295
+ };
4296
+ }
4297
+ function mediaTypeFromPath(value) {
4298
+ try {
4299
+ const pathname = value.startsWith("http") ? new URL(value).pathname : value;
4300
+ const extension = pathname.split(".").pop()?.toLowerCase();
4301
+ if (!extension) return null;
4302
+ if (VIDEO_FILE_EXTENSIONS.has(extension)) return "VIDEO";
4303
+ if (IMAGE_FILE_EXTENSIONS.has(extension)) return "IMAGE";
4304
+ } catch {
4305
+ return null;
4306
+ }
4307
+ return null;
4308
+ }
4309
+ function inferScheduleMediaType(explicit, singleCandidates, collectionCandidates) {
4310
+ if (explicit) return explicit;
4311
+ const populatedCollection = collectionCandidates.find(
4312
+ (values) => Array.isArray(values) && values.length > 0
4313
+ );
4314
+ if (populatedCollection) {
4315
+ if (populatedCollection.length > 1) return "CAROUSEL_ALBUM";
4316
+ for (const values of collectionCandidates) {
4317
+ if (!values || values.length !== 1) continue;
4318
+ const inferred = mediaTypeFromPath(values[0]);
4319
+ if (inferred) return inferred;
4320
+ }
4321
+ return null;
4322
+ }
4323
+ for (const value of singleCandidates) {
4324
+ if (typeof value !== "string" || value.length === 0) continue;
4325
+ const inferred = mediaTypeFromPath(value);
4326
+ if (inferred) return inferred;
4327
+ }
4328
+ return null;
4329
+ }
4330
+ async function validatePublishMediaUrl(url) {
4331
+ try {
4332
+ if (new URL(url).protocol !== "https:") {
4333
+ return "Media URLs must use HTTPS.";
4334
+ }
4335
+ } catch {
4336
+ return "Media URL is invalid.";
4337
+ }
4338
+ const check = await validateUrlForSSRF(url);
4339
+ return check.isValid ? null : check.error || "Media URL failed safety validation.";
4340
+ }
3945
4341
  function accountEffectiveStatus(account) {
3946
4342
  return account.effective_status || account.status;
3947
4343
  }
@@ -3960,22 +4356,11 @@ function requestedAccountIdForPlatform(accountId, accountIds, platform3) {
3960
4356
  if (!accountIds) return void 0;
3961
4357
  return accountIds[platform3] || accountIds[platform3.toLowerCase()];
3962
4358
  }
3963
- function isAlreadyR2Signed(url) {
3964
- try {
3965
- const u = new URL(url);
3966
- return u.searchParams.has("X-Amz-Signature");
3967
- } catch {
3968
- return false;
3969
- }
3970
- }
3971
4359
  async function rehostExternalUrl(mediaUrl, projectId) {
3972
4360
  const ssrf = await validateUrlForSSRF(mediaUrl);
3973
4361
  if (!ssrf.isValid) {
3974
4362
  return { error: ssrf.error ?? "URL rejected by SSRF check" };
3975
4363
  }
3976
- if (isAlreadyR2Signed(mediaUrl)) {
3977
- return { signedUrl: mediaUrl, r2Key: "" };
3978
- }
3979
4364
  const { data, error } = await callEdgeFunction(
3980
4365
  "upload-to-r2",
3981
4366
  { url: ssrf.sanitizedUrl ?? mediaUrl, projectId },
@@ -3994,19 +4379,19 @@ function registerDistributionTools(server2) {
3994
4379
  media_url: z3.string().optional().describe(
3995
4380
  "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."
3996
4381
  ),
3997
- media_urls: z3.array(z3.string()).optional().describe(
4382
+ media_urls: z3.array(z3.string().url()).min(2).max(10).optional().describe(
3998
4383
  "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."
3999
4384
  ),
4000
4385
  r2_key: z3.string().optional().describe(
4001
4386
  "R2 object key from upload_media. Signed on demand at post time \u2014 survives scheduling delays. Alternative to media_url."
4002
4387
  ),
4003
- r2_keys: z3.array(z3.string()).optional().describe(
4388
+ r2_keys: z3.array(z3.string()).min(2).max(10).optional().describe(
4004
4389
  "Array of R2 object keys for carousel posts. Each is signed on demand. Alternative to media_urls."
4005
4390
  ),
4006
4391
  job_id: z3.string().optional().describe(
4007
4392
  "Async job ID from generate_image/generate_video. Resolves the completed job's R2 key and signs it. Alternative to media_url/r2_key."
4008
4393
  ),
4009
- job_ids: z3.array(z3.string()).optional().describe(
4394
+ job_ids: z3.array(z3.string()).min(2).max(10).optional().describe(
4010
4395
  "Array of async job IDs for carousel posts. Each resolved to its R2 key. Alternative to media_urls/r2_keys."
4011
4396
  ),
4012
4397
  platform_metadata: z3.object({
@@ -4036,7 +4421,10 @@ function registerDistributionTools(server2) {
4036
4421
  category_id: z3.string().optional(),
4037
4422
  tags: z3.array(z3.string()).optional(),
4038
4423
  made_for_kids: z3.boolean().optional(),
4039
- notify_subscribers: z3.boolean().optional()
4424
+ notify_subscribers: z3.boolean().optional(),
4425
+ contains_synthetic_media: z3.boolean().optional().describe(
4426
+ "YouTube altered-or-synthetic-content disclosure. Defaults to true for MCP posts; set false explicitly for verified non-AI media."
4427
+ )
4040
4428
  }).optional(),
4041
4429
  facebook: z3.object({
4042
4430
  page_id: z3.string().optional().describe("Facebook Page ID to post to."),
@@ -4119,14 +4507,8 @@ function registerDistributionTools(server2) {
4119
4507
  auto_rehost: z3.boolean().optional().describe(
4120
4508
  "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."
4121
4509
  ),
4122
- visual_gate_result: z3.object({ passed: z3.boolean() }).passthrough().optional().describe(
4123
- "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."
4124
- ),
4125
- origin: z3.enum(["human", "hermes", "user"]).optional().describe(
4126
- "Originator lineage for the post. Marks who scheduled it; use the agent value when calling from an autonomous workflow, otherwise default 'human'."
4127
- ),
4128
- hermes_run_id: z3.string().optional().describe(
4129
- "Optional agent run identifier for traceability between a draft and the run that produced it."
4510
+ idempotency_key: z3.string().regex(/^[a-zA-Z0-9_-]{8,128}$/).optional().describe(
4511
+ "Stable 8-128 character retry key (letters, numbers, underscore, hyphen). Reuse the same key when retrying the same publish request to prevent duplicate posts."
4130
4512
  )
4131
4513
  },
4132
4514
  async ({
@@ -4149,9 +4531,7 @@ function registerDistributionTools(server2) {
4149
4531
  platform_metadata,
4150
4532
  account_id,
4151
4533
  account_ids,
4152
- visual_gate_result,
4153
- origin,
4154
- hermes_run_id
4534
+ idempotency_key
4155
4535
  }) => {
4156
4536
  const format = response_format ?? "text";
4157
4537
  if ((!caption || caption.trim().length === 0) && (!title || title.trim().length === 0)) {
@@ -4180,6 +4560,8 @@ function registerDistributionTools(server2) {
4180
4560
  }
4181
4561
  let resolvedMediaUrl = media_url;
4182
4562
  let resolvedMediaUrls = media_urls;
4563
+ let resolvedMediaUrlIsTrustedR2 = false;
4564
+ let resolvedMediaUrlsAreTrustedR2 = (media_urls ?? []).map(() => false);
4183
4565
  const signR2Key = async (key) => {
4184
4566
  const cleanKey = key.startsWith("r2://") ? key.slice(5) : key;
4185
4567
  const { data: signData } = await callEdgeFunction(
@@ -4197,8 +4579,11 @@ function registerDistributionTools(server2) {
4197
4579
  );
4198
4580
  const resultUrl = jobData?.job?.result_url;
4199
4581
  if (!resultUrl) return null;
4200
- if (!resultUrl.startsWith("http")) return signR2Key(resultUrl);
4201
- return resultUrl;
4582
+ if (!resultUrl.startsWith("http")) {
4583
+ const signed = await signR2Key(resultUrl);
4584
+ return signed ? { url: signed, trustedR2: true } : null;
4585
+ }
4586
+ return { url: resultUrl, trustedR2: false };
4202
4587
  };
4203
4588
  try {
4204
4589
  if (r2_key && !resolvedMediaUrl) {
@@ -4215,6 +4600,7 @@ function registerDistributionTools(server2) {
4215
4600
  };
4216
4601
  }
4217
4602
  resolvedMediaUrl = signed;
4603
+ resolvedMediaUrlIsTrustedR2 = true;
4218
4604
  } else if (job_id && !resolvedMediaUrl && !r2_key) {
4219
4605
  const resolved = await resolveJobId(job_id);
4220
4606
  if (!resolved) {
@@ -4228,7 +4614,8 @@ function registerDistributionTools(server2) {
4228
4614
  isError: true
4229
4615
  };
4230
4616
  }
4231
- resolvedMediaUrl = resolved;
4617
+ resolvedMediaUrl = resolved.url;
4618
+ resolvedMediaUrlIsTrustedR2 = resolved.trustedR2;
4232
4619
  }
4233
4620
  if (r2_keys && r2_keys.length > 0 && !resolvedMediaUrls) {
4234
4621
  const signed = await Promise.all(r2_keys.map(signR2Key));
@@ -4245,6 +4632,7 @@ function registerDistributionTools(server2) {
4245
4632
  };
4246
4633
  }
4247
4634
  resolvedMediaUrls = signed;
4635
+ resolvedMediaUrlsAreTrustedR2 = signed.map(() => true);
4248
4636
  } else if (job_ids && job_ids.length > 0 && !resolvedMediaUrls && !r2_keys) {
4249
4637
  const resolved = await Promise.all(job_ids.map(resolveJobId));
4250
4638
  const failIdx = resolved.findIndex((r) => !r);
@@ -4259,10 +4647,12 @@ function registerDistributionTools(server2) {
4259
4647
  isError: true
4260
4648
  };
4261
4649
  }
4262
- resolvedMediaUrls = resolved;
4650
+ const resolvedJobs = resolved;
4651
+ resolvedMediaUrls = resolvedJobs.map((item) => item.url);
4652
+ resolvedMediaUrlsAreTrustedR2 = resolvedJobs.map((item) => item.trustedR2);
4263
4653
  }
4264
4654
  const shouldRehost = auto_rehost !== false;
4265
- if (shouldRehost && resolvedMediaUrl) {
4655
+ if (shouldRehost && resolvedMediaUrl && !resolvedMediaUrlIsTrustedR2) {
4266
4656
  const rehost = await rehostExternalUrl(resolvedMediaUrl, project_id);
4267
4657
  if ("error" in rehost) {
4268
4658
  return {
@@ -4276,10 +4666,13 @@ function registerDistributionTools(server2) {
4276
4666
  };
4277
4667
  }
4278
4668
  resolvedMediaUrl = rehost.signedUrl;
4669
+ resolvedMediaUrlIsTrustedR2 = true;
4279
4670
  }
4280
4671
  if (shouldRehost && resolvedMediaUrls && resolvedMediaUrls.length > 0) {
4281
4672
  const rehosted = await Promise.all(
4282
- resolvedMediaUrls.map((u) => rehostExternalUrl(u, project_id))
4673
+ resolvedMediaUrls.map(
4674
+ (u, index) => resolvedMediaUrlsAreTrustedR2[index] ? Promise.resolve({ signedUrl: u, r2Key: "" }) : rehostExternalUrl(u, project_id)
4675
+ )
4283
4676
  );
4284
4677
  const failIdx = rehosted.findIndex((r) => "error" in r);
4285
4678
  if (failIdx !== -1) {
@@ -4295,6 +4688,7 @@ function registerDistributionTools(server2) {
4295
4688
  };
4296
4689
  }
4297
4690
  resolvedMediaUrls = rehosted.map((r) => r.signedUrl);
4691
+ resolvedMediaUrlsAreTrustedR2 = resolvedMediaUrls.map(() => true);
4298
4692
  }
4299
4693
  } catch (resolveErr) {
4300
4694
  return {
@@ -4307,6 +4701,42 @@ function registerDistributionTools(server2) {
4307
4701
  isError: true
4308
4702
  };
4309
4703
  }
4704
+ const hasResolvedMedia = Boolean(
4705
+ resolvedMediaUrl || resolvedMediaUrls && resolvedMediaUrls.length > 0
4706
+ );
4707
+ const resolvedMediaType = inferScheduleMediaType(
4708
+ media_type,
4709
+ [resolvedMediaUrl, r2_key],
4710
+ [resolvedMediaUrls, r2_keys]
4711
+ );
4712
+ if (hasResolvedMedia && !resolvedMediaType) {
4713
+ return {
4714
+ content: [
4715
+ {
4716
+ type: "text",
4717
+ text: "media_type is required when the media format cannot be inferred from a file extension. Set IMAGE, VIDEO, or CAROUSEL_ALBUM explicitly."
4718
+ }
4719
+ ],
4720
+ isError: true
4721
+ };
4722
+ }
4723
+ const finalUrls = [resolvedMediaUrl, ...resolvedMediaUrls ?? []].filter(
4724
+ (value) => typeof value === "string" && value.length > 0
4725
+ );
4726
+ for (const url of finalUrls) {
4727
+ const validationError = await validatePublishMediaUrl(url);
4728
+ if (validationError) {
4729
+ return {
4730
+ content: [
4731
+ {
4732
+ type: "text",
4733
+ text: `Media URL blocked: ${validationError}`
4734
+ }
4735
+ ],
4736
+ isError: true
4737
+ };
4738
+ }
4739
+ }
4310
4740
  const normalizedPlatforms = platforms.map(
4311
4741
  (p) => PLATFORM_CASE_MAP[p.toLowerCase()] || p
4312
4742
  );
@@ -4432,12 +4862,32 @@ Created with Social Neuron`;
4432
4862
  };
4433
4863
  return true;
4434
4864
  })();
4865
+ const defaultAiDisclosure = (platformKey, field) => {
4866
+ const existing = normalizedPlatformMetadata?.[platformKey];
4867
+ if (existing && existing[field] !== void 0) return;
4868
+ normalizedPlatformMetadata = {
4869
+ ...normalizedPlatformMetadata ?? {},
4870
+ [platformKey]: {
4871
+ ...existing ?? {},
4872
+ [field]: true
4873
+ }
4874
+ };
4875
+ };
4876
+ if (normalizedPlatforms.includes("TikTok")) {
4877
+ defaultAiDisclosure("tiktok", "is_ai_generated");
4878
+ }
4879
+ if (normalizedPlatforms.includes("Instagram")) {
4880
+ defaultAiDisclosure("instagram", "is_ai_generated");
4881
+ }
4882
+ if (normalizedPlatforms.includes("YouTube")) {
4883
+ defaultAiDisclosure("youtube", "contains_synthetic_media");
4884
+ }
4435
4885
  const { data, error } = await callEdgeFunction(
4436
4886
  "schedule-post",
4437
4887
  {
4438
4888
  mediaUrl: resolvedMediaUrl,
4439
4889
  mediaUrls: resolvedMediaUrls,
4440
- mediaType: media_type,
4890
+ mediaType: resolvedMediaType ?? void 0,
4441
4891
  caption: finalCaption,
4442
4892
  platforms: normalizedPlatforms,
4443
4893
  title,
@@ -4450,14 +4900,10 @@ Created with Social Neuron`;
4450
4900
  normalizedPlatformMetadata
4451
4901
  )
4452
4902
  } : {},
4453
- // Forward visual gate verdict + source so the EF can enforce on media posts.
4454
- ...visual_gate_result ? { visualGateResult: visual_gate_result } : {},
4455
- visualGateSource: "mcp",
4456
- // Originator lineage (Hermes integration, 2026-05-22). EF validates origin
4457
- // against the posts.origin CHECK constraint and persists hermesRunId when
4458
- // origin === 'hermes'.
4459
- ...origin ? { origin } : {},
4460
- ...hermes_run_id ? { hermesRunId: hermes_run_id } : {}
4903
+ ...idempotency_key ? { idempotencyKey: idempotency_key } : {}
4904
+ // Attribution is assigned by the authenticated gateway. Visual QA
4905
+ // attestations are server-produced evidence and are deliberately not
4906
+ // accepted from an MCP caller.
4461
4907
  },
4462
4908
  { timeoutMs: 3e4 }
4463
4909
  );
@@ -4523,6 +4969,99 @@ Created with Social Neuron`;
4523
4969
  };
4524
4970
  }
4525
4971
  );
4972
+ server2.tool(
4973
+ "reschedule_post",
4974
+ "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.",
4975
+ {
4976
+ post_id: z3.string().uuid().describe("Post ID returned by list_recent_posts."),
4977
+ project_id: z3.string().uuid().optional().describe(
4978
+ "Brand/project ID that owns the post. Defaults to the authenticated key's project or the account default."
4979
+ ),
4980
+ scheduled_at: z3.string().datetime({ offset: true }).describe("New future publish time as an ISO 8601 datetime with timezone."),
4981
+ expected_scheduled_at: z3.string().datetime({ offset: true }).optional().describe(
4982
+ "Optional current schedule timestamp. If it changed since you read it, the update is rejected instead of silently overwriting it."
4983
+ ),
4984
+ response_format: z3.enum(["text", "json"]).default("text")
4985
+ },
4986
+ async ({
4987
+ post_id,
4988
+ project_id,
4989
+ scheduled_at,
4990
+ expected_scheduled_at,
4991
+ response_format
4992
+ }) => {
4993
+ const next = new Date(scheduled_at);
4994
+ if (!Number.isFinite(next.getTime()) || next.getTime() <= Date.now()) {
4995
+ return {
4996
+ content: [
4997
+ {
4998
+ type: "text",
4999
+ text: "scheduled_at must be a valid future ISO datetime with timezone."
5000
+ }
5001
+ ],
5002
+ isError: true
5003
+ };
5004
+ }
5005
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
5006
+ if (!resolvedProjectId) {
5007
+ return {
5008
+ content: [
5009
+ {
5010
+ type: "text",
5011
+ text: "No project_id was provided and no default project is configured."
5012
+ }
5013
+ ],
5014
+ isError: true
5015
+ };
5016
+ }
5017
+ const userId = await getDefaultUserId();
5018
+ const rateLimit = checkRateLimit("posting", `reschedule_post:${userId}`);
5019
+ if (!rateLimit.allowed) {
5020
+ return {
5021
+ content: [
5022
+ {
5023
+ type: "text",
5024
+ text: `Rate limit exceeded. Retry in ~${rateLimit.retryAfter}s.`
5025
+ }
5026
+ ],
5027
+ isError: true
5028
+ };
5029
+ }
5030
+ const { data: result, error } = await callEdgeFunction("mcp-data", {
5031
+ action: "reschedule-scheduled-post",
5032
+ post_id,
5033
+ projectId: resolvedProjectId,
5034
+ project_id: resolvedProjectId,
5035
+ scheduled_at: next.toISOString(),
5036
+ ...expected_scheduled_at ? { expected_scheduled_at: new Date(expected_scheduled_at).toISOString() } : {}
5037
+ });
5038
+ if (error || !result?.success) {
5039
+ const code = result?.error ?? error ?? "reschedule_failed";
5040
+ 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.";
5041
+ return {
5042
+ content: [{ type: "text", text: recovery }],
5043
+ isError: true
5044
+ };
5045
+ }
5046
+ const publicResult = {
5047
+ success: true,
5048
+ post_id: result.post_id ?? post_id,
5049
+ project_id: result.project_id ?? resolvedProjectId,
5050
+ previous_scheduled_at: result.previous_scheduled_at ?? null,
5051
+ scheduled_at: result.scheduled_at ?? next.toISOString()
5052
+ };
5053
+ const structuredContent = asEnvelope3(publicResult);
5054
+ return {
5055
+ structuredContent,
5056
+ content: [
5057
+ {
5058
+ type: "text",
5059
+ text: response_format === "json" ? JSON.stringify(structuredContent, null, 2) : `Post ${publicResult.post_id} rescheduled to ${publicResult.scheduled_at}.`
5060
+ }
5061
+ ]
5062
+ };
5063
+ }
5064
+ );
4526
5065
  server2.tool(
4527
5066
  "list_connected_accounts",
4528
5067
  "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.",
@@ -4551,14 +5090,16 @@ Created with Social Neuron`;
4551
5090
  isError: true
4552
5091
  };
4553
5092
  }
4554
- const accounts = result.accounts ?? [];
5093
+ const accounts = (result.accounts ?? []).map(publicConnectedAccount).filter((account) => account !== null);
4555
5094
  if (accounts.length === 0) {
4556
5095
  if (format === "json") {
5096
+ const structuredContent = asEnvelope3({ accounts: [] });
4557
5097
  return {
5098
+ structuredContent,
4558
5099
  content: [
4559
5100
  {
4560
5101
  type: "text",
4561
- text: JSON.stringify(asEnvelope3({ accounts: [] }), null, 2)
5102
+ text: JSON.stringify(structuredContent, null, 2)
4562
5103
  }
4563
5104
  ]
4564
5105
  };
@@ -4586,11 +5127,13 @@ Created with Social Neuron`;
4586
5127
  );
4587
5128
  }
4588
5129
  if (format === "json") {
5130
+ const structuredContent = asEnvelope3({ accounts });
4589
5131
  return {
5132
+ structuredContent,
4590
5133
  content: [
4591
5134
  {
4592
5135
  type: "text",
4593
- text: JSON.stringify(asEnvelope3({ accounts }), null, 2)
5136
+ text: JSON.stringify(structuredContent, null, 2)
4594
5137
  }
4595
5138
  ]
4596
5139
  };
@@ -4604,6 +5147,9 @@ Created with Social Neuron`;
4604
5147
  "list_recent_posts",
4605
5148
  "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.",
4606
5149
  {
5150
+ project_id: z3.string().uuid().optional().describe(
5151
+ "Brand/project ID to scope posts. Defaults to the authenticated key's project or the account default."
5152
+ ),
4607
5153
  platform: z3.enum([
4608
5154
  "youtube",
4609
5155
  "tiktok",
@@ -4619,13 +5165,27 @@ Created with Social Neuron`;
4619
5165
  limit: z3.number().min(1).max(50).optional().describe("Maximum number of posts to return. Defaults to 20."),
4620
5166
  response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
4621
5167
  },
4622
- async ({ platform: platform3, status, days, limit, response_format }) => {
5168
+ async ({ project_id, platform: platform3, status, days, limit, response_format }) => {
4623
5169
  const format = response_format ?? "text";
4624
5170
  const lookbackDays = days ?? 7;
5171
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
5172
+ if (!resolvedProjectId) {
5173
+ return {
5174
+ content: [
5175
+ {
5176
+ type: "text",
5177
+ text: "No project_id was provided and no default project is configured."
5178
+ }
5179
+ ],
5180
+ isError: true
5181
+ };
5182
+ }
4625
5183
  const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
4626
5184
  action: "recent-posts",
4627
5185
  days: lookbackDays,
4628
5186
  limit: limit ?? 20,
5187
+ projectId: resolvedProjectId,
5188
+ project_id: resolvedProjectId,
4629
5189
  ...platform3 ? { platform: platform3 } : {},
4630
5190
  ...status ? { status } : {}
4631
5191
  });
@@ -4640,7 +5200,7 @@ Created with Social Neuron`;
4640
5200
  isError: true
4641
5201
  };
4642
5202
  }
4643
- const rows = result.posts ?? [];
5203
+ const rows = (result.posts ?? []).map(publicPostRecord).filter((post) => post !== null);
4644
5204
  if (rows.length === 0) {
4645
5205
  if (format === "json") {
4646
5206
  const structuredContent2 = asEnvelope3({ posts: [] });
@@ -4715,8 +5275,11 @@ Created with Social Neuron`;
4715
5275
  };
4716
5276
  server2.tool(
4717
5277
  "find_next_slots",
4718
- "Find optimal posting time slots based on best posting times and existing schedule. Returns non-conflicting slots sorted by engagement score.",
5278
+ "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.",
4719
5279
  {
5280
+ project_id: z3.string().uuid().optional().describe(
5281
+ "Brand/project ID used for conflict detection. Defaults to the authenticated key's project or the account default."
5282
+ ),
4720
5283
  platforms: z3.array(
4721
5284
  z3.enum([
4722
5285
  "youtube",
@@ -4735,6 +5298,7 @@ Created with Social Neuron`;
4735
5298
  response_format: z3.enum(["text", "json"]).default("text")
4736
5299
  },
4737
5300
  async ({
5301
+ project_id,
4738
5302
  platforms,
4739
5303
  count,
4740
5304
  start_after,
@@ -4742,13 +5306,35 @@ Created with Social Neuron`;
4742
5306
  response_format
4743
5307
  }) => {
4744
5308
  try {
4745
- const startDate = start_after ? new Date(start_after) : /* @__PURE__ */ new Date();
4746
- const endDate = new Date(startDate.getTime() + 7 * 24 * 60 * 60 * 1e3);
4747
- const { data: postsResult, error: postsError } = await callEdgeFunction("mcp-data", {
4748
- action: "scheduled-posts",
4749
- start_date: startDate.toISOString(),
5309
+ const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
5310
+ if (!resolvedProjectId) {
5311
+ return {
5312
+ content: [
5313
+ {
5314
+ type: "text",
5315
+ text: "No project_id was provided and no default project is configured."
5316
+ }
5317
+ ],
5318
+ isError: true
5319
+ };
5320
+ }
5321
+ const startDate = start_after ? new Date(start_after) : /* @__PURE__ */ new Date();
5322
+ if (!Number.isFinite(startDate.getTime())) {
5323
+ return {
5324
+ content: [
5325
+ { type: "text", text: "start_after must be a valid ISO datetime." }
5326
+ ],
5327
+ isError: true
5328
+ };
5329
+ }
5330
+ const endDate = new Date(startDate.getTime() + 7 * 24 * 60 * 60 * 1e3);
5331
+ const { data: postsResult, error: postsError } = await callEdgeFunction("mcp-data", {
5332
+ action: "scheduled-posts",
5333
+ start_date: startDate.toISOString(),
4750
5334
  end_date: endDate.toISOString(),
4751
- statuses: ["scheduled", "draft"]
5335
+ statuses: ["pending", "scheduled", "draft"],
5336
+ projectId: resolvedProjectId,
5337
+ project_id: resolvedProjectId
4752
5338
  });
4753
5339
  const existingPosts = postsError ? [] : postsResult?.posts ?? [];
4754
5340
  const gapMs = min_gap_hours * 60 * 60 * 1e3;
@@ -4788,19 +5374,17 @@ Created with Social Neuron`;
4788
5374
  const slots = candidates.filter((s) => !s.conflict).sort((a, b) => b.engagement_score - a.engagement_score).slice(0, count);
4789
5375
  const conflictsAvoided = candidates.filter((s) => s.conflict).length;
4790
5376
  if (response_format === "json") {
5377
+ const structuredContent = asEnvelope3({
5378
+ slots,
5379
+ total_candidates: candidates.length,
5380
+ conflicts_avoided: conflictsAvoided
5381
+ });
4791
5382
  return {
5383
+ structuredContent,
4792
5384
  content: [
4793
5385
  {
4794
5386
  type: "text",
4795
- text: JSON.stringify(
4796
- asEnvelope3({
4797
- slots,
4798
- total_candidates: candidates.length,
4799
- conflicts_avoided: conflictsAvoided
4800
- }),
4801
- null,
4802
- 2
4803
- )
5387
+ text: JSON.stringify(structuredContent, null, 2)
4804
5388
  }
4805
5389
  ],
4806
5390
  isError: false
@@ -5165,7 +5749,7 @@ Created with Social Neuron`;
5165
5749
  const results = [];
5166
5750
  const buildIdempotencyKey = (post) => {
5167
5751
  const planId = effectivePlanId ?? (typeof workingPlan.plan_id === "string" ? String(workingPlan.plan_id) : "inline");
5168
- const captionHash = createHash2("sha256").update(post.caption).digest("hex").slice(0, 16);
5752
+ const captionHash = createHash("sha256").update(post.caption).digest("hex").slice(0, 16);
5169
5753
  const raw = [
5170
5754
  "schedule_content_plan",
5171
5755
  planId,
@@ -5175,7 +5759,7 @@ Created with Social Neuron`;
5175
5759
  captionHash,
5176
5760
  idempotency_seed ?? ""
5177
5761
  ].join(":");
5178
- return `plan-${createHash2("sha256").update(raw).digest("hex").slice(0, 48)}`;
5762
+ return `plan-${createHash("sha256").update(raw).digest("hex").slice(0, 48)}`;
5179
5763
  };
5180
5764
  const scheduleOne = async (post) => {
5181
5765
  if (!post.schedule_at) {
@@ -5361,7 +5945,7 @@ Created with Social Neuron`;
5361
5945
  }
5362
5946
  );
5363
5947
  }
5364
- var PLATFORM_CASE_MAP, TIKTOK_AUDIT_APPROVED, MCP_NOT_LIVE_FOR_POSTING;
5948
+ var PLATFORM_CASE_MAP, TIKTOK_AUDIT_APPROVED, MCP_NOT_LIVE_FOR_POSTING, VIDEO_FILE_EXTENSIONS, IMAGE_FILE_EXTENSIONS;
5365
5949
  var init_distribution = __esm({
5366
5950
  "src/tools/distribution.ts"() {
5367
5951
  "use strict";
@@ -5382,8 +5966,29 @@ var init_distribution = __esm({
5382
5966
  threads: "Threads",
5383
5967
  bluesky: "Bluesky"
5384
5968
  };
5385
- TIKTOK_AUDIT_APPROVED = false;
5969
+ TIKTOK_AUDIT_APPROVED = !["false", "0", "no"].includes(
5970
+ (process.env.TIKTOK_AUDIT_APPROVED ?? "").toLowerCase()
5971
+ );
5386
5972
  MCP_NOT_LIVE_FOR_POSTING = /* @__PURE__ */ new Set(["LinkedIn", "Pinterest", "Reddit"]);
5973
+ VIDEO_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
5974
+ "mp4",
5975
+ "mov",
5976
+ "m4v",
5977
+ "webm",
5978
+ "avi",
5979
+ "mkv",
5980
+ "mpeg",
5981
+ "mpg"
5982
+ ]);
5983
+ IMAGE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
5984
+ "jpg",
5985
+ "jpeg",
5986
+ "png",
5987
+ "webp",
5988
+ "gif",
5989
+ "avif",
5990
+ "heic"
5991
+ ]);
5387
5992
  }
5388
5993
  });
5389
5994
 
@@ -5664,7 +6269,10 @@ function registerMediaTools(server2) {
5664
6269
  content: [
5665
6270
  {
5666
6271
  type: "text",
5667
- text: `R2 upload failed (HTTP ${putResp.status}): ${await putResp.text().catch(() => "Unknown error")}`
6272
+ // Presigned-store bodies may include provider request IDs,
6273
+ // bucket names, or signed URL fragments. Status is enough
6274
+ // for user recovery and safe diagnostics.
6275
+ text: `R2 upload failed (HTTP ${putResp.status}). Please retry.`
5668
6276
  }
5669
6277
  ],
5670
6278
  isError: true
@@ -5907,7 +6515,11 @@ function registerAnalyticsTools(server2) {
5907
6515
  action: "analytics",
5908
6516
  platform: platform3,
5909
6517
  days: lookbackDays,
5910
- limit: maxPosts,
6518
+ // Fetch extra snapshots, then deduplicate to the requested post count.
6519
+ // The backend understands latestOnly after the paired application
6520
+ // deployment; older deployments safely ignore the hint.
6521
+ limit: Math.min(maxPosts * 5, 100),
6522
+ latestOnly: true,
5911
6523
  contentId: content_id,
5912
6524
  ...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
5913
6525
  });
@@ -5917,7 +6529,14 @@ function registerAnalyticsTools(server2) {
5917
6529
  isError: true
5918
6530
  };
5919
6531
  }
5920
- const rows = result?.rows ?? [];
6532
+ const snapshotRows = result?.rows ?? [];
6533
+ const seenSnapshots = /* @__PURE__ */ new Set();
6534
+ const rows = [...snapshotRows].sort((a, b) => b.captured_at.localeCompare(a.captured_at)).filter((row) => {
6535
+ const key = `${row.post_id}:${row.platform.toLowerCase()}`;
6536
+ if (seenSnapshots.has(key)) return false;
6537
+ seenSnapshots.add(key);
6538
+ return true;
6539
+ }).slice(0, maxPosts);
5921
6540
  if (rows.length === 0) {
5922
6541
  if (format === "json") {
5923
6542
  const structuredContent = asEnvelope4({
@@ -6096,6 +6715,84 @@ var init_analytics = __esm({
6096
6715
  }
6097
6716
  });
6098
6717
 
6718
+ // src/lib/brandUrlInput.ts
6719
+ function canonicalizeHost(hostname) {
6720
+ return hostname.replace(UNICODE_DOT_VARIANTS, ".").replace(/\.+$/, "");
6721
+ }
6722
+ function hostLooksLikeDomain(hostname) {
6723
+ return /\S+\.\S+/.test(canonicalizeHost(hostname));
6724
+ }
6725
+ function resolveHandle(handle, platform3) {
6726
+ const normalized = handle.replace(/^@/, "").trim();
6727
+ if (!normalized) return { url: null, handle: null, platform: null, ambiguous: false };
6728
+ if (!platform3) return { url: null, handle: normalized, platform: null, ambiguous: true };
6729
+ return {
6730
+ url: PROFILE_URL_BUILDERS[platform3](normalized),
6731
+ handle: normalized,
6732
+ platform: platform3,
6733
+ ambiguous: false
6734
+ };
6735
+ }
6736
+ function normalizeBrandUrlInput(raw, explicitPlatform) {
6737
+ const input = (raw ?? "").trim();
6738
+ if (!input) return { url: null, handle: null, platform: null, ambiguous: false };
6739
+ if (/^https?:\/\//i.test(input)) {
6740
+ let parsed;
6741
+ try {
6742
+ parsed = new URL(input);
6743
+ } catch {
6744
+ return { url: null, handle: null, platform: null, ambiguous: false, invalidUrl: true };
6745
+ }
6746
+ if (hostLooksLikeDomain(parsed.hostname)) {
6747
+ return { url: parsed.toString(), handle: null, platform: null, ambiguous: false };
6748
+ }
6749
+ return resolveHandle(canonicalizeHost(parsed.hostname), explicitPlatform);
6750
+ }
6751
+ const qualified = input.match(/^([a-z]{1,10}):\s*@?(.+)$/i);
6752
+ if (qualified) {
6753
+ const platform3 = PLATFORM_ALIASES[qualified[1].toLowerCase()];
6754
+ if (platform3) return resolveHandle(qualified[2], platform3);
6755
+ }
6756
+ if (input.startsWith("@")) return resolveHandle(input, explicitPlatform);
6757
+ if (!/\s/.test(input)) {
6758
+ try {
6759
+ const parsed = new URL(`https://${input}`);
6760
+ if (hostLooksLikeDomain(parsed.hostname)) {
6761
+ return { url: parsed.toString(), handle: null, platform: null, ambiguous: false };
6762
+ }
6763
+ } catch {
6764
+ }
6765
+ }
6766
+ return resolveHandle(input.replace(UNICODE_DOT_VARIANTS, ".").replace(/\.+$/, ""), explicitPlatform);
6767
+ }
6768
+ var PLATFORM_ALIASES, PROFILE_URL_BUILDERS, UNICODE_DOT_VARIANTS;
6769
+ var init_brandUrlInput = __esm({
6770
+ "src/lib/brandUrlInput.ts"() {
6771
+ "use strict";
6772
+ PLATFORM_ALIASES = {
6773
+ instagram: "instagram",
6774
+ ig: "instagram",
6775
+ insta: "instagram",
6776
+ tiktok: "tiktok",
6777
+ tt: "tiktok",
6778
+ twitter: "twitter",
6779
+ x: "twitter",
6780
+ linkedin: "linkedin",
6781
+ li: "linkedin",
6782
+ youtube: "youtube",
6783
+ yt: "youtube"
6784
+ };
6785
+ PROFILE_URL_BUILDERS = {
6786
+ instagram: (handle) => `https://instagram.com/${encodeURIComponent(handle)}`,
6787
+ tiktok: (handle) => `https://tiktok.com/@${encodeURIComponent(handle)}`,
6788
+ twitter: (handle) => `https://x.com/${encodeURIComponent(handle)}`,
6789
+ linkedin: (handle) => `https://linkedin.com/company/${encodeURIComponent(handle)}`,
6790
+ youtube: (handle) => `https://youtube.com/@${encodeURIComponent(handle)}`
6791
+ };
6792
+ UNICODE_DOT_VARIANTS = /[。.。]/g;
6793
+ }
6794
+ });
6795
+
6099
6796
  // src/tools/brand.ts
6100
6797
  import { z as z6 } from "zod";
6101
6798
  function asEnvelope5(data) {
@@ -6112,12 +6809,31 @@ function registerBrandTools(server2) {
6112
6809
  "extract_brand",
6113
6810
  "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.",
6114
6811
  {
6115
- url: z6.string().url().describe(
6116
- 'The website URL to analyze for brand identity (e.g. "https://example.com").'
6812
+ url: z6.string().min(1).max(2048).describe(
6813
+ '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").'
6117
6814
  ),
6118
6815
  response_format: z6.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
6119
6816
  },
6120
- async ({ url, response_format }) => {
6817
+ async ({ url: rawUrl, response_format }) => {
6818
+ const normalized = normalizeBrandUrlInput(rawUrl);
6819
+ if (normalized.invalidUrl) {
6820
+ return {
6821
+ content: [{ type: "text", text: `URL blocked: "${rawUrl}" is not a valid URL.` }],
6822
+ isError: true
6823
+ };
6824
+ }
6825
+ if (!normalized.url) {
6826
+ return {
6827
+ content: [
6828
+ {
6829
+ type: "text",
6830
+ text: `"${rawUrl}" looks like a handle, not a full URL. Provide a profile URL or use "platform:handle" shorthand (for example "instagram:acmefoods").`
6831
+ }
6832
+ ],
6833
+ isError: true
6834
+ };
6835
+ }
6836
+ const url = normalized.url;
6121
6837
  const ssrfCheck = await validateUrlForSSRF(url);
6122
6838
  if (!ssrfCheck.isValid) {
6123
6839
  return {
@@ -6425,6 +7141,7 @@ var init_brand = __esm({
6425
7141
  init_supabase();
6426
7142
  init_ssrf();
6427
7143
  init_version();
7144
+ init_brandUrlInput();
6428
7145
  }
6429
7146
  });
6430
7147
 
@@ -6800,7 +7517,7 @@ function registerRemotionTools(server2) {
6800
7517
  },
6801
7518
  async ({ composition_id, output_format, props }) => {
6802
7519
  const userId = await getDefaultUserId();
6803
- const rateLimit = checkRateLimit("screenshot", `render_demo_video:${userId}`);
7520
+ const rateLimit = checkRateLimit("generation", `render_demo_video:${userId}`);
6804
7521
  if (!rateLimit.allowed) {
6805
7522
  return {
6806
7523
  content: [
@@ -10634,7 +11351,9 @@ function toolKnowledgeDocument(tool) {
10634
11351
  function getKnowledgeDocuments() {
10635
11352
  return [
10636
11353
  ...STATIC_KNOWLEDGE_DOCUMENTS,
10637
- ...TOOL_CATALOG.filter((t) => !t.internal).map(toolKnowledgeDocument)
11354
+ ...TOOL_CATALOG.filter((t) => !t.internal && !t.hiddenFromPublicCount).map(
11355
+ toolKnowledgeDocument
11356
+ )
10638
11357
  ];
10639
11358
  }
10640
11359
  function tokenize(input) {
@@ -10751,7 +11470,7 @@ function registerDiscoveryTools(server2) {
10751
11470
  if (query) {
10752
11471
  results = searchTools(query);
10753
11472
  }
10754
- results = results.filter((t) => !t.internal);
11473
+ results = results.filter((t) => !t.internal && !t.hiddenFromPublicCount);
10755
11474
  if (module) {
10756
11475
  const moduleTools = getToolsByModule(module);
10757
11476
  results = results.filter((t) => moduleTools.some((mt) => mt.name === t.name));
@@ -12682,11 +13401,11 @@ function hexToLab(hex) {
12682
13401
  const lb = srgbToLinear(b);
12683
13402
  const x = lr * 0.4124564 + lg * 0.3575761 + lb * 0.1804375;
12684
13403
  const y = lr * 0.2126729 + lg * 0.7151522 + lb * 0.072175;
12685
- const z39 = lr * 0.0193339 + lg * 0.119192 + lb * 0.9503041;
13404
+ const z41 = lr * 0.0193339 + lg * 0.119192 + lb * 0.9503041;
12686
13405
  const f = (t) => t > 8856e-6 ? Math.cbrt(t) : 7.787 * t + 16 / 116;
12687
13406
  const fx = f(x / 0.95047);
12688
13407
  const fy = f(y / 1);
12689
- const fz = f(z39 / 1.08883);
13408
+ const fz = f(z41 / 1.08883);
12690
13409
  return { L: 116 * fy - 16, a: 500 * (fx - fy), b: 200 * (fy - fz) };
12691
13410
  }
12692
13411
  function deltaE2000(lab1, lab2) {
@@ -12926,10 +13645,10 @@ function registerBrandRuntimeTools(server2) {
12926
13645
  pagesScraped: meta.pagesScraped || 0
12927
13646
  }
12928
13647
  };
12929
- const envelope = asEnvelope23(runtime);
13648
+ const envelope2 = asEnvelope23(runtime);
12930
13649
  return {
12931
- structuredContent: envelope,
12932
- content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
13650
+ structuredContent: envelope2,
13651
+ content: [{ type: "text", text: JSON.stringify(envelope2, null, 2) }]
12933
13652
  };
12934
13653
  }
12935
13654
  );
@@ -13069,9 +13788,9 @@ function registerBrandRuntimeTools(server2) {
13069
13788
  }
13070
13789
  const profile = row.profile_data;
13071
13790
  const checkResult = computeBrandConsistency(content, profile);
13072
- const envelope = asEnvelope23(checkResult);
13791
+ const envelope2 = asEnvelope23(checkResult);
13073
13792
  return {
13074
- content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
13793
+ content: [{ type: "text", text: JSON.stringify(envelope2, null, 2) }]
13075
13794
  };
13076
13795
  }
13077
13796
  );
@@ -13103,9 +13822,9 @@ function registerBrandRuntimeTools(server2) {
13103
13822
  content_colors,
13104
13823
  threshold ?? 10
13105
13824
  );
13106
- const envelope = asEnvelope23(auditResult);
13825
+ const envelope2 = asEnvelope23(auditResult);
13107
13826
  return {
13108
- content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
13827
+ content: [{ type: "text", text: JSON.stringify(envelope2, null, 2) }]
13109
13828
  };
13110
13829
  }
13111
13830
  );
@@ -13138,9 +13857,9 @@ function registerBrandRuntimeTools(server2) {
13138
13857
  row.profile_data.typography,
13139
13858
  format
13140
13859
  );
13141
- const envelope = asEnvelope23({ format, tokens: output });
13860
+ const envelope2 = asEnvelope23({ format, tokens: output });
13142
13861
  return {
13143
- content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
13862
+ content: [{ type: "text", text: JSON.stringify(envelope2, null, 2) }]
13144
13863
  };
13145
13864
  }
13146
13865
  );
@@ -13294,7 +14013,7 @@ function registerCarouselTools(server2) {
13294
14013
  };
13295
14014
  }
13296
14015
  const userId = await getDefaultUserId();
13297
- const rateLimit = checkRateLimit("posting", `create_carousel:${userId}`);
14016
+ const rateLimit = checkRateLimit("generation", `create_carousel:${userId}`);
13298
14017
  if (!rateLimit.allowed) {
13299
14018
  return {
13300
14019
  content: [
@@ -13366,7 +14085,12 @@ function registerCarouselTools(server2) {
13366
14085
  slideNumber: slide.slideNumber,
13367
14086
  jobId: null,
13368
14087
  model: image_model,
13369
- error: error ?? "No job ID returned"
14088
+ error: error ?? "No job ID returned",
14089
+ creditsReserved: data?.credits_reserved ?? null,
14090
+ creditsCharged: data?.credits_charged ?? null,
14091
+ creditsRefunded: data?.credits_refunded ?? null,
14092
+ billingStatus: data?.billing_status ?? "unknown",
14093
+ failureReason: data?.failure_reason ?? null
13370
14094
  };
13371
14095
  }
13372
14096
  const jobId = data.asyncJobId ?? data.taskId ?? null;
@@ -13378,14 +14102,24 @@ function registerCarouselTools(server2) {
13378
14102
  slideNumber: slide.slideNumber,
13379
14103
  jobId,
13380
14104
  model: image_model,
13381
- error: null
14105
+ error: null,
14106
+ creditsReserved: 0,
14107
+ creditsCharged: data.creditsDeducted ?? perImageCost,
14108
+ creditsRefunded: 0,
14109
+ billingStatus: "charged",
14110
+ failureReason: null
13382
14111
  };
13383
14112
  } catch (err) {
13384
14113
  return {
13385
14114
  slideNumber: slide.slideNumber,
13386
14115
  jobId: null,
13387
14116
  model: image_model,
13388
- error: sanitizeError(err)
14117
+ error: sanitizeError(err),
14118
+ creditsReserved: null,
14119
+ creditsCharged: null,
14120
+ creditsRefunded: null,
14121
+ billingStatus: "unknown",
14122
+ failureReason: null
13389
14123
  };
13390
14124
  }
13391
14125
  })
@@ -13410,7 +14144,8 @@ function registerCarouselTools(server2) {
13410
14144
  return {
13411
14145
  ...s,
13412
14146
  imageJobId: job?.jobId ?? null,
13413
- imageError: job?.error ?? null
14147
+ imageError: job?.error ?? null,
14148
+ imageBillingStatus: job?.billingStatus ?? "unknown"
13414
14149
  };
13415
14150
  }),
13416
14151
  imageModel: image_model,
@@ -13422,11 +14157,19 @@ function registerCarouselTools(server2) {
13422
14157
  jobIds: successfulJobs.map((j) => j.jobId),
13423
14158
  failedSlides: failedJobs.map((j) => ({
13424
14159
  slideNumber: j.slideNumber,
13425
- error: j.error
14160
+ error: j.error,
14161
+ credits_reserved: j.creditsReserved,
14162
+ credits_charged: j.creditsCharged,
14163
+ credits_refunded: j.creditsRefunded,
14164
+ billing_status: j.billingStatus,
14165
+ failure_reason: j.failureReason
13426
14166
  })),
13427
14167
  credits: {
13428
14168
  textGeneration: textCredits,
13429
14169
  imagesEstimated: successfulJobs.length * perImageCost,
14170
+ imagesCharged: imageJobs.reduce((sum, job) => sum + (job.creditsCharged ?? 0), 0),
14171
+ imagesRefunded: imageJobs.reduce((sum, job) => sum + (job.creditsRefunded ?? 0), 0),
14172
+ billingUnknownSlides: imageJobs.filter((job) => job.billingStatus === "unknown").length,
13430
14173
  totalEstimated: textCredits + successfulJobs.length * perImageCost
13431
14174
  }
13432
14175
  }
@@ -13622,7 +14365,7 @@ function registerHyperframesTools(server2) {
13622
14365
  );
13623
14366
  server2.tool(
13624
14367
  "render_hyperframes",
13625
- "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.",
14368
+ "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.",
13626
14369
  {
13627
14370
  composition_html: z30.string().max(5e5).optional().describe(
13628
14371
  "Inline HTML composition (full <html>...</html>). Max 500KB. Use composition_url for larger."
@@ -13635,7 +14378,8 @@ function registerHyperframesTools(server2) {
13635
14378
  duration_sec: z30.number().min(0.1).max(600).describe("Output duration in seconds. Required. Max 600 (10 min)."),
13636
14379
  fps: z30.union([z30.literal(24), z30.literal(30), z30.literal(60)]).optional().describe("Frames per second. Default 30."),
13637
14380
  quality: z30.enum(["draft", "standard", "high"]).optional().describe("draft: fast iteration; standard: production; high: final master."),
13638
- project_id: z30.string().optional().describe("Project ID to associate the Hyperframes render with.")
14381
+ project_id: z30.string().optional().describe("Project ID to associate the Hyperframes render with."),
14382
+ response_format: z30.enum(["text", "json"]).optional().describe("Response format. Use json for a stable job_id handoff.")
13639
14383
  },
13640
14384
  async ({
13641
14385
  composition_html,
@@ -13645,7 +14389,8 @@ function registerHyperframesTools(server2) {
13645
14389
  duration_sec,
13646
14390
  fps,
13647
14391
  quality,
13648
- project_id
14392
+ project_id,
14393
+ response_format
13649
14394
  }) => {
13650
14395
  const userId = await getDefaultUserId();
13651
14396
  const rateLimit = checkRateLimit("generation", `render_hyperframes:${userId}`);
@@ -13696,11 +14441,23 @@ function registerHyperframesTools(server2) {
13696
14441
  if (error || !data?.jobId) {
13697
14442
  throw new Error(error || data?.error || "Failed to create Hyperframes render job");
13698
14443
  }
14444
+ const payload = {
14445
+ job_id: data.jobId,
14446
+ jobId: data.jobId,
14447
+ status: data.status,
14448
+ credits_cost: data.creditsCost,
14449
+ credits: data.creditsCost,
14450
+ duration_sec,
14451
+ fps: fps || 30,
14452
+ aspect_ratio: aspect_ratio || "9:16",
14453
+ quality: quality || "standard",
14454
+ project_id: project_id || null
14455
+ };
13699
14456
  return {
13700
14457
  content: [
13701
14458
  {
13702
14459
  type: "text",
13703
- text: [
14460
+ text: response_format === "json" ? JSON.stringify({ data: payload }) : [
13704
14461
  `Hyperframes render job queued.`,
13705
14462
  ` Job ID: ${data.jobId}`,
13706
14463
  ` Credits: ${data.creditsCost}`,
@@ -13717,7 +14474,7 @@ function registerHyperframesTools(server2) {
13717
14474
  content: [
13718
14475
  {
13719
14476
  type: "text",
13720
- text: `Failed to queue Hyperframes render: ${err instanceof Error ? err.message : "Unknown error"}`
14477
+ text: `Failed to queue Hyperframes render: ${sanitizeError(err)}`
13721
14478
  }
13722
14479
  ],
13723
14480
  isError: true
@@ -13733,6 +14490,7 @@ var init_hyperframes = __esm({
13733
14490
  init_rate_limit();
13734
14491
  init_supabase();
13735
14492
  init_edge_function();
14493
+ init_sanitize_error();
13736
14494
  HYPERFRAMES_BLOCKS = [
13737
14495
  // Transitions
13738
14496
  {
@@ -13850,49 +14608,122 @@ import {
13850
14608
  import { z as z31 } from "zod";
13851
14609
  import fs from "node:fs/promises";
13852
14610
  import path from "node:path";
14611
+ import { fileURLToPath } from "node:url";
13853
14612
  function startOfCurrentWeekMonday() {
13854
14613
  const now = /* @__PURE__ */ new Date();
13855
- const day = now.getDay();
14614
+ const day = now.getUTCDay();
13856
14615
  const monday = new Date(now);
13857
- monday.setUTCDate(now.getUTCDate() - day + (day === 0 ? -6 : 1));
14616
+ monday.setUTCDate(now.getUTCDate() - (day + 6) % 7);
14617
+ monday.setUTCHours(0, 0, 0, 0);
13858
14618
  return monday.toISOString().split("T")[0];
13859
14619
  }
14620
+ function endOfWeek(startDate) {
14621
+ const end = /* @__PURE__ */ new Date(`${startDate}T00:00:00.000Z`);
14622
+ end.setUTCDate(end.getUTCDate() + 7);
14623
+ end.setUTCMilliseconds(end.getUTCMilliseconds() - 1);
14624
+ return end.toISOString();
14625
+ }
14626
+ function isStrictIsoDate(value) {
14627
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
14628
+ const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
14629
+ return Number.isFinite(parsed.getTime()) && parsed.toISOString().slice(0, 10) === value;
14630
+ }
14631
+ function publicRecentPost(value) {
14632
+ if (!value || typeof value !== "object") return null;
14633
+ const row = value;
14634
+ if (typeof row.id !== "string" || typeof row.platform !== "string" || typeof row.status !== "string" || typeof row.created_at !== "string") {
14635
+ return null;
14636
+ }
14637
+ const optional = (name) => typeof row[name] === "string" ? row[name] : null;
14638
+ return {
14639
+ id: row.id,
14640
+ platform: row.platform,
14641
+ status: row.status,
14642
+ title: optional("title"),
14643
+ external_post_id: optional("external_post_id"),
14644
+ published_at: optional("published_at"),
14645
+ scheduled_at: optional("scheduled_at"),
14646
+ created_at: row.created_at
14647
+ };
14648
+ }
14649
+ function calendarHtmlCandidates() {
14650
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
14651
+ return [
14652
+ // Bundled HTTP entry: import.meta.url is dist/http.js.
14653
+ path.join(moduleDir, "apps/content-calendar/mcp-app.html"),
14654
+ // Source/tsx/vitest: import.meta.url is src/apps/content-calendar.ts.
14655
+ path.resolve(moduleDir, "../../dist/apps/content-calendar/mcp-app.html"),
14656
+ // Local development fallback; never the sole package path.
14657
+ path.resolve(process.cwd(), "dist/apps/content-calendar/mcp-app.html")
14658
+ ];
14659
+ }
14660
+ async function readCalendarHtml() {
14661
+ for (const candidate of calendarHtmlCandidates()) {
14662
+ try {
14663
+ return await fs.readFile(candidate, "utf-8");
14664
+ } catch (error) {
14665
+ if (error.code !== "ENOENT") throw error;
14666
+ }
14667
+ }
14668
+ throw new Error("calendar_bundle_missing");
14669
+ }
13860
14670
  function registerContentCalendarApp(server2) {
13861
14671
  registerAppTool(
13862
14672
  server2,
13863
14673
  "open_content_calendar",
13864
14674
  {
13865
14675
  title: "Content Calendar",
13866
- 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.",
14676
+ 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.",
13867
14677
  inputSchema: {
13868
- start_date: z31.string().optional().describe(
14678
+ project_id: z31.string().uuid().optional().describe(
14679
+ "Brand/project ID. Defaults to the authenticated key's project or the account default."
14680
+ ),
14681
+ start_date: z31.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe(
13869
14682
  "ISO date for the week start (YYYY-MM-DD); defaults to the current week's Monday."
13870
14683
  )
13871
14684
  },
13872
14685
  outputSchema: {
13873
14686
  start_date: z31.string(),
14687
+ project_id: z31.string(),
13874
14688
  posts: z31.array(RecentPostOutputSchema),
13875
14689
  scopes: z31.array(z31.string())
13876
14690
  },
13877
14691
  _meta: {
13878
14692
  ui: {
13879
- resourceUri: CALENDAR_URI,
13880
- csp: {
13881
- "img-src": ["'self'", "https://*.r2.cloudflarestorage.com", "data:"],
13882
- "connect-src": ["'self'"]
13883
- }
14693
+ resourceUri: CALENDAR_URI
13884
14694
  }
13885
14695
  }
13886
14696
  },
13887
- async ({ start_date }, extra) => {
13888
- const userScopes = extra.authInfo?.scopes ?? [];
14697
+ async ({ project_id, start_date }) => {
14698
+ const userScopes = getRequestScopes() ?? getAuthenticatedScopes();
14699
+ const resolvedProjectId = project_id ?? await getDefaultProjectId();
14700
+ if (!resolvedProjectId) {
14701
+ return {
14702
+ content: [
14703
+ {
14704
+ type: "text",
14705
+ text: "No project_id was provided and no default project is configured."
14706
+ }
14707
+ ],
14708
+ isError: true
14709
+ };
14710
+ }
13889
14711
  const fromDate = start_date ?? startOfCurrentWeekMonday();
14712
+ if (!isStrictIsoDate(fromDate)) {
14713
+ return {
14714
+ content: [{ type: "text", text: "start_date must be a valid YYYY-MM-DD date." }],
14715
+ isError: true
14716
+ };
14717
+ }
13890
14718
  const { data: result, error } = await callEdgeFunction(
13891
14719
  "mcp-data",
13892
14720
  {
13893
- action: "recent-posts",
13894
- days: 14,
13895
- limit: 50
14721
+ action: "scheduled-posts",
14722
+ start_date: `${fromDate}T00:00:00.000Z`,
14723
+ end_date: endOfWeek(fromDate),
14724
+ statuses: ["pending", "scheduled", "draft"],
14725
+ projectId: resolvedProjectId,
14726
+ project_id: resolvedProjectId
13896
14727
  },
13897
14728
  { timeoutMs: 15e3 }
13898
14729
  );
@@ -13901,19 +14732,16 @@ function registerContentCalendarApp(server2) {
13901
14732
  content: [
13902
14733
  {
13903
14734
  type: "text",
13904
- text: `Failed to load posts: ${error || result?.error || "Unknown error"}`
14735
+ text: "The content calendar could not load posts. Please retry."
13905
14736
  }
13906
14737
  ],
13907
14738
  isError: true
13908
14739
  };
13909
14740
  }
13910
- const posts = (result.posts ?? []).filter((p) => {
13911
- const ts = p.scheduled_at ?? p.published_at ?? p.created_at;
13912
- if (!ts) return false;
13913
- return ts.split("T")[0] >= fromDate;
13914
- });
14741
+ const posts = (result.posts ?? []).map(publicRecentPost).filter((post) => post !== null);
13915
14742
  const structuredContent = {
13916
14743
  start_date: fromDate,
14744
+ project_id: resolvedProjectId,
13917
14745
  posts,
13918
14746
  scopes: userScopes
13919
14747
  };
@@ -13932,37 +14760,60 @@ function registerContentCalendarApp(server2) {
13932
14760
  server2,
13933
14761
  CALENDAR_URI,
13934
14762
  CALENDAR_URI,
13935
- { mimeType: RESOURCE_MIME_TYPE },
14763
+ {
14764
+ mimeType: RESOURCE_MIME_TYPE,
14765
+ description: "Self-contained Social Neuron project content calendar.",
14766
+ _meta: { ui: { csp: CALENDAR_CSP } }
14767
+ },
13936
14768
  async () => {
13937
- const htmlPath = path.join(process.cwd(), "apps/content-calendar/dist/mcp-app.html");
13938
14769
  try {
13939
- const html = await fs.readFile(htmlPath, "utf-8");
14770
+ const html = await readCalendarHtml();
13940
14771
  return {
13941
- contents: [{ uri: CALENDAR_URI, mimeType: RESOURCE_MIME_TYPE, text: html }]
14772
+ contents: [
14773
+ {
14774
+ uri: CALENDAR_URI,
14775
+ mimeType: RESOURCE_MIME_TYPE,
14776
+ text: html,
14777
+ _meta: { ui: { csp: CALENDAR_CSP } }
14778
+ }
14779
+ ]
13942
14780
  };
13943
- } catch (err) {
14781
+ } catch {
13944
14782
  const errorHtml = `<!DOCTYPE html>
13945
14783
  <html><head><title>Content Calendar \u2014 unavailable</title></head>
13946
14784
  <body style="font-family:sans-serif;padding:24px;color:#444;">
13947
14785
  <h2>Content Calendar app bundle missing</h2>
13948
- <p>The server registered <code>open_content_calendar</code> but
13949
- <code>apps/content-calendar/dist/mcp-app.html</code> is not built.
13950
- Run <code>npm run build:app</code> in the mcp-server directory and redeploy.</p>
13951
- <p style="color:#999;font-size:12px;">${err.message}</p>
14786
+ <p>The interactive bundle is unavailable on this deployment. Please contact Social Neuron support.</p>
13952
14787
  </body></html>`;
13953
14788
  return {
13954
- contents: [{ uri: CALENDAR_URI, mimeType: RESOURCE_MIME_TYPE, text: errorHtml }]
14789
+ contents: [
14790
+ {
14791
+ uri: CALENDAR_URI,
14792
+ mimeType: RESOURCE_MIME_TYPE,
14793
+ text: errorHtml,
14794
+ _meta: { ui: { csp: CALENDAR_CSP } }
14795
+ }
14796
+ ]
13955
14797
  };
13956
14798
  }
13957
14799
  }
13958
14800
  );
13959
14801
  }
13960
- var CALENDAR_URI, RecentPostOutputSchema;
14802
+ var CALENDAR_URI, CALENDAR_CSP, RecentPostOutputSchema;
13961
14803
  var init_content_calendar = __esm({
13962
14804
  "src/apps/content-calendar.ts"() {
13963
14805
  "use strict";
13964
14806
  init_edge_function();
13965
- CALENDAR_URI = "ui://content-calendar/mcp-app.html";
14807
+ init_request_context();
14808
+ init_supabase();
14809
+ CALENDAR_URI = "ui://content-calendar/v1/mcp-app.html";
14810
+ CALENDAR_CSP = {
14811
+ // The HTML is fully self-contained. Tool calls travel over the host bridge,
14812
+ // not fetch/XHR, so the secure default is no network or remote resources.
14813
+ connectDomains: [],
14814
+ resourceDomains: [],
14815
+ frameDomains: []
14816
+ };
13966
14817
  RecentPostOutputSchema = z31.object({
13967
14818
  id: z31.string(),
13968
14819
  platform: z31.string(),
@@ -13976,8 +14827,257 @@ var init_content_calendar = __esm({
13976
14827
  }
13977
14828
  });
13978
14829
 
13979
- // src/tools/connections.ts
14830
+ // src/apps/analytics-pulse.ts
14831
+ import {
14832
+ registerAppResource as registerAppResource2,
14833
+ registerAppTool as registerAppTool2,
14834
+ RESOURCE_MIME_TYPE as RESOURCE_MIME_TYPE2
14835
+ } from "@modelcontextprotocol/ext-apps/server";
13980
14836
  import { z as z32 } from "zod";
14837
+ import fs2 from "node:fs/promises";
14838
+ import path2 from "node:path";
14839
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
14840
+ function finiteMetric(value) {
14841
+ const parsed = Number(value ?? 0);
14842
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
14843
+ }
14844
+ function publicAnalyticsPost(value) {
14845
+ if (!value || typeof value !== "object") return null;
14846
+ const row = value;
14847
+ const post = row.posts && typeof row.posts === "object" ? row.posts : {};
14848
+ const platform3 = typeof row.platform === "string" ? row.platform : typeof post.platform === "string" ? post.platform : null;
14849
+ const capturedAt = typeof row.captured_at === "string" ? row.captured_at : null;
14850
+ if (!platform3 || !capturedAt) return null;
14851
+ const views = finiteMetric(row.views);
14852
+ const likes = finiteMetric(row.likes);
14853
+ const comments = finiteMetric(row.comments);
14854
+ const shares = finiteMetric(row.shares);
14855
+ const engagement = likes + comments + shares;
14856
+ return {
14857
+ platform: platform3,
14858
+ title: typeof post.title === "string" ? post.title : null,
14859
+ views,
14860
+ likes,
14861
+ comments,
14862
+ shares,
14863
+ engagement_rate: views > 0 ? Number((engagement / views * 100).toFixed(2)) : 0,
14864
+ captured_at: capturedAt,
14865
+ published_at: typeof post.published_at === "string" ? post.published_at : null
14866
+ };
14867
+ }
14868
+ function latestAnalyticsRows(values) {
14869
+ const rows = values.filter((value) => Boolean(value) && typeof value === "object").sort((a, b) => String(b.captured_at ?? "").localeCompare(String(a.captured_at ?? "")));
14870
+ const seen = /* @__PURE__ */ new Set();
14871
+ const latest = [];
14872
+ for (const row of rows) {
14873
+ if (typeof row.post_id !== "string" || typeof row.platform !== "string") continue;
14874
+ const key = `${row.post_id}:${row.platform.toLowerCase()}`;
14875
+ if (seen.has(key)) continue;
14876
+ seen.add(key);
14877
+ latest.push(row);
14878
+ }
14879
+ return latest;
14880
+ }
14881
+ function analyticsHtmlCandidates() {
14882
+ const moduleDir = path2.dirname(fileURLToPath2(import.meta.url));
14883
+ return [
14884
+ path2.join(moduleDir, "apps/analytics-pulse/mcp-app.html"),
14885
+ path2.resolve(moduleDir, "../../dist/apps/analytics-pulse/mcp-app.html"),
14886
+ path2.resolve(process.cwd(), "dist/apps/analytics-pulse/mcp-app.html")
14887
+ ];
14888
+ }
14889
+ async function readAnalyticsHtml() {
14890
+ for (const candidate of analyticsHtmlCandidates()) {
14891
+ try {
14892
+ return await fs2.readFile(candidate, "utf-8");
14893
+ } catch (error) {
14894
+ if (error.code !== "ENOENT") throw error;
14895
+ }
14896
+ }
14897
+ throw new Error("analytics_bundle_missing");
14898
+ }
14899
+ function registerAnalyticsPulseApp(server2) {
14900
+ registerAppTool2(
14901
+ server2,
14902
+ "open_analytics_pulse",
14903
+ {
14904
+ title: "Analytics Pulse",
14905
+ 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.",
14906
+ inputSchema: {
14907
+ project_id: z32.string().uuid().optional().describe("Brand/project ID. Defaults to the authenticated key's project or account default."),
14908
+ platform: z32.enum(PLATFORM_VALUES).optional().describe("Optional platform filter."),
14909
+ days: z32.number().int().min(1).max(365).optional().describe("Lookback window. Defaults to 30 days.")
14910
+ },
14911
+ outputSchema: {
14912
+ project_id: z32.string(),
14913
+ platform: z32.string().nullable(),
14914
+ days: z32.number(),
14915
+ summary: z32.object({
14916
+ views: z32.number(),
14917
+ engagement: z32.number(),
14918
+ engagement_rate: z32.number(),
14919
+ posts: z32.number()
14920
+ }),
14921
+ platform_totals: z32.array(
14922
+ z32.object({
14923
+ platform: z32.string(),
14924
+ views: z32.number(),
14925
+ engagement: z32.number(),
14926
+ posts: z32.number()
14927
+ })
14928
+ ),
14929
+ posts: z32.array(AnalyticsPostOutputSchema)
14930
+ },
14931
+ _meta: { ui: { resourceUri: ANALYTICS_URI } }
14932
+ },
14933
+ async ({ project_id, platform: platform3, days }) => {
14934
+ const resolvedProjectId = project_id ?? await getDefaultProjectId();
14935
+ if (!resolvedProjectId) {
14936
+ return {
14937
+ content: [{ type: "text", text: "No project_id was provided and no default project is configured." }],
14938
+ isError: true
14939
+ };
14940
+ }
14941
+ const lookbackDays = days ?? 30;
14942
+ const { data: result, error } = await callEdgeFunction(
14943
+ "mcp-data",
14944
+ {
14945
+ action: "analytics",
14946
+ projectId: resolvedProjectId,
14947
+ project_id: resolvedProjectId,
14948
+ days: lookbackDays,
14949
+ // The table stores cumulative snapshots. Request a wider window and
14950
+ // deduplicate below so refresh frequency cannot inflate the totals.
14951
+ limit: 100,
14952
+ latestOnly: true,
14953
+ ...platform3 ? { platform: platform3 } : {}
14954
+ },
14955
+ { timeoutMs: 15e3 }
14956
+ );
14957
+ if (error || !result?.success) {
14958
+ return {
14959
+ content: [{ type: "text", text: "The analytics dashboard could not load data. Please retry." }],
14960
+ isError: true
14961
+ };
14962
+ }
14963
+ const posts = latestAnalyticsRows(result.rows ?? []).map(publicAnalyticsPost).filter((post) => post !== null).sort((a, b) => b.views - a.views);
14964
+ const totals = /* @__PURE__ */ new Map();
14965
+ let totalViews = 0;
14966
+ let totalEngagement = 0;
14967
+ for (const post of posts) {
14968
+ const engagement = post.likes + post.comments + post.shares;
14969
+ totalViews += post.views;
14970
+ totalEngagement += engagement;
14971
+ const aggregate = totals.get(post.platform) ?? {
14972
+ platform: post.platform,
14973
+ views: 0,
14974
+ engagement: 0,
14975
+ posts: 0
14976
+ };
14977
+ aggregate.views += post.views;
14978
+ aggregate.engagement += engagement;
14979
+ aggregate.posts += 1;
14980
+ totals.set(post.platform, aggregate);
14981
+ }
14982
+ const structuredContent = {
14983
+ project_id: resolvedProjectId,
14984
+ platform: platform3 ?? null,
14985
+ days: lookbackDays,
14986
+ summary: {
14987
+ views: totalViews,
14988
+ engagement: totalEngagement,
14989
+ engagement_rate: totalViews > 0 ? Number((totalEngagement / totalViews * 100).toFixed(2)) : 0,
14990
+ posts: posts.length
14991
+ },
14992
+ platform_totals: [...totals.values()].sort((a, b) => b.views - a.views),
14993
+ posts
14994
+ };
14995
+ return {
14996
+ structuredContent,
14997
+ content: [
14998
+ {
14999
+ type: "text",
15000
+ text: `Loaded ${posts.length} analytics record${posts.length === 1 ? "" : "s"} for the last ${lookbackDays} days.`
15001
+ }
15002
+ ]
15003
+ };
15004
+ }
15005
+ );
15006
+ registerAppResource2(
15007
+ server2,
15008
+ ANALYTICS_URI,
15009
+ ANALYTICS_URI,
15010
+ {
15011
+ mimeType: RESOURCE_MIME_TYPE2,
15012
+ description: "Self-contained Social Neuron project analytics dashboard.",
15013
+ _meta: { ui: { csp: ANALYTICS_CSP } }
15014
+ },
15015
+ async () => {
15016
+ try {
15017
+ const html = await readAnalyticsHtml();
15018
+ return {
15019
+ contents: [
15020
+ {
15021
+ uri: ANALYTICS_URI,
15022
+ mimeType: RESOURCE_MIME_TYPE2,
15023
+ text: html,
15024
+ _meta: { ui: { csp: ANALYTICS_CSP } }
15025
+ }
15026
+ ]
15027
+ };
15028
+ } catch {
15029
+ return {
15030
+ contents: [
15031
+ {
15032
+ uri: ANALYTICS_URI,
15033
+ mimeType: RESOURCE_MIME_TYPE2,
15034
+ 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>",
15035
+ _meta: { ui: { csp: ANALYTICS_CSP } }
15036
+ }
15037
+ ]
15038
+ };
15039
+ }
15040
+ }
15041
+ );
15042
+ }
15043
+ var ANALYTICS_URI, ANALYTICS_CSP, PLATFORM_VALUES, AnalyticsPostOutputSchema;
15044
+ var init_analytics_pulse = __esm({
15045
+ "src/apps/analytics-pulse.ts"() {
15046
+ "use strict";
15047
+ init_edge_function();
15048
+ init_supabase();
15049
+ ANALYTICS_URI = "ui://analytics-pulse/v1/mcp-app.html";
15050
+ ANALYTICS_CSP = {
15051
+ connectDomains: [],
15052
+ resourceDomains: [],
15053
+ frameDomains: []
15054
+ };
15055
+ PLATFORM_VALUES = [
15056
+ "youtube",
15057
+ "tiktok",
15058
+ "instagram",
15059
+ "twitter",
15060
+ "linkedin",
15061
+ "facebook",
15062
+ "threads",
15063
+ "bluesky"
15064
+ ];
15065
+ AnalyticsPostOutputSchema = z32.object({
15066
+ platform: z32.string(),
15067
+ title: z32.string().nullable(),
15068
+ views: z32.number(),
15069
+ likes: z32.number(),
15070
+ comments: z32.number(),
15071
+ shares: z32.number(),
15072
+ engagement_rate: z32.number(),
15073
+ captured_at: z32.string(),
15074
+ published_at: z32.string().nullable()
15075
+ });
15076
+ }
15077
+ });
15078
+
15079
+ // src/tools/connections.ts
15080
+ import { z as z33 } from "zod";
13981
15081
  function findActiveAccount(accounts, platform3) {
13982
15082
  const target = platform3.toLowerCase();
13983
15083
  return accounts.find((a) => {
@@ -13990,16 +15090,16 @@ function registerConnectionTools(server2) {
13990
15090
  "start_platform_connection",
13991
15091
  "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.",
13992
15092
  {
13993
- platform: z32.enum(PLATFORM_ENUM4).describe("Platform to connect. Lower-case: instagram, tiktok, youtube, etc."),
13994
- project_id: z32.string().optional().describe(
15093
+ platform: z33.enum(PLATFORM_ENUM4).describe("Platform to connect. Lower-case: instagram, tiktok, youtube, etc."),
15094
+ project_id: z33.string().optional().describe(
13995
15095
  "Brand/project ID to bind the new social account to. Use this when connecting a brand-specific account."
13996
15096
  ),
13997
- response_format: z32.enum(["text", "json"]).optional().describe("Response format. Default: text.")
15097
+ response_format: z33.enum(["text", "json"]).optional().describe("Response format. Default: text.")
13998
15098
  },
13999
15099
  async ({ platform: platform3, project_id, response_format }) => {
14000
15100
  const format = response_format ?? "text";
14001
15101
  const userId = await getDefaultUserId();
14002
- const rl = checkRateLimit("read", `start_platform_connection:${userId}`);
15102
+ const rl = checkRateLimit("posting", `start_platform_connection:${userId}`);
14003
15103
  if (!rl.allowed) {
14004
15104
  return {
14005
15105
  content: [
@@ -14079,13 +15179,13 @@ function registerConnectionTools(server2) {
14079
15179
  "wait_for_connection",
14080
15180
  "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.",
14081
15181
  {
14082
- platform: z32.enum(PLATFORM_ENUM4).describe("Platform to wait for."),
14083
- project_id: z32.string().optional().describe(
15182
+ platform: z33.enum(PLATFORM_ENUM4).describe("Platform to wait for."),
15183
+ project_id: z33.string().optional().describe(
14084
15184
  "Brand/project ID to scope the connection poll. Use the same project_id passed to start_platform_connection."
14085
15185
  ),
14086
- timeout_s: z32.number().min(5).max(600).optional().describe("How long to wait, in seconds. Default 120."),
14087
- poll_interval_s: z32.number().min(2).max(30).optional().describe("Poll interval in seconds. Default 5."),
14088
- response_format: z32.enum(["text", "json"]).optional().describe("Response format. Default: text.")
15186
+ timeout_s: z33.number().min(5).max(600).optional().describe("How long to wait, in seconds. Default 120."),
15187
+ poll_interval_s: z33.number().min(2).max(30).optional().describe("Poll interval in seconds. Default 5."),
15188
+ response_format: z33.enum(["text", "json"]).optional().describe("Response format. Default: text.")
14089
15189
  },
14090
15190
  async ({ platform: platform3, project_id, timeout_s, poll_interval_s, response_format }) => {
14091
15191
  const format = response_format ?? "text";
@@ -14241,7 +15341,7 @@ var init_connections = __esm({
14241
15341
  });
14242
15342
 
14243
15343
  // src/tools/harness.ts
14244
- import { z as z33 } from "zod";
15344
+ import { z as z34 } from "zod";
14245
15345
  function registerHarnessTools(server2, _ctx) {
14246
15346
  server2.tool(
14247
15347
  "write_agent_reflection",
@@ -14346,38 +15446,38 @@ var init_harness = __esm({
14346
15446
  "engager"
14347
15447
  ];
14348
15448
  writeReflectionSchema = {
14349
- reflection_text: z33.string().min(1).max(4e3).describe("The verbal reflection text produced by the agent. 1\u20134000 characters."),
14350
- generated_by_agent: z33.enum(ALLOWED_AGENTS).describe(
15449
+ reflection_text: z34.string().min(1).max(4e3).describe("The verbal reflection text produced by the agent. 1\u20134000 characters."),
15450
+ generated_by_agent: z34.enum(ALLOWED_AGENTS).describe(
14351
15451
  "Which agent produced this reflection. Must be one of: conductor, brand-brain, drafter, publisher, analyst, engager."
14352
15452
  ),
14353
- provenance: z33.object({
14354
- content_history_id: z33.string().optional().describe("Related content_history row UUID."),
14355
- outcome_event_id: z33.string().optional().describe("Related outcome event UUID."),
14356
- prm_score_ids: z33.array(z33.string()).optional().describe("PRM score IDs that informed this reflection."),
14357
- handoff_ids: z33.array(z33.string()).optional().describe("Handoff event IDs that triggered this reflection.")
15453
+ provenance: z34.object({
15454
+ content_history_id: z34.string().optional().describe("Related content_history row UUID."),
15455
+ outcome_event_id: z34.string().optional().describe("Related outcome event UUID."),
15456
+ prm_score_ids: z34.array(z34.string()).optional().describe("PRM score IDs that informed this reflection."),
15457
+ handoff_ids: z34.array(z34.string()).optional().describe("Handoff event IDs that triggered this reflection.")
14358
15458
  }).strict().describe(
14359
15459
  "Source evidence for this reflection. Only these four keys are accepted \u2014 unknown keys are rejected to prevent spurious provenance claims."
14360
15460
  ),
14361
- brand_id: z33.string().describe("Brand profile UUID this reflection belongs to."),
14362
- pipeline_id: z33.string().optional().describe("Optional: pipeline run UUID."),
14363
- post_id: z33.string().optional().describe("Optional: post UUID if reflection targets a post.")
15461
+ brand_id: z34.string().describe("Brand profile UUID this reflection belongs to."),
15462
+ pipeline_id: z34.string().optional().describe("Optional: pipeline run UUID."),
15463
+ post_id: z34.string().optional().describe("Optional: post UUID if reflection targets a post.")
14364
15464
  };
14365
15465
  readReflectionSchema = {
14366
- brand_id: z33.string().describe("Brand profile UUID to read reflections for."),
14367
- generated_by_agent: z33.enum(ALLOWED_AGENTS).optional().describe(
15466
+ brand_id: z34.string().describe("Brand profile UUID to read reflections for."),
15467
+ generated_by_agent: z34.enum(ALLOWED_AGENTS).optional().describe(
14368
15468
  "Optional filter: only return reflections produced by this agent. One of: conductor, brand-brain, drafter, publisher, analyst, engager."
14369
15469
  ),
14370
- limit: z33.number().int().min(1).max(100).optional().describe(
15470
+ limit: z34.number().int().min(1).max(100).optional().describe(
14371
15471
  "Maximum number of reflections to return. Clamped to [1, 100]. Default: 50. Results are ordered by created_at DESC, then id ASC (deterministic tiebreak)."
14372
15472
  )
14373
15473
  };
14374
15474
  recordOutcomeSchema = {
14375
- decision_event_id: z33.string().describe("UUID of the decision_events row to record an outcome for."),
14376
- horizon: z33.enum(["1h", "6h", "24h"]).describe(
15475
+ decision_event_id: z34.string().describe("UUID of the decision_events row to record an outcome for."),
15476
+ horizon: z34.enum(["1h", "6h", "24h"]).describe(
14377
15477
  "Observation horizon. Only horizon=24h with a non-null reward triggers a learning-loop update; 1h/6h are stored but inert for learning."
14378
15478
  ),
14379
- reward: z33.number().min(0).max(1).describe("Normalised reward signal in [0, 1]. Higher = better outcome."),
14380
- outcome_metrics: z33.record(z33.string(), z33.number()).optional().describe(
15479
+ reward: z34.number().min(0).max(1).describe("Normalised reward signal in [0, 1]. Higher = better outcome."),
15480
+ outcome_metrics: z34.record(z34.string(), z34.number()).optional().describe(
14381
15481
  'Optional map of raw metric name \u2192 value (e.g. {"likes": 42, "reach": 1200}). Stored verbatim; not used for learning directly.'
14382
15482
  )
14383
15483
  };
@@ -14385,7 +15485,7 @@ var init_harness = __esm({
14385
15485
  });
14386
15486
 
14387
15487
  // src/tools/hermes.ts
14388
- import { z as z34 } from "zod";
15488
+ import { z as z35 } from "zod";
14389
15489
  function asEnvelope25(data) {
14390
15490
  return {
14391
15491
  _meta: {
@@ -14401,12 +15501,12 @@ function registerHermesTools(server2) {
14401
15501
  "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.",
14402
15502
  {
14403
15503
  platform: PLATFORM.describe("Target platform for the draft."),
14404
- copy: z34.string().min(1).max(8e3).describe("The draft post body."),
14405
- project_id: z34.string().optional().describe("SN project UUID. Optional but recommended."),
14406
- media_url: z34.string().url().optional().describe("Optional cover/media URL (R2 signed URL)."),
14407
- hermes_run_id: z34.string().optional().describe("Agent run id, for traceability."),
14408
- source_intel_ids: z34.array(z34.string()).optional().describe("Optional list of intel_signals.id rows that informed this draft."),
14409
- response_format: z34.enum(["text", "json"]).optional()
15504
+ copy: z35.string().min(1).max(8e3).describe("The draft post body."),
15505
+ project_id: z35.string().optional().describe("SN project UUID. Optional but recommended."),
15506
+ media_url: z35.string().url().optional().describe("Optional cover/media URL (R2 signed URL)."),
15507
+ hermes_run_id: z35.string().optional().describe("Agent run id, for traceability."),
15508
+ source_intel_ids: z35.array(z35.string()).optional().describe("Optional list of intel_signals.id rows that informed this draft."),
15509
+ response_format: z35.enum(["text", "json"]).optional()
14410
15510
  },
14411
15511
  async ({ platform: platform3, copy, project_id, media_url, hermes_run_id, response_format }) => {
14412
15512
  const { data, error } = await callEdgeFunction(
@@ -14444,15 +15544,15 @@ function registerHermesTools(server2) {
14444
15544
  "record_voice_lesson",
14445
15545
  '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)".',
14446
15546
  {
14447
- project_id: z34.string().describe("SN project UUID."),
14448
- lesson: z34.string().min(1).max(500).describe('One-sentence rule (e.g. "lowercase IG hooks beat title-case").'),
14449
- evidence: z34.object({
14450
- engagement_lift_pct: z34.number().describe("Engagement lift vs baseline, in percentage points."),
14451
- sample_size: z34.number().int().min(1).describe("Number of posts behind this lesson."),
14452
- top_examples: z34.array(z34.string()).optional().describe("Up to 3 hook examples from the top quartile.")
15547
+ project_id: z35.string().describe("SN project UUID."),
15548
+ lesson: z35.string().min(1).max(500).describe('One-sentence rule (e.g. "lowercase IG hooks beat title-case").'),
15549
+ evidence: z35.object({
15550
+ engagement_lift_pct: z35.number().describe("Engagement lift vs baseline, in percentage points."),
15551
+ sample_size: z35.number().int().min(1).describe("Number of posts behind this lesson."),
15552
+ top_examples: z35.array(z35.string()).optional().describe("Up to 3 hook examples from the top quartile.")
14453
15553
  }).describe("Quantitative evidence backing the lesson."),
14454
- applies_to: z34.array(PLATFORM).min(1).describe("Platforms this lesson applies to."),
14455
- response_format: z34.enum(["text", "json"]).optional()
15554
+ applies_to: z35.array(PLATFORM).min(1).describe("Platforms this lesson applies to."),
15555
+ response_format: z35.enum(["text", "json"]).optional()
14456
15556
  },
14457
15557
  async ({ project_id, lesson, evidence, applies_to, response_format }) => {
14458
15558
  const { data, error } = await callEdgeFunction("mcp-data", {
@@ -14488,10 +15588,10 @@ function registerHermesTools(server2) {
14488
15588
  "record_observation",
14489
15589
  '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.',
14490
15590
  {
14491
- summary: z34.string().min(1).max(2e3).describe("One-paragraph summary, shown to the account owner."),
14492
- 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})."),
14493
- run_id: z34.string().optional().describe("Agent run id for traceability."),
14494
- response_format: z34.enum(["text", "json"]).optional()
15591
+ summary: z35.string().min(1).max(2e3).describe("One-paragraph summary, shown to the account owner."),
15592
+ 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})."),
15593
+ run_id: z35.string().optional().describe("Agent run id for traceability."),
15594
+ response_format: z35.enum(["text", "json"]).optional()
14495
15595
  },
14496
15596
  async ({ summary, deltas, run_id, response_format }) => {
14497
15597
  const { data, error } = await callEdgeFunction("mcp-data", { action: "record-observation", summary, deltas, run_id });
@@ -14518,13 +15618,13 @@ function registerHermesTools(server2) {
14518
15618
  "record_intel_signal",
14519
15619
  "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.",
14520
15620
  {
14521
- source: z34.string().min(1).max(100).describe('Watcher name (e.g. "news-watch", "hackernews-watch").'),
14522
- url: z34.string().url().max(2e3).describe("Canonical URL of the source. Used for dedupe."),
14523
- topic: z34.string().max(200).optional().describe("Best-fit topic key."),
14524
- title: z34.string().max(500).optional(),
14525
- summary: z34.string().max(4e3).optional().describe("One-paragraph summary of the signal."),
14526
- score: z34.number().optional().describe("Relevance score from the watcher (0\u201310 typical)."),
14527
- response_format: z34.enum(["text", "json"]).optional()
15621
+ source: z35.string().min(1).max(100).describe('Watcher name (e.g. "news-watch", "hackernews-watch").'),
15622
+ url: z35.string().url().max(2e3).describe("Canonical URL of the source. Used for dedupe."),
15623
+ topic: z35.string().max(200).optional().describe("Best-fit topic key."),
15624
+ title: z35.string().max(500).optional(),
15625
+ summary: z35.string().max(4e3).optional().describe("One-paragraph summary of the signal."),
15626
+ score: z35.number().optional().describe("Relevance score from the watcher (0\u201310 typical)."),
15627
+ response_format: z35.enum(["text", "json"]).optional()
14528
15628
  },
14529
15629
  async ({ source, url, topic, title, summary, score, response_format }) => {
14530
15630
  const { data, error } = await callEdgeFunction("mcp-data", {
@@ -14562,16 +15662,16 @@ function registerHermesTools(server2) {
14562
15662
  "record_campaign_spend",
14563
15663
  "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.",
14564
15664
  {
14565
- campaign_id: z34.string().min(1).max(200).describe("Campaign identifier (slug)."),
14566
- category: z34.enum([
15665
+ campaign_id: z35.string().min(1).max(200).describe("Campaign identifier (slug)."),
15666
+ category: z35.enum([
14567
15667
  "hermes_drafts",
14568
15668
  "carousel_renders",
14569
15669
  "analytics_pulls",
14570
15670
  "paid_amplification",
14571
15671
  "other"
14572
15672
  ]).describe("Cost category."),
14573
- amount_usd: z34.number().min(0).describe("Amount in USD; supports 4 decimal places."),
14574
- response_format: z34.enum(["text", "json"]).optional()
15673
+ amount_usd: z35.number().min(0).describe("Amount in USD; supports 4 decimal places."),
15674
+ response_format: z35.enum(["text", "json"]).optional()
14575
15675
  },
14576
15676
  async ({ campaign_id, category, amount_usd, response_format }) => {
14577
15677
  const { data, error } = await callEdgeFunction("mcp-data", {
@@ -14606,7 +15706,7 @@ function registerHermesTools(server2) {
14606
15706
  "get_active_campaigns",
14607
15707
  "List currently-running campaigns with thesis, budget, hero format, and current spend. Use to bias drafts toward active campaign themes.",
14608
15708
  {
14609
- response_format: z34.enum(["text", "json"]).optional()
15709
+ response_format: z35.enum(["text", "json"]).optional()
14610
15710
  },
14611
15711
  async ({ response_format }) => {
14612
15712
  const { data, error } = await callEdgeFunction("mcp-data", { action: "get-active-campaigns" });
@@ -14655,7 +15755,7 @@ var init_hermes = __esm({
14655
15755
  "use strict";
14656
15756
  init_edge_function();
14657
15757
  init_version();
14658
- PLATFORM = z34.enum([
15758
+ PLATFORM = z35.enum([
14659
15759
  "instagram",
14660
15760
  "twitter",
14661
15761
  "linkedin",
@@ -14701,7 +15801,20 @@ var init_skills_manifest = __esm({
14701
15801
  });
14702
15802
 
14703
15803
  // src/tools/skills.ts
14704
- import { z as z35 } from "zod";
15804
+ import { z as z36 } from "zod";
15805
+ function renderCatalogRow(row) {
15806
+ const lines = [];
15807
+ const platform3 = row.platform ? ` \xB7 ${row.platform}` : "";
15808
+ lines.push(`${row.slug} (${row.kind}${platform3})`);
15809
+ const desc = row.frontmatter && typeof row.frontmatter.description === "string" ? row.frontmatter.description : null;
15810
+ if (desc) lines.push(` ${desc}`);
15811
+ const lock = row.locked ? ` \u{1F512} (upgrade to unlock)` : "";
15812
+ lines.push(` Tier: ${row.tier_minimum}${lock}`);
15813
+ lines.push(
15814
+ ` Model: ${row.model_id ?? "n/a"} \xB7 ${row.body_chars} chars` + (row.updated_at ? ` \xB7 updated ${row.updated_at}` : "")
15815
+ );
15816
+ return lines.join("\n");
15817
+ }
14705
15818
  function asEnvelope26(data) {
14706
15819
  return {
14707
15820
  _meta: {
@@ -14729,19 +15842,88 @@ function registerSkillsTools(server2) {
14729
15842
  "list_skills",
14730
15843
  '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.',
14731
15844
  {
14732
- studio: z35.enum(STUDIO_VALUES).optional().describe("Filter to one studio (video, avatar, carousel, voice, caption, edit)."),
14733
- featured_only: z35.boolean().optional().describe("Return only featured (recommended) skills. Defaults to false."),
14734
- response_format: z35.enum(["text", "json"]).optional().describe("Response format. Defaults to text \u2014 human-readable summary.")
15845
+ studio: z36.enum(STUDIO_VALUES).optional().describe(
15846
+ "Filter to one studio (video, avatar, carousel, voice, caption, edit)."
15847
+ ),
15848
+ featured_only: z36.boolean().optional().describe(
15849
+ "Return only featured (recommended) skills. Defaults to false."
15850
+ ),
15851
+ response_format: z36.enum(["text", "json"]).optional().describe(
15852
+ "Response format. Defaults to text \u2014 human-readable summary."
15853
+ )
14735
15854
  },
14736
15855
  async ({ studio, featured_only, response_format }) => {
14737
- const skills = listSkills({ studio, featuredOnly: featured_only });
14738
15856
  const format = response_format ?? "text";
15857
+ const { data: efData, error: efError } = await callEdgeFunction("mcp-data", { action: "get-skills" });
15858
+ const catalogRows = efData?.skills;
15859
+ if (!efError && Array.isArray(catalogRows) && catalogRows.length > 0) {
15860
+ const workflows = listSkills({ studio, featuredOnly: featured_only });
15861
+ const filteredGuideRows = catalogRows.filter(
15862
+ (row) => !studio && (!featured_only || row.frontmatter?.featured === true)
15863
+ );
15864
+ const guides = filteredGuideRows.map((row) => ({
15865
+ ...row,
15866
+ use_with: "get_skill"
15867
+ }));
15868
+ const total = guides.length + workflows.length;
15869
+ if (format === "json") {
15870
+ return {
15871
+ content: [
15872
+ {
15873
+ type: "text",
15874
+ text: JSON.stringify(
15875
+ asEnvelope26({
15876
+ count: total,
15877
+ guides,
15878
+ workflows: workflows.map((w) => ({
15879
+ ...w,
15880
+ use_with: "run_skill"
15881
+ }))
15882
+ }),
15883
+ null,
15884
+ 2
15885
+ )
15886
+ }
15887
+ ]
15888
+ };
15889
+ }
15890
+ const guideBlocks = filteredGuideRows.map(
15891
+ (row) => `${renderCatalogRow(row)}
15892
+ \u2192 Read it: get_skill(slug: "${row.slug}")`
15893
+ ).join("\n\n");
15894
+ const workflowBlocks = workflows.map(renderSkillSummary).join("\n\n");
15895
+ const header2 = `${total} skill${total === 1 ? "" : "s"} available (${guides.length} guide${guides.length === 1 ? "" : "s"} \xB7 ${workflows.length} workflow${workflows.length === 1 ? "" : "s"})
15896
+ ${"=".repeat(40)}`;
15897
+ const sections = [
15898
+ guides.length > 0 ? `GUIDES \u2014 living how-to documents. Fetch with get_skill(slug).
15899
+
15900
+ ${guideBlocks}` : "",
15901
+ workflows.length > 0 ? `WORKFLOWS \u2014 executable content pipelines. Launch with run_skill(skill_id).
15902
+
15903
+ ${workflowBlocks}` : ""
15904
+ ].filter(Boolean);
15905
+ return {
15906
+ content: [
15907
+ {
15908
+ type: "text",
15909
+ text: `${header2}
15910
+
15911
+ ${sections.join("\n\n")}`
15912
+ }
15913
+ ]
15914
+ };
15915
+ }
15916
+ const skills = listSkills({ studio, featuredOnly: featured_only });
14739
15917
  if (format === "json") {
14740
15918
  return {
14741
15919
  content: [
14742
15920
  {
14743
15921
  type: "text",
14744
- text: JSON.stringify(asEnvelope26({ count: skills.length, skills }), null, 2)
15922
+ text: JSON.stringify(
15923
+ asEnvelope26({ count: skills.length, skills }),
15924
+ null,
15925
+ 2
15926
+ )
14745
15927
  }
14746
15928
  ]
14747
15929
  };
@@ -14751,7 +15933,9 @@ function registerSkillsTools(server2) {
14751
15933
  content: [
14752
15934
  {
14753
15935
  type: "text",
14754
- 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(", ")
15936
+ 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(
15937
+ ", "
15938
+ )
14755
15939
  }
14756
15940
  ]
14757
15941
  };
@@ -14770,18 +15954,22 @@ ${blocks}` }]
14770
15954
  "run_skill",
14771
15955
  "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.",
14772
15956
  {
14773
- skill_id: z35.string().describe(
15957
+ skill_id: z36.string().describe(
14774
15958
  'The id of the skill to run (e.g. "skill-brand-locked-viral-hook-reel"). Use list_skills to discover available ids.'
14775
15959
  ),
14776
- topic: z35.string().min(1).describe('What the content is about (e.g. "why we built Social Neuron").'),
14777
- audience: z35.string().optional().describe(
15960
+ topic: z36.string().min(1).describe(
15961
+ 'What the content is about (e.g. "why we built Social Neuron").'
15962
+ ),
15963
+ audience: z36.string().optional().describe(
14778
15964
  'Who the content is for (e.g. "first-time founders"). Defaults to brand persona.'
14779
15965
  ),
14780
- hook: z35.string().optional().describe(
15966
+ hook: z36.string().optional().describe(
14781
15967
  "Optional explicit hook. If omitted, the skill drafts and scores 3-5 candidates."
14782
15968
  ),
14783
- cta: z35.string().optional().describe("Optional call-to-action override. Defaults to brand standard CTA."),
14784
- response_format: z35.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
15969
+ cta: z36.string().optional().describe(
15970
+ "Optional call-to-action override. Defaults to brand standard CTA."
15971
+ ),
15972
+ response_format: z36.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
14785
15973
  },
14786
15974
  async ({ skill_id, topic, audience, hook, cta, response_format }) => {
14787
15975
  const skill = getSkill(skill_id);
@@ -14849,19 +16037,96 @@ ${blocks}` }]
14849
16037
  };
14850
16038
  }
14851
16039
  );
16040
+ server2.tool(
16041
+ "get_skill",
16042
+ `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).`,
16043
+ {
16044
+ slug: z36.string().min(1).max(100).regex(/^[a-z0-9][a-z0-9-]*$/).describe(
16045
+ 'The skill slug (e.g. "tiktok-content"). Use list_skills to discover slugs.'
16046
+ ),
16047
+ response_format: z36.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
16048
+ },
16049
+ async ({ slug, response_format }) => {
16050
+ const format = response_format ?? "text";
16051
+ const { data, error } = await callEdgeFunction("mcp-data", {
16052
+ action: "get-skill",
16053
+ slug
16054
+ });
16055
+ if (error) {
16056
+ return toolError(
16057
+ "upstream_error",
16058
+ "The skill catalogue could not be loaded. Please retry."
16059
+ );
16060
+ }
16061
+ const skill = data?.skill;
16062
+ if (!skill) {
16063
+ return toolError("not_found", `No skill found with slug "${slug}".`, {
16064
+ recover_with: ["Call list_skills to see available skills."]
16065
+ });
16066
+ }
16067
+ if (skill.locked) {
16068
+ return toolError(
16069
+ "permission_denied",
16070
+ "This skill requires a higher plan tier.",
16071
+ {
16072
+ details: { slug: skill.slug, tier_minimum: skill.tier_minimum },
16073
+ recover_with: [
16074
+ "Upgrade the account or choose an unlocked skill from list_skills."
16075
+ ]
16076
+ }
16077
+ );
16078
+ }
16079
+ if (format === "json") {
16080
+ return {
16081
+ content: [
16082
+ {
16083
+ type: "text",
16084
+ text: JSON.stringify(asEnvelope26(skill), null, 2)
16085
+ }
16086
+ ]
16087
+ };
16088
+ }
16089
+ const platform3 = skill.platform ? ` \xB7 ${skill.platform}` : "";
16090
+ const lock = skill.locked ? " \u{1F512} (upgrade to unlock)" : "";
16091
+ const header = [
16092
+ `${skill.slug} (${skill.kind}${platform3})`,
16093
+ "=".repeat(40),
16094
+ `Tier: ${skill.tier_minimum}${lock} \xB7 version ${skill.version}` + (skill.recipe_slug ? ` \xB7 recipe: ${skill.recipe_slug}` : ""),
16095
+ ""
16096
+ ].join("\n");
16097
+ const compiled = skill.compiled_section ? `
16098
+
16099
+ --- What's working now ---
16100
+ ${skill.compiled_section}` : "";
16101
+ return {
16102
+ content: [
16103
+ { type: "text", text: `${header}${skill.body}${compiled}` }
16104
+ ]
16105
+ };
16106
+ }
16107
+ );
14852
16108
  }
14853
16109
  var STUDIO_VALUES;
14854
16110
  var init_skills = __esm({
14855
16111
  "src/tools/skills.ts"() {
14856
16112
  "use strict";
14857
16113
  init_version();
16114
+ init_tool_error();
16115
+ init_edge_function();
14858
16116
  init_skills_manifest();
14859
- STUDIO_VALUES = ["video", "avatar", "carousel", "voice", "caption", "edit"];
16117
+ STUDIO_VALUES = [
16118
+ "video",
16119
+ "avatar",
16120
+ "carousel",
16121
+ "voice",
16122
+ "caption",
16123
+ "edit"
16124
+ ];
14860
16125
  }
14861
16126
  });
14862
16127
 
14863
16128
  // src/tools/loopPulse.ts
14864
- import { z as z36 } from "zod";
16129
+ import { z as z37 } from "zod";
14865
16130
  function asEnvelope27(data) {
14866
16131
  return {
14867
16132
  _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
@@ -14873,7 +16138,7 @@ function registerLoopPulseTools(server2) {
14873
16138
  "get_loop_pulse",
14874
16139
  '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.',
14875
16140
  {
14876
- response_format: z36.enum(["text", "json"]).optional().describe("Output format. Defaults to text.")
16141
+ response_format: z37.enum(["text", "json"]).optional().describe("Output format. Defaults to text.")
14877
16142
  },
14878
16143
  async ({ response_format }) => {
14879
16144
  const format = response_format ?? "text";
@@ -14923,7 +16188,7 @@ var init_loopPulse = __esm({
14923
16188
  });
14924
16189
 
14925
16190
  // src/tools/banditState.ts
14926
- import { z as z37 } from "zod";
16191
+ import { z as z38 } from "zod";
14927
16192
  function asEnvelope28(data) {
14928
16193
  return {
14929
16194
  _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
@@ -14935,8 +16200,8 @@ function registerBanditStateTools(server2) {
14935
16200
  "get_bandit_state",
14936
16201
  "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).",
14937
16202
  {
14938
- project_id: z37.string().uuid().optional().describe("Project UUID. Defaults to the authenticated user default project."),
14939
- platform: z37.enum([
16203
+ project_id: z38.string().uuid().optional().describe("Project UUID. Defaults to the authenticated user default project."),
16204
+ platform: z38.enum([
14940
16205
  "instagram",
14941
16206
  "tiktok",
14942
16207
  "youtube",
@@ -14948,7 +16213,7 @@ function registerBanditStateTools(server2) {
14948
16213
  ]).optional().describe(
14949
16214
  "Lowercase platform name. Omit to return both platform-scoped and legacy platform-agnostic arms."
14950
16215
  ),
14951
- arm_type: z37.enum([
16216
+ arm_type: z38.enum([
14952
16217
  "hook_family",
14953
16218
  "hook_type",
14954
16219
  "format",
@@ -14962,8 +16227,8 @@ function registerBanditStateTools(server2) {
14962
16227
  "posting_time_bucket",
14963
16228
  "content_format"
14964
16229
  ]).optional().describe("Arm dimension. Omit to return all types grouped."),
14965
- top_k: z37.number().min(1).max(50).optional().describe("Max arms per (arm_type) group. Default 5."),
14966
- response_format: z37.enum(["text", "json"]).optional().describe("Output format. Defaults to text.")
16230
+ top_k: z38.number().min(1).max(50).optional().describe("Max arms per (arm_type) group. Default 5."),
16231
+ response_format: z38.enum(["text", "json"]).optional().describe("Output format. Defaults to text.")
14967
16232
  },
14968
16233
  async ({ project_id, platform: platform3, arm_type, top_k, response_format }) => {
14969
16234
  const format = response_format ?? "text";
@@ -15026,12 +16291,134 @@ var init_banditState = __esm({
15026
16291
  }
15027
16292
  });
15028
16293
 
16294
+ // src/tools/lifecycle.ts
16295
+ import { z as z39 } from "zod";
16296
+ function envelope(data) {
16297
+ return {
16298
+ _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
16299
+ data
16300
+ };
16301
+ }
16302
+ function lifecycleFailure(error) {
16303
+ if (/not_found|not found/i.test(error)) {
16304
+ return toolError("not_found", "The requested object was not found in this project.");
16305
+ }
16306
+ if (/not_cancellable|publishing_in_progress|schedule_conflict|post_in_progress/i.test(error)) {
16307
+ return toolError("validation_error", "The object can no longer be cancelled in its current state.", {
16308
+ recover_with: ["Refresh its status before deciding the next action."]
16309
+ });
16310
+ }
16311
+ return toolError("upstream_error", "The lifecycle operation could not be completed. Please retry.");
16312
+ }
16313
+ async function projectContext(projectId) {
16314
+ return projectId ?? await getDefaultProjectId();
16315
+ }
16316
+ async function invokeLifecycle(action, projectId, identifiers) {
16317
+ const resolvedProjectId = await projectContext(projectId);
16318
+ if (!resolvedProjectId) {
16319
+ return toolError("validation_error", "A project_id is required because no default project is configured.");
16320
+ }
16321
+ const { data, error } = await callEdgeFunction("mcp-data", {
16322
+ action,
16323
+ projectId: resolvedProjectId,
16324
+ project_id: resolvedProjectId,
16325
+ ...identifiers
16326
+ });
16327
+ if (error) return lifecycleFailure(error);
16328
+ if (!data?.success) return toolError("upstream_error", "The lifecycle operation returned no result.");
16329
+ const result = envelope(data);
16330
+ return {
16331
+ structuredContent: result,
16332
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
16333
+ };
16334
+ }
16335
+ function registerLifecycleTools(server2) {
16336
+ server2.tool(
16337
+ "cancel_async_job",
16338
+ "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.",
16339
+ {
16340
+ job_id: z39.string().uuid().describe("Owned async_jobs ID returned by a generation tool."),
16341
+ project_id: PROJECT_ID,
16342
+ confirm: CONFIRM
16343
+ },
16344
+ async ({ job_id, project_id }) => invokeLifecycle("cancel-async-job", project_id, { job_id })
16345
+ );
16346
+ server2.tool(
16347
+ "cancel_scheduled_post",
16348
+ "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.",
16349
+ {
16350
+ post_id: z39.string().uuid().describe("Owned scheduled post ID."),
16351
+ project_id: PROJECT_ID,
16352
+ confirm: CONFIRM
16353
+ },
16354
+ async ({ post_id, project_id }) => invokeLifecycle("cancel-scheduled-post", project_id, { post_id })
16355
+ );
16356
+ server2.tool(
16357
+ "delete_carousel",
16358
+ "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.",
16359
+ {
16360
+ content_id: z39.string().uuid().describe("Owned carousel content_history ID."),
16361
+ project_id: PROJECT_ID,
16362
+ confirm: CONFIRM
16363
+ },
16364
+ async ({ content_id, project_id }) => invokeLifecycle("delete-carousel", project_id, { content_id })
16365
+ );
16366
+ server2.tool(
16367
+ "delete_content_plan",
16368
+ "Permanently delete an owned content plan in one project. This does not cancel posts that were already scheduled from the plan.",
16369
+ {
16370
+ plan_id: z39.string().uuid().describe("Owned content plan ID."),
16371
+ project_id: PROJECT_ID,
16372
+ confirm: CONFIRM
16373
+ },
16374
+ async ({ plan_id, project_id }) => invokeLifecycle("delete-content-plan", project_id, { plan_id })
16375
+ );
16376
+ server2.tool(
16377
+ "delete_autopilot_config",
16378
+ "Permanently delete an owned autopilot configuration in one project. Historical runs and already-published posts are retained.",
16379
+ {
16380
+ config_id: z39.string().uuid().describe("Owned autopilot configuration ID."),
16381
+ project_id: PROJECT_ID,
16382
+ confirm: CONFIRM
16383
+ },
16384
+ async ({ config_id, project_id }) => invokeLifecycle("delete-autopilot-config", project_id, { config_id })
16385
+ );
16386
+ }
16387
+ var PROJECT_ID, CONFIRM;
16388
+ var init_lifecycle = __esm({
16389
+ "src/tools/lifecycle.ts"() {
16390
+ "use strict";
16391
+ init_edge_function();
16392
+ init_supabase();
16393
+ init_tool_error();
16394
+ init_version();
16395
+ PROJECT_ID = z39.string().uuid().optional().describe("Brand/project ID. Defaults to the authenticated key's project or account default.");
16396
+ CONFIRM = z39.literal(true).describe("Must be true only after the user explicitly confirms this destructive action.");
16397
+ }
16398
+ });
16399
+
15029
16400
  // src/lib/register-tools.ts
16401
+ function redactBase64Payloads(args) {
16402
+ if (typeof args !== "object" || args === null || Array.isArray(args)) return args;
16403
+ let changed = false;
16404
+ const out = { ...args };
16405
+ for (const key of BASE64_PAYLOAD_KEYS) {
16406
+ const value = out[key];
16407
+ if (typeof value !== "string" || value.length < 64) continue;
16408
+ const body = value.replace(DATA_URI_PREFIX2, "");
16409
+ if (STRICT_BASE64.test(body)) {
16410
+ out[key] = `[base64:${body.length} chars omitted from prose scan]`;
16411
+ changed = true;
16412
+ }
16413
+ }
16414
+ return changed ? out : args;
16415
+ }
15030
16416
  function wrapToolWithScanner(toolName, handler) {
15031
16417
  return async function scannerWrappedHandler(...handlerArgs) {
15032
16418
  const args = handlerArgs[0];
15033
16419
  const ctx = handlerArgs[1];
15034
- const inputText = args === void 0 ? "{}" : typeof args === "string" ? args : JSON.stringify(args);
16420
+ const scanArgs = redactBase64Payloads(args);
16421
+ const inputText = scanArgs === void 0 ? "{}" : typeof scanArgs === "string" ? scanArgs : JSON.stringify(scanArgs);
15035
16422
  const inputScan = scan(inputText, {
15036
16423
  mode: "block",
15037
16424
  source: "mcp_tool_input",
@@ -15059,6 +16446,16 @@ function wrapToolWithScanner(toolName, handler) {
15059
16446
  source: "mcp_tool_output",
15060
16447
  user_id: ctx?.userId
15061
16448
  });
16449
+ if (!outputScan.passed) {
16450
+ try {
16451
+ ctx?.logScan?.(toolName, "output", outputScan);
16452
+ } catch {
16453
+ }
16454
+ return toolError(
16455
+ "server_error",
16456
+ "The response exceeded the safe output limit and was not returned."
16457
+ );
16458
+ }
15062
16459
  if (outputScan.sanitized_text !== void 0) {
15063
16460
  try {
15064
16461
  ctx?.logScan?.(toolName, "output", outputScan);
@@ -15067,7 +16464,10 @@ function wrapToolWithScanner(toolName, handler) {
15067
16464
  try {
15068
16465
  return JSON.parse(outputScan.sanitized_text);
15069
16466
  } catch {
15070
- return result;
16467
+ return toolError(
16468
+ "server_error",
16469
+ "The response could not be returned safely. Please retry or contact support."
16470
+ );
15071
16471
  }
15072
16472
  }
15073
16473
  return result;
@@ -15117,8 +16517,7 @@ function applyScopeEnforcement(server2, scopeResolver) {
15117
16517
  details: {
15118
16518
  source: "wrapper",
15119
16519
  // A thrown exception escaped the handler — an unclassified fault.
15120
- error_type: "server_error",
15121
- exception: err instanceof Error ? err.message.slice(0, 200) : "unknown"
16520
+ error_type: "server_error"
15122
16521
  }
15123
16522
  });
15124
16523
  throw err;
@@ -15250,12 +16649,14 @@ function registerAllTools(server2, options) {
15250
16649
  registerSkillsTools(server2);
15251
16650
  registerLoopPulseTools(server2);
15252
16651
  registerBanditStateTools(server2);
16652
+ registerLifecycleTools(server2);
15253
16653
  if (!options?.skipApps) {
15254
16654
  registerContentCalendarApp(server2);
16655
+ registerAnalyticsPulseApp(server2);
15255
16656
  }
15256
16657
  applyAnnotations(server2);
15257
16658
  }
15258
- var RESPONSE_CHAR_LIMIT;
16659
+ var BASE64_PAYLOAD_KEYS, DATA_URI_PREFIX2, STRICT_BASE64, RESPONSE_CHAR_LIMIT;
15259
16660
  var init_register_tools = __esm({
15260
16661
  "src/lib/register-tools.ts"() {
15261
16662
  "use strict";
@@ -15297,18 +16698,23 @@ var init_register_tools = __esm({
15297
16698
  init_niche_research();
15298
16699
  init_hyperframes();
15299
16700
  init_content_calendar();
16701
+ init_analytics_pulse();
15300
16702
  init_connections();
15301
16703
  init_harness();
15302
16704
  init_hermes();
15303
16705
  init_skills();
15304
16706
  init_loopPulse();
15305
16707
  init_banditState();
16708
+ init_lifecycle();
16709
+ BASE64_PAYLOAD_KEYS = /* @__PURE__ */ new Set(["file_data", "fileData"]);
16710
+ DATA_URI_PREFIX2 = /^data:[\w.+-]+\/[\w.+-]+;base64,/;
16711
+ STRICT_BASE64 = /^[A-Za-z0-9+/]+={0,2}$/;
15306
16712
  RESPONSE_CHAR_LIMIT = 1e5;
15307
16713
  }
15308
16714
  });
15309
16715
 
15310
16716
  // src/cli/sn/parse.ts
15311
- import { createHash as createHash3 } from "node:crypto";
16717
+ import { createHash as createHash2 } from "node:crypto";
15312
16718
  function parseSnArgs(argv) {
15313
16719
  const parsed = { _: [] };
15314
16720
  for (let i = 0; i < argv.length; i += 1) {
@@ -15338,8 +16744,8 @@ function isEnabledFlag(value) {
15338
16744
  }
15339
16745
  function emitSnResult(payload, asJson) {
15340
16746
  if (asJson) {
15341
- const envelope = { schema_version: "1", ...payload };
15342
- process.stdout.write(JSON.stringify(envelope, null, 2) + "\n");
16747
+ const envelope2 = { schema_version: "1", ...payload };
16748
+ process.stdout.write(JSON.stringify(envelope2, null, 2) + "\n");
15343
16749
  }
15344
16750
  }
15345
16751
  function isValidHttpsUrl(value) {
@@ -15374,7 +16780,7 @@ async function checkUrlReachability(url) {
15374
16780
  } catch (error) {
15375
16781
  return {
15376
16782
  ok: false,
15377
- error: error instanceof Error ? error.message : String(error)
16783
+ error: "Network request failed."
15378
16784
  };
15379
16785
  } finally {
15380
16786
  clearTimeout(timer);
@@ -15404,7 +16810,7 @@ function buildPublishIdempotencyKey(input) {
15404
16810
  title: input.title ?? "",
15405
16811
  scheduledAt: input.scheduledAt ?? ""
15406
16812
  });
15407
- return `sn_${createHash3("sha256").update(material).digest("hex").slice(0, 24)}`;
16813
+ return `sn_${createHash2("sha256").update(material).digest("hex").slice(0, 24)}`;
15408
16814
  }
15409
16815
  function normalizePlatforms(platformsRaw) {
15410
16816
  const caseMap = {
@@ -16464,17 +17870,21 @@ __export(discovery_exports, {
16464
17870
  handleInfo: () => handleInfo,
16465
17871
  handleTools: () => handleTools
16466
17872
  });
17873
+ function publicStdioTools() {
17874
+ return TOOL_CATALOG.filter(
17875
+ (t) => !t.internal && !t.hiddenFromPublicCount && t.module !== "apps"
17876
+ );
17877
+ }
16467
17878
  async function handleTools(args, asJson) {
16468
- let tools = TOOL_CATALOG;
17879
+ let tools = publicStdioTools();
16469
17880
  const scope = args.scope;
16470
17881
  if (typeof scope === "string") {
16471
- tools = getToolsByScope(scope);
17882
+ tools = tools.filter((tool) => tool.scope === scope);
16472
17883
  }
16473
17884
  const module = args.module;
16474
17885
  if (typeof module === "string") {
16475
- tools = getToolsByModule(module);
17886
+ tools = tools.filter((tool) => tool.module === module);
16476
17887
  }
16477
- tools = tools.filter((t) => !t.internal);
16478
17888
  if (asJson) {
16479
17889
  emitSnResult({ ok: true, command: "tools", toolCount: tools.length, tools }, true);
16480
17890
  return;
@@ -16503,27 +17913,31 @@ Module: ${moduleName} (${moduleTools.length} tools)`);
16503
17913
  process.exit(0);
16504
17914
  }
16505
17915
  async function handleInfo(args, asJson) {
17916
+ const stdioTools = publicStdioTools();
16506
17917
  const info = {
16507
17918
  version: MCP_VERSION,
16508
- toolCount: TOOL_CATALOG.filter((t) => !t.internal).length,
16509
- modules: getModules()
17919
+ toolCount: stdioTools.length,
17920
+ modules: Array.from(new Set(stdioTools.map((tool) => tool.module))).sort(),
17921
+ auth: null
16510
17922
  };
16511
- try {
16512
- const { loadApiKey: loadApiKey2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
16513
- const { validateApiKey: validateApiKey2 } = await Promise.resolve().then(() => (init_api_keys(), api_keys_exports));
16514
- const apiKey = await loadApiKey2();
16515
- if (apiKey) {
16516
- const result = await validateApiKey2(apiKey);
16517
- if (result.valid) {
16518
- info.auth = {
16519
- email: result.email || null,
16520
- scopes: result.scopes || [],
16521
- expiresAt: result.expiresAt || null
16522
- };
17923
+ if (!isEnabledFlag(args.offline)) {
17924
+ try {
17925
+ const { loadApiKey: loadApiKey2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
17926
+ const { validateApiKey: validateApiKey2 } = await Promise.resolve().then(() => (init_api_keys(), api_keys_exports));
17927
+ const apiKey = await loadApiKey2();
17928
+ if (apiKey) {
17929
+ const result = await validateApiKey2(apiKey);
17930
+ if (result.valid) {
17931
+ info.auth = {
17932
+ email: result.email || null,
17933
+ scopes: result.scopes || [],
17934
+ expiresAt: result.expiresAt || null
17935
+ };
17936
+ }
16523
17937
  }
17938
+ } catch {
17939
+ info.auth = null;
16524
17940
  }
16525
- } catch {
16526
- info.auth = null;
16527
17941
  }
16528
17942
  if (info.auth) {
16529
17943
  try {
@@ -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
  server2.close();
17440
18875
  resolve3({ error: "Authorization timed out after 120 seconds." });
17441
18876
  }, 12e4);
17442
18877
  server2.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) {
@@ -18165,17 +19606,17 @@ import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js
18165
19606
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
18166
19607
 
18167
19608
  // src/prompts.ts
18168
- import { z as z38 } from "zod";
19609
+ import { z as z40 } from "zod";
18169
19610
  function registerPrompts(server2) {
18170
19611
  server2.prompt(
18171
19612
  "create_weekly_content_plan",
18172
19613
  "Generate a full week of social media content (7 days, multiple platforms). Returns a structured plan with topics, formats, and posting times.",
18173
19614
  {
18174
- niche: z38.string().describe('Your content niche or industry (e.g., "fitness coaching", "SaaS marketing")'),
18175
- platforms: z38.string().optional().describe(
19615
+ niche: z40.string().describe('Your content niche or industry (e.g., "fitness coaching", "SaaS marketing")'),
19616
+ platforms: z40.string().optional().describe(
18176
19617
  'Comma-separated platforms to target (default: "YouTube, Instagram, TikTok, LinkedIn")'
18177
19618
  ),
18178
- tone: z38.string().optional().describe('Brand tone of voice (e.g., "professional", "casual", "bold and edgy")')
19619
+ tone: z40.string().optional().describe('Brand tone of voice (e.g., "professional", "casual", "bold and edgy")')
18179
19620
  },
18180
19621
  ({ niche, platforms, tone }) => {
18181
19622
  const targetPlatforms = platforms || "YouTube, Instagram, TikTok, LinkedIn";
@@ -18217,8 +19658,8 @@ After building the plan, use \`save_content_plan\` to save it.`
18217
19658
  "analyze_top_content",
18218
19659
  "Analyze your best-performing posts to identify patterns and replicate success. Returns insights on hooks, formats, timing, and topics that resonate.",
18219
19660
  {
18220
- timeframe: z38.string().optional().describe('Analysis period (default: "30 days"). E.g., "7 days", "90 days"'),
18221
- platform: z38.string().optional().describe('Filter to a specific platform (e.g., "youtube", "instagram")')
19661
+ timeframe: z40.string().optional().describe('Analysis period (default: "30 days"). E.g., "7 days", "90 days"'),
19662
+ platform: z40.string().optional().describe('Filter to a specific platform (e.g., "youtube", "instagram")')
18222
19663
  },
18223
19664
  ({ timeframe, platform: platform3 }) => {
18224
19665
  const period = timeframe || "30 days";
@@ -18255,10 +19696,10 @@ Format as a clear, actionable performance report.`
18255
19696
  "repurpose_content",
18256
19697
  "Take one piece of content and transform it into 8-10 pieces across multiple platforms and formats.",
18257
19698
  {
18258
- source: z38.string().describe(
19699
+ source: z40.string().describe(
18259
19700
  "The source content to repurpose \u2014 a URL, transcript, or the content text itself"
18260
19701
  ),
18261
- target_platforms: z38.string().optional().describe(
19702
+ target_platforms: z40.string().optional().describe(
18262
19703
  'Comma-separated target platforms (default: "Twitter, LinkedIn, Instagram, TikTok, YouTube")'
18263
19704
  )
18264
19705
  },
@@ -18300,9 +19741,9 @@ For each piece, include the platform, format, character count, and suggested pos
18300
19741
  "setup_brand_voice",
18301
19742
  "Define or refine your brand voice profile so all generated content stays on-brand. Walks through tone, audience, values, and style.",
18302
19743
  {
18303
- brand_name: z38.string().describe("Your brand or business name"),
18304
- industry: z38.string().optional().describe('Your industry or niche (e.g., "B2B SaaS", "fitness coaching")'),
18305
- website: z38.string().optional().describe("Your website URL for context")
19744
+ brand_name: z40.string().describe("Your brand or business name"),
19745
+ industry: z40.string().optional().describe('Your industry or niche (e.g., "B2B SaaS", "fitness coaching")'),
19746
+ website: z40.string().optional().describe("Your website URL for context")
18306
19747
  },
18307
19748
  ({ brand_name, industry, website }) => {
18308
19749
  const industryContext = industry ? ` in the ${industry} space` : "";
@@ -18706,10 +20147,10 @@ var command = process.argv[2];
18706
20147
  if (command === "--version" || command === "-v") {
18707
20148
  const { readFileSync: readFileSync5 } = await import("node:fs");
18708
20149
  const { resolve: resolve3, dirname } = await import("node:path");
18709
- const { fileURLToPath } = await import("node:url");
20150
+ const { fileURLToPath: fileURLToPath3 } = await import("node:url");
18710
20151
  let version = MCP_VERSION;
18711
20152
  try {
18712
- const pkgPath = resolve3(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
20153
+ const pkgPath = resolve3(dirname(fileURLToPath3(import.meta.url)), "..", "package.json");
18713
20154
  const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
18714
20155
  version = pkg.version;
18715
20156
  } catch {