@socialneuron/mcp-server 1.8.0 → 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/CHANGELOG.md +39 -0
- package/README.md +76 -47
- package/dist/apps/analytics-pulse/mcp-app.html +167 -0
- package/dist/apps/content-calendar/mcp-app.html +475 -0
- package/dist/http.js +2086 -480
- package/dist/index.js +1868 -373
- package/dist/sn.js +1893 -398
- package/package.json +15 -12
- package/tools.lock.json +116 -101
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.
|
|
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 {
|
|
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 (
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
401
|
-
|
|
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})
|
|
748
|
+
error: `Validation failed (HTTP ${response.status}).`
|
|
622
749
|
};
|
|
623
750
|
}
|
|
624
|
-
|
|
625
|
-
|
|
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:
|
|
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 {
|
|
789
|
+
import { createHmac, randomBytes } from "node:crypto";
|
|
655
790
|
import { PostHog } from "posthog-node";
|
|
656
791
|
function hashUserId(userId) {
|
|
657
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1949
|
-
|
|
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:
|
|
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:
|
|
1964
|
-
error: `Forbidden (HTTP 403): ${
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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("
|
|
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("
|
|
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
|
|
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
|
|
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("
|
|
2977
|
+
const rateLimit = checkRateLimit("generation", `generate_video:${userId}`);
|
|
2681
2978
|
if (!rateLimit.allowed) {
|
|
2682
2979
|
return {
|
|
2683
2980
|
content: [
|
|
@@ -2775,7 +3072,7 @@ function registerContentTools(server2) {
|
|
|
2775
3072
|
);
|
|
2776
3073
|
server2.tool(
|
|
2777
3074
|
"generate_image",
|
|
2778
|
-
"Start an async AI image generation job \u2014 returns a job_id immediately. Poll with check_status every 5-15s until complete. Costs
|
|
3075
|
+
"Start an async AI image generation job \u2014 returns a job_id immediately. Poll with check_status every 5-15s until complete. Costs 15-50 credits depending on model. Use for social media posts, carousel slides, or as input to generate_video (image-to-video). Pass project_id so the asset is stored with the correct brand/project.",
|
|
2779
3076
|
{
|
|
2780
3077
|
prompt: z2.string().max(2e3).describe(
|
|
2781
3078
|
"Text prompt describing the image to generate. Be specific about style, composition, colors, lighting, and subject matter."
|
|
@@ -2797,9 +3094,10 @@ function registerContentTools(server2) {
|
|
|
2797
3094
|
image_url: z2.string().optional().describe(
|
|
2798
3095
|
"Reference image URL for image-to-image generation. Required for ideogram model. Optional for others."
|
|
2799
3096
|
),
|
|
3097
|
+
project_id: z2.string().optional().describe("Project ID to associate the generated image with."),
|
|
2800
3098
|
response_format: z2.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
2801
3099
|
},
|
|
2802
|
-
async ({ prompt: prompt2, model, aspect_ratio, image_url, response_format }) => {
|
|
3100
|
+
async ({ prompt: prompt2, model, aspect_ratio, image_url, project_id, response_format }) => {
|
|
2803
3101
|
const format = response_format ?? "text";
|
|
2804
3102
|
const userId = await getDefaultUserId();
|
|
2805
3103
|
const assetBudget = checkAssetBudget();
|
|
@@ -2817,7 +3115,7 @@ function registerContentTools(server2) {
|
|
|
2817
3115
|
isError: true
|
|
2818
3116
|
};
|
|
2819
3117
|
}
|
|
2820
|
-
const rateLimit = checkRateLimit("
|
|
3118
|
+
const rateLimit = checkRateLimit("generation", `generate_image:${userId}`);
|
|
2821
3119
|
if (!rateLimit.allowed) {
|
|
2822
3120
|
return {
|
|
2823
3121
|
content: [
|
|
@@ -2835,7 +3133,8 @@ function registerContentTools(server2) {
|
|
|
2835
3133
|
prompt: prompt2,
|
|
2836
3134
|
model,
|
|
2837
3135
|
aspectRatio: aspect_ratio ?? "1:1",
|
|
2838
|
-
imageUrl: image_url
|
|
3136
|
+
imageUrl: image_url,
|
|
3137
|
+
...project_id && { projectId: project_id }
|
|
2839
3138
|
},
|
|
2840
3139
|
{ timeoutMs: 3e4 }
|
|
2841
3140
|
);
|
|
@@ -2874,7 +3173,8 @@ function registerContentTools(server2) {
|
|
|
2874
3173
|
jobId,
|
|
2875
3174
|
taskId: data.taskId,
|
|
2876
3175
|
asyncJobId: data.asyncJobId,
|
|
2877
|
-
model: data.model
|
|
3176
|
+
model: data.model,
|
|
3177
|
+
projectId: project_id ?? null
|
|
2878
3178
|
}),
|
|
2879
3179
|
null,
|
|
2880
3180
|
2
|
|
@@ -2955,20 +3255,28 @@ function registerContentTools(server2) {
|
|
|
2955
3255
|
}
|
|
2956
3256
|
);
|
|
2957
3257
|
if (liveStatus) {
|
|
3258
|
+
const livePayload = buildCheckStatusPayload(job, liveStatus);
|
|
2958
3259
|
const lines2 = [
|
|
2959
3260
|
`Job: ${job.id}`,
|
|
2960
3261
|
`Type: ${job.job_type}`,
|
|
2961
3262
|
`Model: ${job.model}`,
|
|
2962
|
-
`Status: ${
|
|
2963
|
-
`Progress: ${
|
|
3263
|
+
`Status: ${livePayload.status}`,
|
|
3264
|
+
`Progress: ${livePayload.progress}%`
|
|
2964
3265
|
];
|
|
2965
|
-
if (
|
|
2966
|
-
lines2.push(`Result URL: ${
|
|
3266
|
+
if (livePayload.result_url) {
|
|
3267
|
+
lines2.push(`Result URL: ${livePayload.result_url}`);
|
|
2967
3268
|
}
|
|
2968
|
-
if (
|
|
2969
|
-
lines2.push(`Error: ${
|
|
3269
|
+
if (livePayload.error) {
|
|
3270
|
+
lines2.push(`Error: ${livePayload.error}`);
|
|
2970
3271
|
}
|
|
3272
|
+
const fallbackDisclosure2 = buildFallbackDisclosureLine(job.result_metadata);
|
|
3273
|
+
if (fallbackDisclosure2) lines2.push(fallbackDisclosure2);
|
|
2971
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
|
+
}
|
|
2972
3280
|
lines2.push(`Created: ${job.created_at}`);
|
|
2973
3281
|
if (format === "json") {
|
|
2974
3282
|
return {
|
|
@@ -2986,7 +3294,7 @@ function registerContentTools(server2) {
|
|
|
2986
3294
|
// `job` + `liveStatus`; do not duplicate them here.
|
|
2987
3295
|
asEnvelope2({
|
|
2988
3296
|
...liveStatus,
|
|
2989
|
-
...
|
|
3297
|
+
...livePayload
|
|
2990
3298
|
}),
|
|
2991
3299
|
null,
|
|
2992
3300
|
2
|
|
@@ -3029,7 +3337,14 @@ function registerContentTools(server2) {
|
|
|
3029
3337
|
if (job.error_message) {
|
|
3030
3338
|
lines.push(`Error: ${job.error_message}`);
|
|
3031
3339
|
}
|
|
3340
|
+
const fallbackDisclosure = buildFallbackDisclosureLine(job.result_metadata);
|
|
3341
|
+
if (fallbackDisclosure) lines.push(fallbackDisclosure);
|
|
3032
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
|
+
}
|
|
3033
3348
|
lines.push(`Created: ${job.created_at}`);
|
|
3034
3349
|
if (job.completed_at) {
|
|
3035
3350
|
lines.push(`Completed: ${job.completed_at}`);
|
|
@@ -3055,7 +3370,7 @@ function registerContentTools(server2) {
|
|
|
3055
3370
|
);
|
|
3056
3371
|
server2.tool(
|
|
3057
3372
|
"create_storyboard",
|
|
3058
|
-
"Plan a multi-scene video storyboard with AI-generated prompts, durations, captions, and voiceover text per frame. Use before generate_video or generate_image to create cohesive multi-shot content. Include brand_context from get_brand_profile for consistent
|
|
3373
|
+
"Plan a multi-scene video storyboard with AI-generated prompts, durations, captions, and voiceover text per frame. Use before generate_video or generate_image to create cohesive multi-shot content. Include brand_context from get_brand_profile and project_id for consistent, project-scoped production. Costs 10 credits.",
|
|
3059
3374
|
{
|
|
3060
3375
|
concept: z2.string().max(2e3).describe(
|
|
3061
3376
|
'The video concept/idea. Include: hook, key messages, target audience, and desired outcome (e.g., "TikTok ad for VPN app targeting privacy-conscious millennials, hook with shocking stat about data leaks").'
|
|
@@ -3079,6 +3394,7 @@ function registerContentTools(server2) {
|
|
|
3079
3394
|
style: z2.string().optional().describe(
|
|
3080
3395
|
'Visual style direction (e.g., "cinematic", "anime", "documentary", "motion graphics").'
|
|
3081
3396
|
),
|
|
3397
|
+
project_id: z2.string().optional().describe("Project ID for brand-scoped generation and attribution."),
|
|
3082
3398
|
response_format: z2.enum(["text", "json"]).optional().describe(
|
|
3083
3399
|
"Response format. Defaults to json for structured storyboard data."
|
|
3084
3400
|
)
|
|
@@ -3090,6 +3406,7 @@ function registerContentTools(server2) {
|
|
|
3090
3406
|
target_duration,
|
|
3091
3407
|
num_scenes,
|
|
3092
3408
|
style,
|
|
3409
|
+
project_id,
|
|
3093
3410
|
response_format
|
|
3094
3411
|
}) => {
|
|
3095
3412
|
const format = response_format ?? "json";
|
|
@@ -3172,7 +3489,8 @@ Return ONLY valid JSON in this exact format:
|
|
|
3172
3489
|
prompt: storyboardPrompt,
|
|
3173
3490
|
type: "storyboard",
|
|
3174
3491
|
model: "gemini-2.5-flash",
|
|
3175
|
-
responseFormat: "json"
|
|
3492
|
+
responseFormat: "json",
|
|
3493
|
+
...project_id && { projectId: project_id }
|
|
3176
3494
|
},
|
|
3177
3495
|
{ timeoutMs: 6e4 }
|
|
3178
3496
|
);
|
|
@@ -3187,7 +3505,18 @@ Return ONLY valid JSON in this exact format:
|
|
|
3187
3505
|
isError: true
|
|
3188
3506
|
};
|
|
3189
3507
|
}
|
|
3190
|
-
const rawContent = data?.content
|
|
3508
|
+
const rawContent = data?.content?.trim() || data?.text?.trim() || "";
|
|
3509
|
+
if (!rawContent) {
|
|
3510
|
+
return {
|
|
3511
|
+
content: [
|
|
3512
|
+
{
|
|
3513
|
+
type: "text",
|
|
3514
|
+
text: "Storyboard generation failed: the AI service returned an empty response."
|
|
3515
|
+
}
|
|
3516
|
+
],
|
|
3517
|
+
isError: true
|
|
3518
|
+
};
|
|
3519
|
+
}
|
|
3191
3520
|
addCreditsUsed(estimatedCost);
|
|
3192
3521
|
if (format === "json") {
|
|
3193
3522
|
try {
|
|
@@ -3223,16 +3552,17 @@ Return ONLY valid JSON in this exact format:
|
|
|
3223
3552
|
);
|
|
3224
3553
|
server2.tool(
|
|
3225
3554
|
"generate_voiceover",
|
|
3226
|
-
"Generate a voiceover audio file for video narration. Returns an R2-hosted audio URL. Use after create_storyboard to add narration to each scene, or standalone for podcast intros and ad reads. Costs
|
|
3555
|
+
"Generate a voiceover audio file for video narration. Returns an R2-hosted audio URL. Use after create_storyboard to add narration to each scene, or standalone for podcast intros and ad reads. Pass project_id to keep the asset with the correct brand/project. Costs 15 credits per generation.",
|
|
3227
3556
|
{
|
|
3228
3557
|
text: z2.string().max(5e3).describe("The script/text to convert to speech."),
|
|
3229
3558
|
voice: z2.enum(["rachel", "domi"]).optional().describe(
|
|
3230
3559
|
"Voice selection. rachel=warm female, domi=confident female. Defaults to rachel."
|
|
3231
3560
|
),
|
|
3232
3561
|
speed: z2.number().min(0.5).max(2).optional().describe("Speech speed multiplier. 1.0 is normal. Defaults to 1.0."),
|
|
3562
|
+
project_id: z2.string().optional().describe("Project ID to associate the generated voiceover with."),
|
|
3233
3563
|
response_format: z2.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
|
|
3234
3564
|
},
|
|
3235
|
-
async ({ text, voice, speed, response_format }) => {
|
|
3565
|
+
async ({ text, voice, speed, project_id, response_format }) => {
|
|
3236
3566
|
const format = response_format ?? "text";
|
|
3237
3567
|
const userId = await getDefaultUserId();
|
|
3238
3568
|
const estimatedCost = 15;
|
|
@@ -3244,7 +3574,7 @@ Return ONLY valid JSON in this exact format:
|
|
|
3244
3574
|
};
|
|
3245
3575
|
}
|
|
3246
3576
|
const rateLimit = checkRateLimit(
|
|
3247
|
-
"
|
|
3577
|
+
"generation",
|
|
3248
3578
|
`generate_voiceover:${userId}`
|
|
3249
3579
|
);
|
|
3250
3580
|
if (!rateLimit.allowed) {
|
|
@@ -3267,7 +3597,8 @@ Return ONLY valid JSON in this exact format:
|
|
|
3267
3597
|
{
|
|
3268
3598
|
text,
|
|
3269
3599
|
voiceId: ELEVENLABS_VOICE_IDS[voice ?? "rachel"],
|
|
3270
|
-
speed: speed ?? 1
|
|
3600
|
+
speed: speed ?? 1,
|
|
3601
|
+
...project_id && { projectId: project_id }
|
|
3271
3602
|
},
|
|
3272
3603
|
{ timeoutMs: 6e4 }
|
|
3273
3604
|
);
|
|
@@ -3303,7 +3634,8 @@ Return ONLY valid JSON in this exact format:
|
|
|
3303
3634
|
asEnvelope2({
|
|
3304
3635
|
audioUrl: data.audioUrl,
|
|
3305
3636
|
durationSeconds: data.durationSeconds,
|
|
3306
|
-
voice: voice ?? "rachel"
|
|
3637
|
+
voice: voice ?? "rachel",
|
|
3638
|
+
projectId: project_id ?? null
|
|
3307
3639
|
}),
|
|
3308
3640
|
null,
|
|
3309
3641
|
2
|
|
@@ -3409,7 +3741,7 @@ Return ONLY valid JSON in this exact format:
|
|
|
3409
3741
|
}
|
|
3410
3742
|
const userId = await getDefaultUserId();
|
|
3411
3743
|
const rateLimit = checkRateLimit(
|
|
3412
|
-
"
|
|
3744
|
+
"generation",
|
|
3413
3745
|
`generate_carousel:${userId}`
|
|
3414
3746
|
);
|
|
3415
3747
|
if (!rateLimit.allowed) {
|
|
@@ -3563,9 +3895,6 @@ var init_content = __esm({
|
|
|
3563
3895
|
// src/lib/sanitize-error.ts
|
|
3564
3896
|
function sanitizeError(error) {
|
|
3565
3897
|
const msg = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
|
|
3566
|
-
if (process.env.NODE_ENV !== "production") {
|
|
3567
|
-
console.error("[Error]", msg);
|
|
3568
|
-
}
|
|
3569
3898
|
for (const [pattern, userMessage] of ERROR_PATTERNS) {
|
|
3570
3899
|
if (pattern.test(msg)) {
|
|
3571
3900
|
return userMessage;
|
|
@@ -3779,6 +4108,7 @@ function evaluateQuality(input) {
|
|
|
3779
4108
|
const firstLine = caption.split("\n")[0]?.trim() ?? "";
|
|
3780
4109
|
const hashtags = countHashtags(caption);
|
|
3781
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;
|
|
3782
4112
|
const blockedTerms = [
|
|
3783
4113
|
...(input.brandAvoidPatterns ?? []).map((t) => t.trim()).filter(Boolean),
|
|
3784
4114
|
...(input.customBannedTerms ?? []).map((t) => t.trim()).filter(Boolean)
|
|
@@ -3788,11 +4118,12 @@ function evaluateQuality(input) {
|
|
|
3788
4118
|
if (firstLine.length >= 20 && firstLine.length <= 120) hookScore += 1;
|
|
3789
4119
|
if (/[!?]/.test(firstLine) || /\b\d+(\.\d+)?\b/.test(firstLine)) hookScore += 1;
|
|
3790
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);
|
|
3791
4122
|
categories.push({
|
|
3792
4123
|
name: "Hook Strength",
|
|
3793
4124
|
score: Math.min(5, hookScore),
|
|
3794
4125
|
maxScore: 5,
|
|
3795
|
-
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."
|
|
3796
4127
|
});
|
|
3797
4128
|
let clarityScore = 2;
|
|
3798
4129
|
if (caption.length >= 80 && caption.length <= 1200) clarityScore += 2;
|
|
@@ -3818,7 +4149,8 @@ function evaluateQuality(input) {
|
|
|
3818
4149
|
detail: "Length, title, and hashtag usage should match target platforms."
|
|
3819
4150
|
});
|
|
3820
4151
|
let brandScore = 3;
|
|
3821
|
-
const
|
|
4152
|
+
const rawBrandKeyword = input.brandKeyword ?? process.env.SOCIALNEURON_BRAND_KEYWORD?.trim();
|
|
4153
|
+
const brandKeyword = rawBrandKeyword ? rawBrandKeyword.slice(0, 80).replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : null;
|
|
3822
4154
|
if (brandKeyword && new RegExp("\\b" + brandKeyword + "\\b", "i").test(title + " " + caption))
|
|
3823
4155
|
brandScore += 1;
|
|
3824
4156
|
if (!/\b(you|your|customer|audience)\b/i.test(caption)) brandScore -= 1;
|
|
@@ -3829,11 +4161,12 @@ function evaluateQuality(input) {
|
|
|
3829
4161
|
brandScore -= Math.min(2, matched.length);
|
|
3830
4162
|
}
|
|
3831
4163
|
}
|
|
4164
|
+
if (isShortFormX) brandScore = Math.max(brandScore, 3);
|
|
3832
4165
|
categories.push({
|
|
3833
4166
|
name: "Brand Alignment",
|
|
3834
4167
|
score: Math.max(0, Math.min(5, brandScore)),
|
|
3835
4168
|
maxScore: 5,
|
|
3836
|
-
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."
|
|
3837
4170
|
});
|
|
3838
4171
|
let noveltyScore = 2;
|
|
3839
4172
|
if (/\b(case study|framework|workflow|playbook|breakdown|behind the scenes)\b/i.test(caption))
|
|
@@ -3844,21 +4177,23 @@ function evaluateQuality(input) {
|
|
|
3844
4177
|
const matched = blockedTerms.filter((term) => lowerCombined.includes(term.toLowerCase()));
|
|
3845
4178
|
if (matched.length > 0) noveltyScore -= Math.min(2, matched.length);
|
|
3846
4179
|
}
|
|
4180
|
+
if (isShortFormX) noveltyScore = Math.max(noveltyScore, 3);
|
|
3847
4181
|
categories.push({
|
|
3848
4182
|
name: "Novelty",
|
|
3849
4183
|
score: Math.max(0, Math.min(5, noveltyScore)),
|
|
3850
4184
|
maxScore: 5,
|
|
3851
|
-
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."
|
|
3852
4186
|
});
|
|
3853
4187
|
let ctaScore = 2;
|
|
3854
4188
|
if (/\b(comment|reply|share|save|follow|subscribe|click|try|book|download)\b/i.test(caption))
|
|
3855
4189
|
ctaScore += 2;
|
|
3856
4190
|
if (/\?$/.test(firstLine)) ctaScore += 1;
|
|
4191
|
+
if (isShortFormX) ctaScore = Math.max(ctaScore, 3);
|
|
3857
4192
|
categories.push({
|
|
3858
4193
|
name: "CTA Strength",
|
|
3859
4194
|
score: Math.min(5, ctaScore),
|
|
3860
4195
|
maxScore: 5,
|
|
3861
|
-
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."
|
|
3862
4197
|
});
|
|
3863
4198
|
let safetyScore = 5;
|
|
3864
4199
|
if (/\b(guarantee|guaranteed|no risk|risk-free|always works|100%)\b/i.test(caption))
|
|
@@ -3896,7 +4231,7 @@ var init_quality = __esm({
|
|
|
3896
4231
|
|
|
3897
4232
|
// src/tools/distribution.ts
|
|
3898
4233
|
import { z as z3 } from "zod";
|
|
3899
|
-
import { createHash
|
|
4234
|
+
import { createHash } from "node:crypto";
|
|
3900
4235
|
function snakeToCamel(obj) {
|
|
3901
4236
|
const result = {};
|
|
3902
4237
|
for (const [key, value] of Object.entries(obj)) {
|
|
@@ -3922,6 +4257,87 @@ function asEnvelope3(data) {
|
|
|
3922
4257
|
data
|
|
3923
4258
|
};
|
|
3924
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
|
+
}
|
|
3925
4341
|
function accountEffectiveStatus(account) {
|
|
3926
4342
|
return account.effective_status || account.status;
|
|
3927
4343
|
}
|
|
@@ -3940,22 +4356,11 @@ function requestedAccountIdForPlatform(accountId, accountIds, platform3) {
|
|
|
3940
4356
|
if (!accountIds) return void 0;
|
|
3941
4357
|
return accountIds[platform3] || accountIds[platform3.toLowerCase()];
|
|
3942
4358
|
}
|
|
3943
|
-
function isAlreadyR2Signed(url) {
|
|
3944
|
-
try {
|
|
3945
|
-
const u = new URL(url);
|
|
3946
|
-
return u.searchParams.has("X-Amz-Signature");
|
|
3947
|
-
} catch {
|
|
3948
|
-
return false;
|
|
3949
|
-
}
|
|
3950
|
-
}
|
|
3951
4359
|
async function rehostExternalUrl(mediaUrl, projectId) {
|
|
3952
4360
|
const ssrf = await validateUrlForSSRF(mediaUrl);
|
|
3953
4361
|
if (!ssrf.isValid) {
|
|
3954
4362
|
return { error: ssrf.error ?? "URL rejected by SSRF check" };
|
|
3955
4363
|
}
|
|
3956
|
-
if (isAlreadyR2Signed(mediaUrl)) {
|
|
3957
|
-
return { signedUrl: mediaUrl, r2Key: "" };
|
|
3958
|
-
}
|
|
3959
4364
|
const { data, error } = await callEdgeFunction(
|
|
3960
4365
|
"upload-to-r2",
|
|
3961
4366
|
{ url: ssrf.sanitizedUrl ?? mediaUrl, projectId },
|
|
@@ -3974,19 +4379,19 @@ function registerDistributionTools(server2) {
|
|
|
3974
4379
|
media_url: z3.string().optional().describe(
|
|
3975
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."
|
|
3976
4381
|
),
|
|
3977
|
-
media_urls: z3.array(z3.string()).optional().describe(
|
|
4382
|
+
media_urls: z3.array(z3.string().url()).min(2).max(10).optional().describe(
|
|
3978
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."
|
|
3979
4384
|
),
|
|
3980
4385
|
r2_key: z3.string().optional().describe(
|
|
3981
4386
|
"R2 object key from upload_media. Signed on demand at post time \u2014 survives scheduling delays. Alternative to media_url."
|
|
3982
4387
|
),
|
|
3983
|
-
r2_keys: z3.array(z3.string()).optional().describe(
|
|
4388
|
+
r2_keys: z3.array(z3.string()).min(2).max(10).optional().describe(
|
|
3984
4389
|
"Array of R2 object keys for carousel posts. Each is signed on demand. Alternative to media_urls."
|
|
3985
4390
|
),
|
|
3986
4391
|
job_id: z3.string().optional().describe(
|
|
3987
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."
|
|
3988
4393
|
),
|
|
3989
|
-
job_ids: z3.array(z3.string()).optional().describe(
|
|
4394
|
+
job_ids: z3.array(z3.string()).min(2).max(10).optional().describe(
|
|
3990
4395
|
"Array of async job IDs for carousel posts. Each resolved to its R2 key. Alternative to media_urls/r2_keys."
|
|
3991
4396
|
),
|
|
3992
4397
|
platform_metadata: z3.object({
|
|
@@ -4016,7 +4421,10 @@ function registerDistributionTools(server2) {
|
|
|
4016
4421
|
category_id: z3.string().optional(),
|
|
4017
4422
|
tags: z3.array(z3.string()).optional(),
|
|
4018
4423
|
made_for_kids: z3.boolean().optional(),
|
|
4019
|
-
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
|
+
)
|
|
4020
4428
|
}).optional(),
|
|
4021
4429
|
facebook: z3.object({
|
|
4022
4430
|
page_id: z3.string().optional().describe("Facebook Page ID to post to."),
|
|
@@ -4099,14 +4507,8 @@ function registerDistributionTools(server2) {
|
|
|
4099
4507
|
auto_rehost: z3.boolean().optional().describe(
|
|
4100
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."
|
|
4101
4509
|
),
|
|
4102
|
-
|
|
4103
|
-
"
|
|
4104
|
-
),
|
|
4105
|
-
origin: z3.enum(["human", "hermes", "user"]).optional().describe(
|
|
4106
|
-
"Originator lineage for the post. Marks who scheduled it; use the agent value when calling from an autonomous workflow, otherwise default 'human'."
|
|
4107
|
-
),
|
|
4108
|
-
hermes_run_id: z3.string().optional().describe(
|
|
4109
|
-
"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."
|
|
4110
4512
|
)
|
|
4111
4513
|
},
|
|
4112
4514
|
async ({
|
|
@@ -4129,9 +4531,7 @@ function registerDistributionTools(server2) {
|
|
|
4129
4531
|
platform_metadata,
|
|
4130
4532
|
account_id,
|
|
4131
4533
|
account_ids,
|
|
4132
|
-
|
|
4133
|
-
origin,
|
|
4134
|
-
hermes_run_id
|
|
4534
|
+
idempotency_key
|
|
4135
4535
|
}) => {
|
|
4136
4536
|
const format = response_format ?? "text";
|
|
4137
4537
|
if ((!caption || caption.trim().length === 0) && (!title || title.trim().length === 0)) {
|
|
@@ -4160,6 +4560,8 @@ function registerDistributionTools(server2) {
|
|
|
4160
4560
|
}
|
|
4161
4561
|
let resolvedMediaUrl = media_url;
|
|
4162
4562
|
let resolvedMediaUrls = media_urls;
|
|
4563
|
+
let resolvedMediaUrlIsTrustedR2 = false;
|
|
4564
|
+
let resolvedMediaUrlsAreTrustedR2 = (media_urls ?? []).map(() => false);
|
|
4163
4565
|
const signR2Key = async (key) => {
|
|
4164
4566
|
const cleanKey = key.startsWith("r2://") ? key.slice(5) : key;
|
|
4165
4567
|
const { data: signData } = await callEdgeFunction(
|
|
@@ -4177,8 +4579,11 @@ function registerDistributionTools(server2) {
|
|
|
4177
4579
|
);
|
|
4178
4580
|
const resultUrl = jobData?.job?.result_url;
|
|
4179
4581
|
if (!resultUrl) return null;
|
|
4180
|
-
if (!resultUrl.startsWith("http"))
|
|
4181
|
-
|
|
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 };
|
|
4182
4587
|
};
|
|
4183
4588
|
try {
|
|
4184
4589
|
if (r2_key && !resolvedMediaUrl) {
|
|
@@ -4195,6 +4600,7 @@ function registerDistributionTools(server2) {
|
|
|
4195
4600
|
};
|
|
4196
4601
|
}
|
|
4197
4602
|
resolvedMediaUrl = signed;
|
|
4603
|
+
resolvedMediaUrlIsTrustedR2 = true;
|
|
4198
4604
|
} else if (job_id && !resolvedMediaUrl && !r2_key) {
|
|
4199
4605
|
const resolved = await resolveJobId(job_id);
|
|
4200
4606
|
if (!resolved) {
|
|
@@ -4208,7 +4614,8 @@ function registerDistributionTools(server2) {
|
|
|
4208
4614
|
isError: true
|
|
4209
4615
|
};
|
|
4210
4616
|
}
|
|
4211
|
-
resolvedMediaUrl = resolved;
|
|
4617
|
+
resolvedMediaUrl = resolved.url;
|
|
4618
|
+
resolvedMediaUrlIsTrustedR2 = resolved.trustedR2;
|
|
4212
4619
|
}
|
|
4213
4620
|
if (r2_keys && r2_keys.length > 0 && !resolvedMediaUrls) {
|
|
4214
4621
|
const signed = await Promise.all(r2_keys.map(signR2Key));
|
|
@@ -4225,6 +4632,7 @@ function registerDistributionTools(server2) {
|
|
|
4225
4632
|
};
|
|
4226
4633
|
}
|
|
4227
4634
|
resolvedMediaUrls = signed;
|
|
4635
|
+
resolvedMediaUrlsAreTrustedR2 = signed.map(() => true);
|
|
4228
4636
|
} else if (job_ids && job_ids.length > 0 && !resolvedMediaUrls && !r2_keys) {
|
|
4229
4637
|
const resolved = await Promise.all(job_ids.map(resolveJobId));
|
|
4230
4638
|
const failIdx = resolved.findIndex((r) => !r);
|
|
@@ -4239,10 +4647,12 @@ function registerDistributionTools(server2) {
|
|
|
4239
4647
|
isError: true
|
|
4240
4648
|
};
|
|
4241
4649
|
}
|
|
4242
|
-
|
|
4650
|
+
const resolvedJobs = resolved;
|
|
4651
|
+
resolvedMediaUrls = resolvedJobs.map((item) => item.url);
|
|
4652
|
+
resolvedMediaUrlsAreTrustedR2 = resolvedJobs.map((item) => item.trustedR2);
|
|
4243
4653
|
}
|
|
4244
4654
|
const shouldRehost = auto_rehost !== false;
|
|
4245
|
-
if (shouldRehost && resolvedMediaUrl) {
|
|
4655
|
+
if (shouldRehost && resolvedMediaUrl && !resolvedMediaUrlIsTrustedR2) {
|
|
4246
4656
|
const rehost = await rehostExternalUrl(resolvedMediaUrl, project_id);
|
|
4247
4657
|
if ("error" in rehost) {
|
|
4248
4658
|
return {
|
|
@@ -4256,10 +4666,13 @@ function registerDistributionTools(server2) {
|
|
|
4256
4666
|
};
|
|
4257
4667
|
}
|
|
4258
4668
|
resolvedMediaUrl = rehost.signedUrl;
|
|
4669
|
+
resolvedMediaUrlIsTrustedR2 = true;
|
|
4259
4670
|
}
|
|
4260
4671
|
if (shouldRehost && resolvedMediaUrls && resolvedMediaUrls.length > 0) {
|
|
4261
4672
|
const rehosted = await Promise.all(
|
|
4262
|
-
resolvedMediaUrls.map(
|
|
4673
|
+
resolvedMediaUrls.map(
|
|
4674
|
+
(u, index) => resolvedMediaUrlsAreTrustedR2[index] ? Promise.resolve({ signedUrl: u, r2Key: "" }) : rehostExternalUrl(u, project_id)
|
|
4675
|
+
)
|
|
4263
4676
|
);
|
|
4264
4677
|
const failIdx = rehosted.findIndex((r) => "error" in r);
|
|
4265
4678
|
if (failIdx !== -1) {
|
|
@@ -4275,6 +4688,7 @@ function registerDistributionTools(server2) {
|
|
|
4275
4688
|
};
|
|
4276
4689
|
}
|
|
4277
4690
|
resolvedMediaUrls = rehosted.map((r) => r.signedUrl);
|
|
4691
|
+
resolvedMediaUrlsAreTrustedR2 = resolvedMediaUrls.map(() => true);
|
|
4278
4692
|
}
|
|
4279
4693
|
} catch (resolveErr) {
|
|
4280
4694
|
return {
|
|
@@ -4287,6 +4701,42 @@ function registerDistributionTools(server2) {
|
|
|
4287
4701
|
isError: true
|
|
4288
4702
|
};
|
|
4289
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
|
+
}
|
|
4290
4740
|
const normalizedPlatforms = platforms.map(
|
|
4291
4741
|
(p) => PLATFORM_CASE_MAP[p.toLowerCase()] || p
|
|
4292
4742
|
);
|
|
@@ -4412,12 +4862,32 @@ Created with Social Neuron`;
|
|
|
4412
4862
|
};
|
|
4413
4863
|
return true;
|
|
4414
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
|
+
}
|
|
4415
4885
|
const { data, error } = await callEdgeFunction(
|
|
4416
4886
|
"schedule-post",
|
|
4417
4887
|
{
|
|
4418
4888
|
mediaUrl: resolvedMediaUrl,
|
|
4419
4889
|
mediaUrls: resolvedMediaUrls,
|
|
4420
|
-
mediaType:
|
|
4890
|
+
mediaType: resolvedMediaType ?? void 0,
|
|
4421
4891
|
caption: finalCaption,
|
|
4422
4892
|
platforms: normalizedPlatforms,
|
|
4423
4893
|
title,
|
|
@@ -4430,14 +4900,10 @@ Created with Social Neuron`;
|
|
|
4430
4900
|
normalizedPlatformMetadata
|
|
4431
4901
|
)
|
|
4432
4902
|
} : {},
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
//
|
|
4437
|
-
// against the posts.origin CHECK constraint and persists hermesRunId when
|
|
4438
|
-
// origin === 'hermes'.
|
|
4439
|
-
...origin ? { origin } : {},
|
|
4440
|
-
...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.
|
|
4441
4907
|
},
|
|
4442
4908
|
{ timeoutMs: 3e4 }
|
|
4443
4909
|
);
|
|
@@ -4503,6 +4969,99 @@ Created with Social Neuron`;
|
|
|
4503
4969
|
};
|
|
4504
4970
|
}
|
|
4505
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
|
+
);
|
|
4506
5065
|
server2.tool(
|
|
4507
5066
|
"list_connected_accounts",
|
|
4508
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.",
|
|
@@ -4531,14 +5090,16 @@ Created with Social Neuron`;
|
|
|
4531
5090
|
isError: true
|
|
4532
5091
|
};
|
|
4533
5092
|
}
|
|
4534
|
-
const accounts = result.accounts ?? [];
|
|
5093
|
+
const accounts = (result.accounts ?? []).map(publicConnectedAccount).filter((account) => account !== null);
|
|
4535
5094
|
if (accounts.length === 0) {
|
|
4536
5095
|
if (format === "json") {
|
|
5096
|
+
const structuredContent = asEnvelope3({ accounts: [] });
|
|
4537
5097
|
return {
|
|
5098
|
+
structuredContent,
|
|
4538
5099
|
content: [
|
|
4539
5100
|
{
|
|
4540
5101
|
type: "text",
|
|
4541
|
-
text: JSON.stringify(
|
|
5102
|
+
text: JSON.stringify(structuredContent, null, 2)
|
|
4542
5103
|
}
|
|
4543
5104
|
]
|
|
4544
5105
|
};
|
|
@@ -4566,11 +5127,13 @@ Created with Social Neuron`;
|
|
|
4566
5127
|
);
|
|
4567
5128
|
}
|
|
4568
5129
|
if (format === "json") {
|
|
5130
|
+
const structuredContent = asEnvelope3({ accounts });
|
|
4569
5131
|
return {
|
|
5132
|
+
structuredContent,
|
|
4570
5133
|
content: [
|
|
4571
5134
|
{
|
|
4572
5135
|
type: "text",
|
|
4573
|
-
text: JSON.stringify(
|
|
5136
|
+
text: JSON.stringify(structuredContent, null, 2)
|
|
4574
5137
|
}
|
|
4575
5138
|
]
|
|
4576
5139
|
};
|
|
@@ -4584,6 +5147,9 @@ Created with Social Neuron`;
|
|
|
4584
5147
|
"list_recent_posts",
|
|
4585
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.",
|
|
4586
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
|
+
),
|
|
4587
5153
|
platform: z3.enum([
|
|
4588
5154
|
"youtube",
|
|
4589
5155
|
"tiktok",
|
|
@@ -4599,13 +5165,27 @@ Created with Social Neuron`;
|
|
|
4599
5165
|
limit: z3.number().min(1).max(50).optional().describe("Maximum number of posts to return. Defaults to 20."),
|
|
4600
5166
|
response_format: z3.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
4601
5167
|
},
|
|
4602
|
-
async ({ platform: platform3, status, days, limit, response_format }) => {
|
|
5168
|
+
async ({ project_id, platform: platform3, status, days, limit, response_format }) => {
|
|
4603
5169
|
const format = response_format ?? "text";
|
|
4604
5170
|
const lookbackDays = days ?? 7;
|
|
4605
|
-
const
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
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
|
+
}
|
|
5183
|
+
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
5184
|
+
action: "recent-posts",
|
|
5185
|
+
days: lookbackDays,
|
|
5186
|
+
limit: limit ?? 20,
|
|
5187
|
+
projectId: resolvedProjectId,
|
|
5188
|
+
project_id: resolvedProjectId,
|
|
4609
5189
|
...platform3 ? { platform: platform3 } : {},
|
|
4610
5190
|
...status ? { status } : {}
|
|
4611
5191
|
});
|
|
@@ -4620,7 +5200,7 @@ Created with Social Neuron`;
|
|
|
4620
5200
|
isError: true
|
|
4621
5201
|
};
|
|
4622
5202
|
}
|
|
4623
|
-
const rows = result.posts ?? [];
|
|
5203
|
+
const rows = (result.posts ?? []).map(publicPostRecord).filter((post) => post !== null);
|
|
4624
5204
|
if (rows.length === 0) {
|
|
4625
5205
|
if (format === "json") {
|
|
4626
5206
|
const structuredContent2 = asEnvelope3({ posts: [] });
|
|
@@ -4695,8 +5275,11 @@ Created with Social Neuron`;
|
|
|
4695
5275
|
};
|
|
4696
5276
|
server2.tool(
|
|
4697
5277
|
"find_next_slots",
|
|
4698
|
-
"Find optimal posting time slots based on
|
|
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.",
|
|
4699
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
|
+
),
|
|
4700
5283
|
platforms: z3.array(
|
|
4701
5284
|
z3.enum([
|
|
4702
5285
|
"youtube",
|
|
@@ -4715,6 +5298,7 @@ Created with Social Neuron`;
|
|
|
4715
5298
|
response_format: z3.enum(["text", "json"]).default("text")
|
|
4716
5299
|
},
|
|
4717
5300
|
async ({
|
|
5301
|
+
project_id,
|
|
4718
5302
|
platforms,
|
|
4719
5303
|
count,
|
|
4720
5304
|
start_after,
|
|
@@ -4722,13 +5306,35 @@ Created with Social Neuron`;
|
|
|
4722
5306
|
response_format
|
|
4723
5307
|
}) => {
|
|
4724
5308
|
try {
|
|
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
|
+
}
|
|
4725
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
|
+
}
|
|
4726
5330
|
const endDate = new Date(startDate.getTime() + 7 * 24 * 60 * 60 * 1e3);
|
|
4727
5331
|
const { data: postsResult, error: postsError } = await callEdgeFunction("mcp-data", {
|
|
4728
5332
|
action: "scheduled-posts",
|
|
4729
5333
|
start_date: startDate.toISOString(),
|
|
4730
5334
|
end_date: endDate.toISOString(),
|
|
4731
|
-
statuses: ["scheduled", "draft"]
|
|
5335
|
+
statuses: ["pending", "scheduled", "draft"],
|
|
5336
|
+
projectId: resolvedProjectId,
|
|
5337
|
+
project_id: resolvedProjectId
|
|
4732
5338
|
});
|
|
4733
5339
|
const existingPosts = postsError ? [] : postsResult?.posts ?? [];
|
|
4734
5340
|
const gapMs = min_gap_hours * 60 * 60 * 1e3;
|
|
@@ -4768,19 +5374,17 @@ Created with Social Neuron`;
|
|
|
4768
5374
|
const slots = candidates.filter((s) => !s.conflict).sort((a, b) => b.engagement_score - a.engagement_score).slice(0, count);
|
|
4769
5375
|
const conflictsAvoided = candidates.filter((s) => s.conflict).length;
|
|
4770
5376
|
if (response_format === "json") {
|
|
5377
|
+
const structuredContent = asEnvelope3({
|
|
5378
|
+
slots,
|
|
5379
|
+
total_candidates: candidates.length,
|
|
5380
|
+
conflicts_avoided: conflictsAvoided
|
|
5381
|
+
});
|
|
4771
5382
|
return {
|
|
5383
|
+
structuredContent,
|
|
4772
5384
|
content: [
|
|
4773
5385
|
{
|
|
4774
5386
|
type: "text",
|
|
4775
|
-
text: JSON.stringify(
|
|
4776
|
-
asEnvelope3({
|
|
4777
|
-
slots,
|
|
4778
|
-
total_candidates: candidates.length,
|
|
4779
|
-
conflicts_avoided: conflictsAvoided
|
|
4780
|
-
}),
|
|
4781
|
-
null,
|
|
4782
|
-
2
|
|
4783
|
-
)
|
|
5387
|
+
text: JSON.stringify(structuredContent, null, 2)
|
|
4784
5388
|
}
|
|
4785
5389
|
],
|
|
4786
5390
|
isError: false
|
|
@@ -5145,7 +5749,7 @@ Created with Social Neuron`;
|
|
|
5145
5749
|
const results = [];
|
|
5146
5750
|
const buildIdempotencyKey = (post) => {
|
|
5147
5751
|
const planId = effectivePlanId ?? (typeof workingPlan.plan_id === "string" ? String(workingPlan.plan_id) : "inline");
|
|
5148
|
-
const captionHash =
|
|
5752
|
+
const captionHash = createHash("sha256").update(post.caption).digest("hex").slice(0, 16);
|
|
5149
5753
|
const raw = [
|
|
5150
5754
|
"schedule_content_plan",
|
|
5151
5755
|
planId,
|
|
@@ -5155,7 +5759,7 @@ Created with Social Neuron`;
|
|
|
5155
5759
|
captionHash,
|
|
5156
5760
|
idempotency_seed ?? ""
|
|
5157
5761
|
].join(":");
|
|
5158
|
-
return `plan-${
|
|
5762
|
+
return `plan-${createHash("sha256").update(raw).digest("hex").slice(0, 48)}`;
|
|
5159
5763
|
};
|
|
5160
5764
|
const scheduleOne = async (post) => {
|
|
5161
5765
|
if (!post.schedule_at) {
|
|
@@ -5341,7 +5945,7 @@ Created with Social Neuron`;
|
|
|
5341
5945
|
}
|
|
5342
5946
|
);
|
|
5343
5947
|
}
|
|
5344
|
-
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;
|
|
5345
5949
|
var init_distribution = __esm({
|
|
5346
5950
|
"src/tools/distribution.ts"() {
|
|
5347
5951
|
"use strict";
|
|
@@ -5362,8 +5966,29 @@ var init_distribution = __esm({
|
|
|
5362
5966
|
threads: "Threads",
|
|
5363
5967
|
bluesky: "Bluesky"
|
|
5364
5968
|
};
|
|
5365
|
-
TIKTOK_AUDIT_APPROVED = false
|
|
5969
|
+
TIKTOK_AUDIT_APPROVED = !["false", "0", "no"].includes(
|
|
5970
|
+
(process.env.TIKTOK_AUDIT_APPROVED ?? "").toLowerCase()
|
|
5971
|
+
);
|
|
5366
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
|
+
]);
|
|
5367
5992
|
}
|
|
5368
5993
|
});
|
|
5369
5994
|
|
|
@@ -5644,7 +6269,10 @@ function registerMediaTools(server2) {
|
|
|
5644
6269
|
content: [
|
|
5645
6270
|
{
|
|
5646
6271
|
type: "text",
|
|
5647
|
-
|
|
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.`
|
|
5648
6276
|
}
|
|
5649
6277
|
],
|
|
5650
6278
|
isError: true
|
|
@@ -5856,7 +6484,7 @@ function asEnvelope4(data) {
|
|
|
5856
6484
|
function registerAnalyticsTools(server2) {
|
|
5857
6485
|
server2.tool(
|
|
5858
6486
|
"fetch_analytics",
|
|
5859
|
-
"Get post performance metrics \u2014 views, likes, comments, shares, and engagement rate. Filter by platform, time range (default 30 days), or specific content_id. Call refresh_platform_analytics first if data seems stale. Results sorted by most recent capture.",
|
|
6487
|
+
"Get project-scoped post performance metrics \u2014 views, likes, comments, shares, and engagement rate. Filter by platform, time range (default 30 days), or specific content_id. Call refresh_platform_analytics first if data seems stale. Results sorted by most recent capture.",
|
|
5860
6488
|
{
|
|
5861
6489
|
platform: z5.enum([
|
|
5862
6490
|
"youtube",
|
|
@@ -5874,19 +6502,26 @@ function registerAnalyticsTools(server2) {
|
|
|
5874
6502
|
content_id: z5.string().uuid().optional().describe(
|
|
5875
6503
|
"Filter to a specific content_history ID to see performance of one piece of content."
|
|
5876
6504
|
),
|
|
6505
|
+
project_id: z5.string().optional().describe("Project ID. Defaults to the active project context."),
|
|
5877
6506
|
limit: z5.number().min(1).max(100).optional().describe("Maximum number of posts to return. Defaults to 20."),
|
|
5878
6507
|
response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
5879
6508
|
},
|
|
5880
|
-
async ({ platform: platform3, days, content_id, limit, response_format }) => {
|
|
6509
|
+
async ({ platform: platform3, days, content_id, project_id, limit, response_format }) => {
|
|
5881
6510
|
const format = response_format ?? "text";
|
|
5882
6511
|
const lookbackDays = days ?? 30;
|
|
5883
6512
|
const maxPosts = limit ?? 20;
|
|
6513
|
+
const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
|
|
5884
6514
|
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
5885
6515
|
action: "analytics",
|
|
5886
6516
|
platform: platform3,
|
|
5887
6517
|
days: lookbackDays,
|
|
5888
|
-
|
|
5889
|
-
|
|
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,
|
|
6523
|
+
contentId: content_id,
|
|
6524
|
+
...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
|
|
5890
6525
|
});
|
|
5891
6526
|
if (efError) {
|
|
5892
6527
|
return {
|
|
@@ -5894,7 +6529,14 @@ function registerAnalyticsTools(server2) {
|
|
|
5894
6529
|
isError: true
|
|
5895
6530
|
};
|
|
5896
6531
|
}
|
|
5897
|
-
const
|
|
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);
|
|
5898
6540
|
if (rows.length === 0) {
|
|
5899
6541
|
if (format === "json") {
|
|
5900
6542
|
const structuredContent = asEnvelope4({
|
|
@@ -5956,14 +6598,19 @@ function registerAnalyticsTools(server2) {
|
|
|
5956
6598
|
);
|
|
5957
6599
|
server2.tool(
|
|
5958
6600
|
"refresh_platform_analytics",
|
|
5959
|
-
"Queue analytics refresh jobs for
|
|
6601
|
+
"Queue analytics refresh jobs for posts from the last 7 days in one project across its connected platforms. Call this before fetch_analytics if you need fresh data. Returns immediately \u2014 data updates asynchronously over the next 1-5 minutes.",
|
|
5960
6602
|
{
|
|
6603
|
+
project_id: z5.string().optional().describe("Project ID. Defaults to the active project context."),
|
|
5961
6604
|
response_format: z5.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
5962
6605
|
},
|
|
5963
|
-
async ({ response_format }) => {
|
|
6606
|
+
async ({ project_id, response_format }) => {
|
|
5964
6607
|
const format = response_format ?? "text";
|
|
5965
6608
|
const userId = await getDefaultUserId();
|
|
5966
|
-
const
|
|
6609
|
+
const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
|
|
6610
|
+
const rateLimit = checkRateLimit(
|
|
6611
|
+
"posting",
|
|
6612
|
+
`refresh_platform_analytics:${userId}:${resolvedProjectId ?? "default"}`
|
|
6613
|
+
);
|
|
5967
6614
|
if (!rateLimit.allowed) {
|
|
5968
6615
|
return {
|
|
5969
6616
|
content: [
|
|
@@ -5975,7 +6622,10 @@ function registerAnalyticsTools(server2) {
|
|
|
5975
6622
|
isError: true
|
|
5976
6623
|
};
|
|
5977
6624
|
}
|
|
5978
|
-
const { data, error } = await callEdgeFunction("fetch-analytics", {
|
|
6625
|
+
const { data, error } = await callEdgeFunction("fetch-analytics", {
|
|
6626
|
+
userId,
|
|
6627
|
+
...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
|
|
6628
|
+
});
|
|
5979
6629
|
if (error) {
|
|
5980
6630
|
return {
|
|
5981
6631
|
content: [{ type: "text", text: `Error refreshing analytics: ${error}` }],
|
|
@@ -6004,7 +6654,8 @@ function registerAnalyticsTools(server2) {
|
|
|
6004
6654
|
success: true,
|
|
6005
6655
|
postsProcessed: result.postsProcessed,
|
|
6006
6656
|
queued,
|
|
6007
|
-
errored
|
|
6657
|
+
errored,
|
|
6658
|
+
projectId: resolvedProjectId ?? null
|
|
6008
6659
|
});
|
|
6009
6660
|
return {
|
|
6010
6661
|
structuredContent,
|
|
@@ -6064,6 +6715,84 @@ var init_analytics = __esm({
|
|
|
6064
6715
|
}
|
|
6065
6716
|
});
|
|
6066
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
|
+
|
|
6067
6796
|
// src/tools/brand.ts
|
|
6068
6797
|
import { z as z6 } from "zod";
|
|
6069
6798
|
function asEnvelope5(data) {
|
|
@@ -6080,12 +6809,31 @@ function registerBrandTools(server2) {
|
|
|
6080
6809
|
"extract_brand",
|
|
6081
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.",
|
|
6082
6811
|
{
|
|
6083
|
-
url: z6.string().
|
|
6084
|
-
'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").'
|
|
6085
6814
|
),
|
|
6086
6815
|
response_format: z6.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
6087
6816
|
},
|
|
6088
|
-
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;
|
|
6089
6837
|
const ssrfCheck = await validateUrlForSSRF(url);
|
|
6090
6838
|
if (!ssrfCheck.isValid) {
|
|
6091
6839
|
return {
|
|
@@ -6393,6 +7141,7 @@ var init_brand = __esm({
|
|
|
6393
7141
|
init_supabase();
|
|
6394
7142
|
init_ssrf();
|
|
6395
7143
|
init_version();
|
|
7144
|
+
init_brandUrlInput();
|
|
6396
7145
|
}
|
|
6397
7146
|
});
|
|
6398
7147
|
|
|
@@ -6768,7 +7517,7 @@ function registerRemotionTools(server2) {
|
|
|
6768
7517
|
},
|
|
6769
7518
|
async ({ composition_id, output_format, props }) => {
|
|
6770
7519
|
const userId = await getDefaultUserId();
|
|
6771
|
-
const rateLimit = checkRateLimit("
|
|
7520
|
+
const rateLimit = checkRateLimit("generation", `render_demo_video:${userId}`);
|
|
6772
7521
|
if (!rateLimit.allowed) {
|
|
6773
7522
|
return {
|
|
6774
7523
|
content: [
|
|
@@ -7107,22 +7856,25 @@ function asEnvelope6(data) {
|
|
|
7107
7856
|
function registerInsightsTools(server2) {
|
|
7108
7857
|
server2.tool(
|
|
7109
7858
|
"get_performance_insights",
|
|
7110
|
-
"Query performance insights derived from post analytics. Returns metrics like engagement rate, view velocity, and click rate aggregated over time. Use this to understand what content is performing well.",
|
|
7859
|
+
"Query project-scoped performance insights derived from post analytics. Returns metrics like engagement rate, view velocity, and click rate aggregated over time. Use this to understand what content is performing well.",
|
|
7111
7860
|
{
|
|
7112
7861
|
insight_type: z9.enum(["top_hooks", "optimal_timing", "best_models", "competitor_patterns"]).optional().describe("Filter to a specific insight type."),
|
|
7113
7862
|
days: z9.number().min(1).max(90).optional().describe("Number of days to look back. Defaults to 30. Max 90."),
|
|
7114
7863
|
limit: z9.number().min(1).max(50).optional().describe("Maximum number of insights to return. Defaults to 10."),
|
|
7864
|
+
project_id: z9.string().optional().describe("Project ID. Defaults to the active project context."),
|
|
7115
7865
|
response_format: z9.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
7116
7866
|
},
|
|
7117
|
-
async ({ insight_type, days, limit, response_format }) => {
|
|
7867
|
+
async ({ insight_type, days, limit, project_id, response_format }) => {
|
|
7118
7868
|
const format = response_format ?? "text";
|
|
7119
7869
|
const lookbackDays = days ?? 30;
|
|
7120
7870
|
const maxRows = limit ?? 10;
|
|
7121
7871
|
const effectiveDays = Math.min(lookbackDays, MAX_INSIGHT_AGE_DAYS);
|
|
7872
|
+
const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
|
|
7122
7873
|
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
7123
7874
|
action: "performance-insights",
|
|
7124
7875
|
days: effectiveDays,
|
|
7125
|
-
limit: maxRows
|
|
7876
|
+
limit: maxRows,
|
|
7877
|
+
...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
|
|
7126
7878
|
});
|
|
7127
7879
|
if (efError || !result?.success) {
|
|
7128
7880
|
return {
|
|
@@ -7209,19 +7961,22 @@ function registerInsightsTools(server2) {
|
|
|
7209
7961
|
);
|
|
7210
7962
|
server2.tool(
|
|
7211
7963
|
"get_best_posting_times",
|
|
7212
|
-
"Analyze post analytics data to find the best times to post for maximum engagement. Returns the top 5 time slots (day of week + hour) ranked by average engagement.",
|
|
7964
|
+
"Analyze project-scoped post analytics data to find the best times to post for maximum engagement. Returns the top 5 time slots (day of week + hour) ranked by average engagement.",
|
|
7213
7965
|
{
|
|
7214
7966
|
platform: z9.enum(PLATFORM_ENUM).optional().describe("Filter to a specific platform."),
|
|
7215
7967
|
days: z9.number().min(1).max(90).optional().describe("Number of days to analyze. Defaults to 30. Max 90."),
|
|
7968
|
+
project_id: z9.string().optional().describe("Project ID. Defaults to the active project context."),
|
|
7216
7969
|
response_format: z9.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
7217
7970
|
},
|
|
7218
|
-
async ({ platform: platform3, days, response_format }) => {
|
|
7971
|
+
async ({ platform: platform3, days, project_id, response_format }) => {
|
|
7219
7972
|
const format = response_format ?? "text";
|
|
7220
7973
|
const lookbackDays = days ?? 30;
|
|
7974
|
+
const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
|
|
7221
7975
|
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
7222
7976
|
action: "best-posting-times",
|
|
7223
7977
|
days: lookbackDays,
|
|
7224
|
-
platform: platform3 ?? void 0
|
|
7978
|
+
platform: platform3 ?? void 0,
|
|
7979
|
+
...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
|
|
7225
7980
|
});
|
|
7226
7981
|
if (efError || !result?.success) {
|
|
7227
7982
|
return {
|
|
@@ -7335,6 +8090,7 @@ var init_insights = __esm({
|
|
|
7335
8090
|
"src/tools/insights.ts"() {
|
|
7336
8091
|
"use strict";
|
|
7337
8092
|
init_edge_function();
|
|
8093
|
+
init_supabase();
|
|
7338
8094
|
init_version();
|
|
7339
8095
|
MAX_INSIGHT_AGE_DAYS = 30;
|
|
7340
8096
|
PLATFORM_ENUM = [
|
|
@@ -8378,7 +9134,7 @@ Schedule: ${JSON.stringify(updated.schedule_config)}`
|
|
|
8378
9134
|
}
|
|
8379
9135
|
const statusData = {
|
|
8380
9136
|
activeConfigs: result?.activeConfigs ?? 0,
|
|
8381
|
-
recentRuns: [],
|
|
9137
|
+
recentRuns: result?.recentRuns ?? result?.recent_runs ?? [],
|
|
8382
9138
|
pendingApprovals: result?.pendingApprovals ?? 0
|
|
8383
9139
|
};
|
|
8384
9140
|
if (format === "json") {
|
|
@@ -8400,8 +9156,20 @@ ${"=".repeat(40)}
|
|
|
8400
9156
|
text += `Pending Approvals: ${statusData.pendingApprovals}
|
|
8401
9157
|
|
|
8402
9158
|
`;
|
|
8403
|
-
|
|
9159
|
+
if (statusData.recentRuns.length === 0) {
|
|
9160
|
+
text += `No recent runs.
|
|
9161
|
+
`;
|
|
9162
|
+
} else {
|
|
9163
|
+
text += `Recent Runs (${statusData.recentRuns.length}):
|
|
8404
9164
|
`;
|
|
9165
|
+
for (const run of statusData.recentRuns.slice(0, 10)) {
|
|
9166
|
+
const id = String(run.id ?? run.run_id ?? "unknown");
|
|
9167
|
+
const status = String(run.status ?? "unknown");
|
|
9168
|
+
const credits = run.credits_used ?? run.creditsUsed;
|
|
9169
|
+
text += ` ${id}: ${status}${credits == null ? "" : ` (${String(credits)} credits)`}
|
|
9170
|
+
`;
|
|
9171
|
+
}
|
|
9172
|
+
}
|
|
8405
9173
|
return {
|
|
8406
9174
|
content: [{ type: "text", text }]
|
|
8407
9175
|
};
|
|
@@ -10583,7 +11351,9 @@ function toolKnowledgeDocument(tool) {
|
|
|
10583
11351
|
function getKnowledgeDocuments() {
|
|
10584
11352
|
return [
|
|
10585
11353
|
...STATIC_KNOWLEDGE_DOCUMENTS,
|
|
10586
|
-
...TOOL_CATALOG.filter((t) => !t.internal).map(
|
|
11354
|
+
...TOOL_CATALOG.filter((t) => !t.internal && !t.hiddenFromPublicCount).map(
|
|
11355
|
+
toolKnowledgeDocument
|
|
11356
|
+
)
|
|
10587
11357
|
];
|
|
10588
11358
|
}
|
|
10589
11359
|
function tokenize(input) {
|
|
@@ -10700,7 +11470,7 @@ function registerDiscoveryTools(server2) {
|
|
|
10700
11470
|
if (query) {
|
|
10701
11471
|
results = searchTools(query);
|
|
10702
11472
|
}
|
|
10703
|
-
results = results.filter((t) => !t.internal);
|
|
11473
|
+
results = results.filter((t) => !t.internal && !t.hiddenFromPublicCount);
|
|
10704
11474
|
if (module) {
|
|
10705
11475
|
const moduleTools = getToolsByModule(module);
|
|
10706
11476
|
results = results.filter((t) => moduleTools.some((mt) => mt.name === t.name));
|
|
@@ -12631,11 +13401,11 @@ function hexToLab(hex) {
|
|
|
12631
13401
|
const lb = srgbToLinear(b);
|
|
12632
13402
|
const x = lr * 0.4124564 + lg * 0.3575761 + lb * 0.1804375;
|
|
12633
13403
|
const y = lr * 0.2126729 + lg * 0.7151522 + lb * 0.072175;
|
|
12634
|
-
const
|
|
13404
|
+
const z41 = lr * 0.0193339 + lg * 0.119192 + lb * 0.9503041;
|
|
12635
13405
|
const f = (t) => t > 8856e-6 ? Math.cbrt(t) : 7.787 * t + 16 / 116;
|
|
12636
13406
|
const fx = f(x / 0.95047);
|
|
12637
13407
|
const fy = f(y / 1);
|
|
12638
|
-
const fz = f(
|
|
13408
|
+
const fz = f(z41 / 1.08883);
|
|
12639
13409
|
return { L: 116 * fy - 16, a: 500 * (fx - fy), b: 200 * (fy - fz) };
|
|
12640
13410
|
}
|
|
12641
13411
|
function deltaE2000(lab1, lab2) {
|
|
@@ -12875,10 +13645,10 @@ function registerBrandRuntimeTools(server2) {
|
|
|
12875
13645
|
pagesScraped: meta.pagesScraped || 0
|
|
12876
13646
|
}
|
|
12877
13647
|
};
|
|
12878
|
-
const
|
|
13648
|
+
const envelope2 = asEnvelope23(runtime);
|
|
12879
13649
|
return {
|
|
12880
|
-
structuredContent:
|
|
12881
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
13650
|
+
structuredContent: envelope2,
|
|
13651
|
+
content: [{ type: "text", text: JSON.stringify(envelope2, null, 2) }]
|
|
12882
13652
|
};
|
|
12883
13653
|
}
|
|
12884
13654
|
);
|
|
@@ -13018,9 +13788,9 @@ function registerBrandRuntimeTools(server2) {
|
|
|
13018
13788
|
}
|
|
13019
13789
|
const profile = row.profile_data;
|
|
13020
13790
|
const checkResult = computeBrandConsistency(content, profile);
|
|
13021
|
-
const
|
|
13791
|
+
const envelope2 = asEnvelope23(checkResult);
|
|
13022
13792
|
return {
|
|
13023
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
13793
|
+
content: [{ type: "text", text: JSON.stringify(envelope2, null, 2) }]
|
|
13024
13794
|
};
|
|
13025
13795
|
}
|
|
13026
13796
|
);
|
|
@@ -13052,9 +13822,9 @@ function registerBrandRuntimeTools(server2) {
|
|
|
13052
13822
|
content_colors,
|
|
13053
13823
|
threshold ?? 10
|
|
13054
13824
|
);
|
|
13055
|
-
const
|
|
13825
|
+
const envelope2 = asEnvelope23(auditResult);
|
|
13056
13826
|
return {
|
|
13057
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
13827
|
+
content: [{ type: "text", text: JSON.stringify(envelope2, null, 2) }]
|
|
13058
13828
|
};
|
|
13059
13829
|
}
|
|
13060
13830
|
);
|
|
@@ -13087,9 +13857,9 @@ function registerBrandRuntimeTools(server2) {
|
|
|
13087
13857
|
row.profile_data.typography,
|
|
13088
13858
|
format
|
|
13089
13859
|
);
|
|
13090
|
-
const
|
|
13860
|
+
const envelope2 = asEnvelope23({ format, tokens: output });
|
|
13091
13861
|
return {
|
|
13092
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
13862
|
+
content: [{ type: "text", text: JSON.stringify(envelope2, null, 2) }]
|
|
13093
13863
|
};
|
|
13094
13864
|
}
|
|
13095
13865
|
);
|
|
@@ -13243,7 +14013,7 @@ function registerCarouselTools(server2) {
|
|
|
13243
14013
|
};
|
|
13244
14014
|
}
|
|
13245
14015
|
const userId = await getDefaultUserId();
|
|
13246
|
-
const rateLimit = checkRateLimit("
|
|
14016
|
+
const rateLimit = checkRateLimit("generation", `create_carousel:${userId}`);
|
|
13247
14017
|
if (!rateLimit.allowed) {
|
|
13248
14018
|
return {
|
|
13249
14019
|
content: [
|
|
@@ -13315,7 +14085,12 @@ function registerCarouselTools(server2) {
|
|
|
13315
14085
|
slideNumber: slide.slideNumber,
|
|
13316
14086
|
jobId: null,
|
|
13317
14087
|
model: image_model,
|
|
13318
|
-
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
|
|
13319
14094
|
};
|
|
13320
14095
|
}
|
|
13321
14096
|
const jobId = data.asyncJobId ?? data.taskId ?? null;
|
|
@@ -13327,14 +14102,24 @@ function registerCarouselTools(server2) {
|
|
|
13327
14102
|
slideNumber: slide.slideNumber,
|
|
13328
14103
|
jobId,
|
|
13329
14104
|
model: image_model,
|
|
13330
|
-
error: null
|
|
14105
|
+
error: null,
|
|
14106
|
+
creditsReserved: 0,
|
|
14107
|
+
creditsCharged: data.creditsDeducted ?? perImageCost,
|
|
14108
|
+
creditsRefunded: 0,
|
|
14109
|
+
billingStatus: "charged",
|
|
14110
|
+
failureReason: null
|
|
13331
14111
|
};
|
|
13332
14112
|
} catch (err) {
|
|
13333
14113
|
return {
|
|
13334
14114
|
slideNumber: slide.slideNumber,
|
|
13335
14115
|
jobId: null,
|
|
13336
14116
|
model: image_model,
|
|
13337
|
-
error: sanitizeError(err)
|
|
14117
|
+
error: sanitizeError(err),
|
|
14118
|
+
creditsReserved: null,
|
|
14119
|
+
creditsCharged: null,
|
|
14120
|
+
creditsRefunded: null,
|
|
14121
|
+
billingStatus: "unknown",
|
|
14122
|
+
failureReason: null
|
|
13338
14123
|
};
|
|
13339
14124
|
}
|
|
13340
14125
|
})
|
|
@@ -13359,7 +14144,8 @@ function registerCarouselTools(server2) {
|
|
|
13359
14144
|
return {
|
|
13360
14145
|
...s,
|
|
13361
14146
|
imageJobId: job?.jobId ?? null,
|
|
13362
|
-
imageError: job?.error ?? null
|
|
14147
|
+
imageError: job?.error ?? null,
|
|
14148
|
+
imageBillingStatus: job?.billingStatus ?? "unknown"
|
|
13363
14149
|
};
|
|
13364
14150
|
}),
|
|
13365
14151
|
imageModel: image_model,
|
|
@@ -13371,11 +14157,19 @@ function registerCarouselTools(server2) {
|
|
|
13371
14157
|
jobIds: successfulJobs.map((j) => j.jobId),
|
|
13372
14158
|
failedSlides: failedJobs.map((j) => ({
|
|
13373
14159
|
slideNumber: j.slideNumber,
|
|
13374
|
-
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
|
|
13375
14166
|
})),
|
|
13376
14167
|
credits: {
|
|
13377
14168
|
textGeneration: textCredits,
|
|
13378
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,
|
|
13379
14173
|
totalEstimated: textCredits + successfulJobs.length * perImageCost
|
|
13380
14174
|
}
|
|
13381
14175
|
}
|
|
@@ -13571,7 +14365,7 @@ function registerHyperframesTools(server2) {
|
|
|
13571
14365
|
);
|
|
13572
14366
|
server2.tool(
|
|
13573
14367
|
"render_hyperframes",
|
|
13574
|
-
"Render an HTML video composition (Hyperframes) to MP4.
|
|
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.",
|
|
13575
14369
|
{
|
|
13576
14370
|
composition_html: z30.string().max(5e5).optional().describe(
|
|
13577
14371
|
"Inline HTML composition (full <html>...</html>). Max 500KB. Use composition_url for larger."
|
|
@@ -13583,7 +14377,9 @@ function registerHyperframesTools(server2) {
|
|
|
13583
14377
|
aspect_ratio: z30.enum(["9:16", "16:9", "1:1"]).optional().describe('Output aspect ratio. Default "9:16".'),
|
|
13584
14378
|
duration_sec: z30.number().min(0.1).max(600).describe("Output duration in seconds. Required. Max 600 (10 min)."),
|
|
13585
14379
|
fps: z30.union([z30.literal(24), z30.literal(30), z30.literal(60)]).optional().describe("Frames per second. Default 30."),
|
|
13586
|
-
quality: z30.enum(["draft", "standard", "high"]).optional().describe("draft: fast iteration; standard: production; high: final master.")
|
|
14380
|
+
quality: z30.enum(["draft", "standard", "high"]).optional().describe("draft: fast iteration; standard: production; high: final master."),
|
|
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.")
|
|
13587
14383
|
},
|
|
13588
14384
|
async ({
|
|
13589
14385
|
composition_html,
|
|
@@ -13592,7 +14388,9 @@ function registerHyperframesTools(server2) {
|
|
|
13592
14388
|
aspect_ratio,
|
|
13593
14389
|
duration_sec,
|
|
13594
14390
|
fps,
|
|
13595
|
-
quality
|
|
14391
|
+
quality,
|
|
14392
|
+
project_id,
|
|
14393
|
+
response_format
|
|
13596
14394
|
}) => {
|
|
13597
14395
|
const userId = await getDefaultUserId();
|
|
13598
14396
|
const rateLimit = checkRateLimit("generation", `render_hyperframes:${userId}`);
|
|
@@ -13637,23 +14435,36 @@ function registerHyperframesTools(server2) {
|
|
|
13637
14435
|
aspectRatio: aspect_ratio || "9:16",
|
|
13638
14436
|
durationSec: duration_sec,
|
|
13639
14437
|
fps: fps || 30,
|
|
13640
|
-
quality: quality || "standard"
|
|
14438
|
+
quality: quality || "standard",
|
|
14439
|
+
...project_id && { projectId: project_id }
|
|
13641
14440
|
});
|
|
13642
14441
|
if (error || !data?.jobId) {
|
|
13643
14442
|
throw new Error(error || data?.error || "Failed to create Hyperframes render job");
|
|
13644
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
|
+
};
|
|
13645
14456
|
return {
|
|
13646
14457
|
content: [
|
|
13647
14458
|
{
|
|
13648
14459
|
type: "text",
|
|
13649
|
-
text: [
|
|
14460
|
+
text: response_format === "json" ? JSON.stringify({ data: payload }) : [
|
|
13650
14461
|
`Hyperframes render job queued.`,
|
|
13651
14462
|
` Job ID: ${data.jobId}`,
|
|
13652
14463
|
` Credits: ${data.creditsCost}`,
|
|
13653
14464
|
` Duration: ${duration_sec}s @ ${fps || 30}fps (${aspect_ratio || "9:16"})`,
|
|
13654
14465
|
` Quality: ${quality || "standard"}`,
|
|
13655
14466
|
``,
|
|
13656
|
-
`Poll with check_status
|
|
14467
|
+
`Poll with check_status.`
|
|
13657
14468
|
].join("\n")
|
|
13658
14469
|
}
|
|
13659
14470
|
]
|
|
@@ -13663,7 +14474,7 @@ function registerHyperframesTools(server2) {
|
|
|
13663
14474
|
content: [
|
|
13664
14475
|
{
|
|
13665
14476
|
type: "text",
|
|
13666
|
-
text: `Failed to queue Hyperframes render: ${err
|
|
14477
|
+
text: `Failed to queue Hyperframes render: ${sanitizeError(err)}`
|
|
13667
14478
|
}
|
|
13668
14479
|
],
|
|
13669
14480
|
isError: true
|
|
@@ -13679,6 +14490,7 @@ var init_hyperframes = __esm({
|
|
|
13679
14490
|
init_rate_limit();
|
|
13680
14491
|
init_supabase();
|
|
13681
14492
|
init_edge_function();
|
|
14493
|
+
init_sanitize_error();
|
|
13682
14494
|
HYPERFRAMES_BLOCKS = [
|
|
13683
14495
|
// Transitions
|
|
13684
14496
|
{
|
|
@@ -13796,49 +14608,122 @@ import {
|
|
|
13796
14608
|
import { z as z31 } from "zod";
|
|
13797
14609
|
import fs from "node:fs/promises";
|
|
13798
14610
|
import path from "node:path";
|
|
14611
|
+
import { fileURLToPath } from "node:url";
|
|
13799
14612
|
function startOfCurrentWeekMonday() {
|
|
13800
14613
|
const now = /* @__PURE__ */ new Date();
|
|
13801
|
-
const day = now.
|
|
14614
|
+
const day = now.getUTCDay();
|
|
13802
14615
|
const monday = new Date(now);
|
|
13803
|
-
monday.setUTCDate(now.getUTCDate() - day +
|
|
14616
|
+
monday.setUTCDate(now.getUTCDate() - (day + 6) % 7);
|
|
14617
|
+
monday.setUTCHours(0, 0, 0, 0);
|
|
13804
14618
|
return monday.toISOString().split("T")[0];
|
|
13805
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
|
+
}
|
|
13806
14670
|
function registerContentCalendarApp(server2) {
|
|
13807
14671
|
registerAppTool(
|
|
13808
14672
|
server2,
|
|
13809
14673
|
"open_content_calendar",
|
|
13810
14674
|
{
|
|
13811
14675
|
title: "Content Calendar",
|
|
13812
|
-
description: "Open
|
|
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.",
|
|
13813
14677
|
inputSchema: {
|
|
13814
|
-
|
|
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(
|
|
13815
14682
|
"ISO date for the week start (YYYY-MM-DD); defaults to the current week's Monday."
|
|
13816
14683
|
)
|
|
13817
14684
|
},
|
|
13818
14685
|
outputSchema: {
|
|
13819
14686
|
start_date: z31.string(),
|
|
14687
|
+
project_id: z31.string(),
|
|
13820
14688
|
posts: z31.array(RecentPostOutputSchema),
|
|
13821
14689
|
scopes: z31.array(z31.string())
|
|
13822
14690
|
},
|
|
13823
14691
|
_meta: {
|
|
13824
14692
|
ui: {
|
|
13825
|
-
resourceUri: CALENDAR_URI
|
|
13826
|
-
csp: {
|
|
13827
|
-
"img-src": ["'self'", "https://*.r2.cloudflarestorage.com", "data:"],
|
|
13828
|
-
"connect-src": ["'self'"]
|
|
13829
|
-
}
|
|
14693
|
+
resourceUri: CALENDAR_URI
|
|
13830
14694
|
}
|
|
13831
14695
|
}
|
|
13832
14696
|
},
|
|
13833
|
-
async ({ start_date }
|
|
13834
|
-
const userScopes =
|
|
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
|
+
}
|
|
13835
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
|
+
}
|
|
13836
14718
|
const { data: result, error } = await callEdgeFunction(
|
|
13837
14719
|
"mcp-data",
|
|
13838
14720
|
{
|
|
13839
|
-
action: "
|
|
13840
|
-
|
|
13841
|
-
|
|
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
|
|
13842
14727
|
},
|
|
13843
14728
|
{ timeoutMs: 15e3 }
|
|
13844
14729
|
);
|
|
@@ -13847,19 +14732,16 @@ function registerContentCalendarApp(server2) {
|
|
|
13847
14732
|
content: [
|
|
13848
14733
|
{
|
|
13849
14734
|
type: "text",
|
|
13850
|
-
text:
|
|
14735
|
+
text: "The content calendar could not load posts. Please retry."
|
|
13851
14736
|
}
|
|
13852
14737
|
],
|
|
13853
14738
|
isError: true
|
|
13854
14739
|
};
|
|
13855
14740
|
}
|
|
13856
|
-
const posts = (result.posts ?? []).filter((
|
|
13857
|
-
const ts = p.scheduled_at ?? p.published_at ?? p.created_at;
|
|
13858
|
-
if (!ts) return false;
|
|
13859
|
-
return ts.split("T")[0] >= fromDate;
|
|
13860
|
-
});
|
|
14741
|
+
const posts = (result.posts ?? []).map(publicRecentPost).filter((post) => post !== null);
|
|
13861
14742
|
const structuredContent = {
|
|
13862
14743
|
start_date: fromDate,
|
|
14744
|
+
project_id: resolvedProjectId,
|
|
13863
14745
|
posts,
|
|
13864
14746
|
scopes: userScopes
|
|
13865
14747
|
};
|
|
@@ -13878,37 +14760,60 @@ function registerContentCalendarApp(server2) {
|
|
|
13878
14760
|
server2,
|
|
13879
14761
|
CALENDAR_URI,
|
|
13880
14762
|
CALENDAR_URI,
|
|
13881
|
-
{
|
|
14763
|
+
{
|
|
14764
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
14765
|
+
description: "Self-contained Social Neuron project content calendar.",
|
|
14766
|
+
_meta: { ui: { csp: CALENDAR_CSP } }
|
|
14767
|
+
},
|
|
13882
14768
|
async () => {
|
|
13883
|
-
const htmlPath = path.join(process.cwd(), "apps/content-calendar/dist/mcp-app.html");
|
|
13884
14769
|
try {
|
|
13885
|
-
const html = await
|
|
14770
|
+
const html = await readCalendarHtml();
|
|
13886
14771
|
return {
|
|
13887
|
-
contents: [
|
|
14772
|
+
contents: [
|
|
14773
|
+
{
|
|
14774
|
+
uri: CALENDAR_URI,
|
|
14775
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
14776
|
+
text: html,
|
|
14777
|
+
_meta: { ui: { csp: CALENDAR_CSP } }
|
|
14778
|
+
}
|
|
14779
|
+
]
|
|
13888
14780
|
};
|
|
13889
|
-
} catch
|
|
14781
|
+
} catch {
|
|
13890
14782
|
const errorHtml = `<!DOCTYPE html>
|
|
13891
14783
|
<html><head><title>Content Calendar \u2014 unavailable</title></head>
|
|
13892
14784
|
<body style="font-family:sans-serif;padding:24px;color:#444;">
|
|
13893
14785
|
<h2>Content Calendar app bundle missing</h2>
|
|
13894
|
-
<p>The
|
|
13895
|
-
<code>apps/content-calendar/dist/mcp-app.html</code> is not built.
|
|
13896
|
-
Run <code>npm run build:app</code> in the mcp-server directory and redeploy.</p>
|
|
13897
|
-
<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>
|
|
13898
14787
|
</body></html>`;
|
|
13899
14788
|
return {
|
|
13900
|
-
contents: [
|
|
14789
|
+
contents: [
|
|
14790
|
+
{
|
|
14791
|
+
uri: CALENDAR_URI,
|
|
14792
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
14793
|
+
text: errorHtml,
|
|
14794
|
+
_meta: { ui: { csp: CALENDAR_CSP } }
|
|
14795
|
+
}
|
|
14796
|
+
]
|
|
13901
14797
|
};
|
|
13902
14798
|
}
|
|
13903
14799
|
}
|
|
13904
14800
|
);
|
|
13905
14801
|
}
|
|
13906
|
-
var CALENDAR_URI, RecentPostOutputSchema;
|
|
14802
|
+
var CALENDAR_URI, CALENDAR_CSP, RecentPostOutputSchema;
|
|
13907
14803
|
var init_content_calendar = __esm({
|
|
13908
14804
|
"src/apps/content-calendar.ts"() {
|
|
13909
14805
|
"use strict";
|
|
13910
14806
|
init_edge_function();
|
|
13911
|
-
|
|
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
|
+
};
|
|
13912
14817
|
RecentPostOutputSchema = z31.object({
|
|
13913
14818
|
id: z31.string(),
|
|
13914
14819
|
platform: z31.string(),
|
|
@@ -13922,8 +14827,257 @@ var init_content_calendar = __esm({
|
|
|
13922
14827
|
}
|
|
13923
14828
|
});
|
|
13924
14829
|
|
|
13925
|
-
// src/
|
|
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";
|
|
13926
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";
|
|
13927
15081
|
function findActiveAccount(accounts, platform3) {
|
|
13928
15082
|
const target = platform3.toLowerCase();
|
|
13929
15083
|
return accounts.find((a) => {
|
|
@@ -13936,16 +15090,16 @@ function registerConnectionTools(server2) {
|
|
|
13936
15090
|
"start_platform_connection",
|
|
13937
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.",
|
|
13938
15092
|
{
|
|
13939
|
-
platform:
|
|
13940
|
-
project_id:
|
|
15093
|
+
platform: z33.enum(PLATFORM_ENUM4).describe("Platform to connect. Lower-case: instagram, tiktok, youtube, etc."),
|
|
15094
|
+
project_id: z33.string().optional().describe(
|
|
13941
15095
|
"Brand/project ID to bind the new social account to. Use this when connecting a brand-specific account."
|
|
13942
15096
|
),
|
|
13943
|
-
response_format:
|
|
15097
|
+
response_format: z33.enum(["text", "json"]).optional().describe("Response format. Default: text.")
|
|
13944
15098
|
},
|
|
13945
15099
|
async ({ platform: platform3, project_id, response_format }) => {
|
|
13946
15100
|
const format = response_format ?? "text";
|
|
13947
15101
|
const userId = await getDefaultUserId();
|
|
13948
|
-
const rl = checkRateLimit("
|
|
15102
|
+
const rl = checkRateLimit("posting", `start_platform_connection:${userId}`);
|
|
13949
15103
|
if (!rl.allowed) {
|
|
13950
15104
|
return {
|
|
13951
15105
|
content: [
|
|
@@ -14025,13 +15179,13 @@ function registerConnectionTools(server2) {
|
|
|
14025
15179
|
"wait_for_connection",
|
|
14026
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.",
|
|
14027
15181
|
{
|
|
14028
|
-
platform:
|
|
14029
|
-
project_id:
|
|
15182
|
+
platform: z33.enum(PLATFORM_ENUM4).describe("Platform to wait for."),
|
|
15183
|
+
project_id: z33.string().optional().describe(
|
|
14030
15184
|
"Brand/project ID to scope the connection poll. Use the same project_id passed to start_platform_connection."
|
|
14031
15185
|
),
|
|
14032
|
-
timeout_s:
|
|
14033
|
-
poll_interval_s:
|
|
14034
|
-
response_format:
|
|
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.")
|
|
14035
15189
|
},
|
|
14036
15190
|
async ({ platform: platform3, project_id, timeout_s, poll_interval_s, response_format }) => {
|
|
14037
15191
|
const format = response_format ?? "text";
|
|
@@ -14187,7 +15341,7 @@ var init_connections = __esm({
|
|
|
14187
15341
|
});
|
|
14188
15342
|
|
|
14189
15343
|
// src/tools/harness.ts
|
|
14190
|
-
import { z as
|
|
15344
|
+
import { z as z34 } from "zod";
|
|
14191
15345
|
function registerHarnessTools(server2, _ctx) {
|
|
14192
15346
|
server2.tool(
|
|
14193
15347
|
"write_agent_reflection",
|
|
@@ -14292,38 +15446,38 @@ var init_harness = __esm({
|
|
|
14292
15446
|
"engager"
|
|
14293
15447
|
];
|
|
14294
15448
|
writeReflectionSchema = {
|
|
14295
|
-
reflection_text:
|
|
14296
|
-
generated_by_agent:
|
|
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(
|
|
14297
15451
|
"Which agent produced this reflection. Must be one of: conductor, brand-brain, drafter, publisher, analyst, engager."
|
|
14298
15452
|
),
|
|
14299
|
-
provenance:
|
|
14300
|
-
content_history_id:
|
|
14301
|
-
outcome_event_id:
|
|
14302
|
-
prm_score_ids:
|
|
14303
|
-
handoff_ids:
|
|
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.")
|
|
14304
15458
|
}).strict().describe(
|
|
14305
15459
|
"Source evidence for this reflection. Only these four keys are accepted \u2014 unknown keys are rejected to prevent spurious provenance claims."
|
|
14306
15460
|
),
|
|
14307
|
-
brand_id:
|
|
14308
|
-
pipeline_id:
|
|
14309
|
-
post_id:
|
|
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.")
|
|
14310
15464
|
};
|
|
14311
15465
|
readReflectionSchema = {
|
|
14312
|
-
brand_id:
|
|
14313
|
-
generated_by_agent:
|
|
15466
|
+
brand_id: z34.string().describe("Brand profile UUID to read reflections for."),
|
|
15467
|
+
generated_by_agent: z34.enum(ALLOWED_AGENTS).optional().describe(
|
|
14314
15468
|
"Optional filter: only return reflections produced by this agent. One of: conductor, brand-brain, drafter, publisher, analyst, engager."
|
|
14315
15469
|
),
|
|
14316
|
-
limit:
|
|
15470
|
+
limit: z34.number().int().min(1).max(100).optional().describe(
|
|
14317
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)."
|
|
14318
15472
|
)
|
|
14319
15473
|
};
|
|
14320
15474
|
recordOutcomeSchema = {
|
|
14321
|
-
decision_event_id:
|
|
14322
|
-
horizon:
|
|
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(
|
|
14323
15477
|
"Observation horizon. Only horizon=24h with a non-null reward triggers a learning-loop update; 1h/6h are stored but inert for learning."
|
|
14324
15478
|
),
|
|
14325
|
-
reward:
|
|
14326
|
-
outcome_metrics:
|
|
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(
|
|
14327
15481
|
'Optional map of raw metric name \u2192 value (e.g. {"likes": 42, "reach": 1200}). Stored verbatim; not used for learning directly.'
|
|
14328
15482
|
)
|
|
14329
15483
|
};
|
|
@@ -14331,7 +15485,7 @@ var init_harness = __esm({
|
|
|
14331
15485
|
});
|
|
14332
15486
|
|
|
14333
15487
|
// src/tools/hermes.ts
|
|
14334
|
-
import { z as
|
|
15488
|
+
import { z as z35 } from "zod";
|
|
14335
15489
|
function asEnvelope25(data) {
|
|
14336
15490
|
return {
|
|
14337
15491
|
_meta: {
|
|
@@ -14347,12 +15501,12 @@ function registerHermesTools(server2) {
|
|
|
14347
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.",
|
|
14348
15502
|
{
|
|
14349
15503
|
platform: PLATFORM.describe("Target platform for the draft."),
|
|
14350
|
-
copy:
|
|
14351
|
-
project_id:
|
|
14352
|
-
media_url:
|
|
14353
|
-
hermes_run_id:
|
|
14354
|
-
source_intel_ids:
|
|
14355
|
-
response_format:
|
|
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()
|
|
14356
15510
|
},
|
|
14357
15511
|
async ({ platform: platform3, copy, project_id, media_url, hermes_run_id, response_format }) => {
|
|
14358
15512
|
const { data, error } = await callEdgeFunction(
|
|
@@ -14390,15 +15544,15 @@ function registerHermesTools(server2) {
|
|
|
14390
15544
|
"record_voice_lesson",
|
|
14391
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)".',
|
|
14392
15546
|
{
|
|
14393
|
-
project_id:
|
|
14394
|
-
lesson:
|
|
14395
|
-
evidence:
|
|
14396
|
-
engagement_lift_pct:
|
|
14397
|
-
sample_size:
|
|
14398
|
-
top_examples:
|
|
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.")
|
|
14399
15553
|
}).describe("Quantitative evidence backing the lesson."),
|
|
14400
|
-
applies_to:
|
|
14401
|
-
response_format:
|
|
15554
|
+
applies_to: z35.array(PLATFORM).min(1).describe("Platforms this lesson applies to."),
|
|
15555
|
+
response_format: z35.enum(["text", "json"]).optional()
|
|
14402
15556
|
},
|
|
14403
15557
|
async ({ project_id, lesson, evidence, applies_to, response_format }) => {
|
|
14404
15558
|
const { data, error } = await callEdgeFunction("mcp-data", {
|
|
@@ -14434,10 +15588,10 @@ function registerHermesTools(server2) {
|
|
|
14434
15588
|
"record_observation",
|
|
14435
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.',
|
|
14436
15590
|
{
|
|
14437
|
-
summary:
|
|
14438
|
-
deltas:
|
|
14439
|
-
run_id:
|
|
14440
|
-
response_format:
|
|
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()
|
|
14441
15595
|
},
|
|
14442
15596
|
async ({ summary, deltas, run_id, response_format }) => {
|
|
14443
15597
|
const { data, error } = await callEdgeFunction("mcp-data", { action: "record-observation", summary, deltas, run_id });
|
|
@@ -14464,13 +15618,13 @@ function registerHermesTools(server2) {
|
|
|
14464
15618
|
"record_intel_signal",
|
|
14465
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.",
|
|
14466
15620
|
{
|
|
14467
|
-
source:
|
|
14468
|
-
url:
|
|
14469
|
-
topic:
|
|
14470
|
-
title:
|
|
14471
|
-
summary:
|
|
14472
|
-
score:
|
|
14473
|
-
response_format:
|
|
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()
|
|
14474
15628
|
},
|
|
14475
15629
|
async ({ source, url, topic, title, summary, score, response_format }) => {
|
|
14476
15630
|
const { data, error } = await callEdgeFunction("mcp-data", {
|
|
@@ -14508,16 +15662,16 @@ function registerHermesTools(server2) {
|
|
|
14508
15662
|
"record_campaign_spend",
|
|
14509
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.",
|
|
14510
15664
|
{
|
|
14511
|
-
campaign_id:
|
|
14512
|
-
category:
|
|
15665
|
+
campaign_id: z35.string().min(1).max(200).describe("Campaign identifier (slug)."),
|
|
15666
|
+
category: z35.enum([
|
|
14513
15667
|
"hermes_drafts",
|
|
14514
15668
|
"carousel_renders",
|
|
14515
15669
|
"analytics_pulls",
|
|
14516
15670
|
"paid_amplification",
|
|
14517
15671
|
"other"
|
|
14518
15672
|
]).describe("Cost category."),
|
|
14519
|
-
amount_usd:
|
|
14520
|
-
response_format:
|
|
15673
|
+
amount_usd: z35.number().min(0).describe("Amount in USD; supports 4 decimal places."),
|
|
15674
|
+
response_format: z35.enum(["text", "json"]).optional()
|
|
14521
15675
|
},
|
|
14522
15676
|
async ({ campaign_id, category, amount_usd, response_format }) => {
|
|
14523
15677
|
const { data, error } = await callEdgeFunction("mcp-data", {
|
|
@@ -14552,7 +15706,7 @@ function registerHermesTools(server2) {
|
|
|
14552
15706
|
"get_active_campaigns",
|
|
14553
15707
|
"List currently-running campaigns with thesis, budget, hero format, and current spend. Use to bias drafts toward active campaign themes.",
|
|
14554
15708
|
{
|
|
14555
|
-
response_format:
|
|
15709
|
+
response_format: z35.enum(["text", "json"]).optional()
|
|
14556
15710
|
},
|
|
14557
15711
|
async ({ response_format }) => {
|
|
14558
15712
|
const { data, error } = await callEdgeFunction("mcp-data", { action: "get-active-campaigns" });
|
|
@@ -14601,7 +15755,7 @@ var init_hermes = __esm({
|
|
|
14601
15755
|
"use strict";
|
|
14602
15756
|
init_edge_function();
|
|
14603
15757
|
init_version();
|
|
14604
|
-
PLATFORM =
|
|
15758
|
+
PLATFORM = z35.enum([
|
|
14605
15759
|
"instagram",
|
|
14606
15760
|
"twitter",
|
|
14607
15761
|
"linkedin",
|
|
@@ -14647,7 +15801,20 @@ var init_skills_manifest = __esm({
|
|
|
14647
15801
|
});
|
|
14648
15802
|
|
|
14649
15803
|
// src/tools/skills.ts
|
|
14650
|
-
import { z as
|
|
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
|
+
}
|
|
14651
15818
|
function asEnvelope26(data) {
|
|
14652
15819
|
return {
|
|
14653
15820
|
_meta: {
|
|
@@ -14675,19 +15842,88 @@ function registerSkillsTools(server2) {
|
|
|
14675
15842
|
"list_skills",
|
|
14676
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.',
|
|
14677
15844
|
{
|
|
14678
|
-
studio:
|
|
14679
|
-
|
|
14680
|
-
|
|
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
|
+
)
|
|
14681
15854
|
},
|
|
14682
15855
|
async ({ studio, featured_only, response_format }) => {
|
|
14683
|
-
const skills = listSkills({ studio, featuredOnly: featured_only });
|
|
14684
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 });
|
|
14685
15917
|
if (format === "json") {
|
|
14686
15918
|
return {
|
|
14687
15919
|
content: [
|
|
14688
15920
|
{
|
|
14689
15921
|
type: "text",
|
|
14690
|
-
text: JSON.stringify(
|
|
15922
|
+
text: JSON.stringify(
|
|
15923
|
+
asEnvelope26({ count: skills.length, skills }),
|
|
15924
|
+
null,
|
|
15925
|
+
2
|
|
15926
|
+
)
|
|
14691
15927
|
}
|
|
14692
15928
|
]
|
|
14693
15929
|
};
|
|
@@ -14697,7 +15933,9 @@ function registerSkillsTools(server2) {
|
|
|
14697
15933
|
content: [
|
|
14698
15934
|
{
|
|
14699
15935
|
type: "text",
|
|
14700
|
-
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
|
+
)
|
|
14701
15939
|
}
|
|
14702
15940
|
]
|
|
14703
15941
|
};
|
|
@@ -14716,18 +15954,22 @@ ${blocks}` }]
|
|
|
14716
15954
|
"run_skill",
|
|
14717
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.",
|
|
14718
15956
|
{
|
|
14719
|
-
skill_id:
|
|
15957
|
+
skill_id: z36.string().describe(
|
|
14720
15958
|
'The id of the skill to run (e.g. "skill-brand-locked-viral-hook-reel"). Use list_skills to discover available ids.'
|
|
14721
15959
|
),
|
|
14722
|
-
topic:
|
|
14723
|
-
|
|
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(
|
|
14724
15964
|
'Who the content is for (e.g. "first-time founders"). Defaults to brand persona.'
|
|
14725
15965
|
),
|
|
14726
|
-
hook:
|
|
15966
|
+
hook: z36.string().optional().describe(
|
|
14727
15967
|
"Optional explicit hook. If omitted, the skill drafts and scores 3-5 candidates."
|
|
14728
15968
|
),
|
|
14729
|
-
cta:
|
|
14730
|
-
|
|
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.")
|
|
14731
15973
|
},
|
|
14732
15974
|
async ({ skill_id, topic, audience, hook, cta, response_format }) => {
|
|
14733
15975
|
const skill = getSkill(skill_id);
|
|
@@ -14795,19 +16037,96 @@ ${blocks}` }]
|
|
|
14795
16037
|
};
|
|
14796
16038
|
}
|
|
14797
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
|
+
);
|
|
14798
16108
|
}
|
|
14799
16109
|
var STUDIO_VALUES;
|
|
14800
16110
|
var init_skills = __esm({
|
|
14801
16111
|
"src/tools/skills.ts"() {
|
|
14802
16112
|
"use strict";
|
|
14803
16113
|
init_version();
|
|
16114
|
+
init_tool_error();
|
|
16115
|
+
init_edge_function();
|
|
14804
16116
|
init_skills_manifest();
|
|
14805
|
-
STUDIO_VALUES = [
|
|
16117
|
+
STUDIO_VALUES = [
|
|
16118
|
+
"video",
|
|
16119
|
+
"avatar",
|
|
16120
|
+
"carousel",
|
|
16121
|
+
"voice",
|
|
16122
|
+
"caption",
|
|
16123
|
+
"edit"
|
|
16124
|
+
];
|
|
14806
16125
|
}
|
|
14807
16126
|
});
|
|
14808
16127
|
|
|
14809
16128
|
// src/tools/loopPulse.ts
|
|
14810
|
-
import { z as
|
|
16129
|
+
import { z as z37 } from "zod";
|
|
14811
16130
|
function asEnvelope27(data) {
|
|
14812
16131
|
return {
|
|
14813
16132
|
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
@@ -14819,7 +16138,7 @@ function registerLoopPulseTools(server2) {
|
|
|
14819
16138
|
"get_loop_pulse",
|
|
14820
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.',
|
|
14821
16140
|
{
|
|
14822
|
-
response_format:
|
|
16141
|
+
response_format: z37.enum(["text", "json"]).optional().describe("Output format. Defaults to text.")
|
|
14823
16142
|
},
|
|
14824
16143
|
async ({ response_format }) => {
|
|
14825
16144
|
const format = response_format ?? "text";
|
|
@@ -14869,7 +16188,7 @@ var init_loopPulse = __esm({
|
|
|
14869
16188
|
});
|
|
14870
16189
|
|
|
14871
16190
|
// src/tools/banditState.ts
|
|
14872
|
-
import { z as
|
|
16191
|
+
import { z as z38 } from "zod";
|
|
14873
16192
|
function asEnvelope28(data) {
|
|
14874
16193
|
return {
|
|
14875
16194
|
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
@@ -14881,8 +16200,8 @@ function registerBanditStateTools(server2) {
|
|
|
14881
16200
|
"get_bandit_state",
|
|
14882
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).",
|
|
14883
16202
|
{
|
|
14884
|
-
project_id:
|
|
14885
|
-
platform:
|
|
16203
|
+
project_id: z38.string().uuid().optional().describe("Project UUID. Defaults to the authenticated user default project."),
|
|
16204
|
+
platform: z38.enum([
|
|
14886
16205
|
"instagram",
|
|
14887
16206
|
"tiktok",
|
|
14888
16207
|
"youtube",
|
|
@@ -14894,7 +16213,7 @@ function registerBanditStateTools(server2) {
|
|
|
14894
16213
|
]).optional().describe(
|
|
14895
16214
|
"Lowercase platform name. Omit to return both platform-scoped and legacy platform-agnostic arms."
|
|
14896
16215
|
),
|
|
14897
|
-
arm_type:
|
|
16216
|
+
arm_type: z38.enum([
|
|
14898
16217
|
"hook_family",
|
|
14899
16218
|
"hook_type",
|
|
14900
16219
|
"format",
|
|
@@ -14908,8 +16227,8 @@ function registerBanditStateTools(server2) {
|
|
|
14908
16227
|
"posting_time_bucket",
|
|
14909
16228
|
"content_format"
|
|
14910
16229
|
]).optional().describe("Arm dimension. Omit to return all types grouped."),
|
|
14911
|
-
top_k:
|
|
14912
|
-
response_format:
|
|
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.")
|
|
14913
16232
|
},
|
|
14914
16233
|
async ({ project_id, platform: platform3, arm_type, top_k, response_format }) => {
|
|
14915
16234
|
const format = response_format ?? "text";
|
|
@@ -14972,12 +16291,134 @@ var init_banditState = __esm({
|
|
|
14972
16291
|
}
|
|
14973
16292
|
});
|
|
14974
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
|
+
|
|
14975
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
|
+
}
|
|
14976
16416
|
function wrapToolWithScanner(toolName, handler) {
|
|
14977
16417
|
return async function scannerWrappedHandler(...handlerArgs) {
|
|
14978
16418
|
const args = handlerArgs[0];
|
|
14979
16419
|
const ctx = handlerArgs[1];
|
|
14980
|
-
const
|
|
16420
|
+
const scanArgs = redactBase64Payloads(args);
|
|
16421
|
+
const inputText = scanArgs === void 0 ? "{}" : typeof scanArgs === "string" ? scanArgs : JSON.stringify(scanArgs);
|
|
14981
16422
|
const inputScan = scan(inputText, {
|
|
14982
16423
|
mode: "block",
|
|
14983
16424
|
source: "mcp_tool_input",
|
|
@@ -15005,6 +16446,16 @@ function wrapToolWithScanner(toolName, handler) {
|
|
|
15005
16446
|
source: "mcp_tool_output",
|
|
15006
16447
|
user_id: ctx?.userId
|
|
15007
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
|
+
}
|
|
15008
16459
|
if (outputScan.sanitized_text !== void 0) {
|
|
15009
16460
|
try {
|
|
15010
16461
|
ctx?.logScan?.(toolName, "output", outputScan);
|
|
@@ -15013,7 +16464,10 @@ function wrapToolWithScanner(toolName, handler) {
|
|
|
15013
16464
|
try {
|
|
15014
16465
|
return JSON.parse(outputScan.sanitized_text);
|
|
15015
16466
|
} catch {
|
|
15016
|
-
return
|
|
16467
|
+
return toolError(
|
|
16468
|
+
"server_error",
|
|
16469
|
+
"The response could not be returned safely. Please retry or contact support."
|
|
16470
|
+
);
|
|
15017
16471
|
}
|
|
15018
16472
|
}
|
|
15019
16473
|
return result;
|
|
@@ -15063,8 +16517,7 @@ function applyScopeEnforcement(server2, scopeResolver) {
|
|
|
15063
16517
|
details: {
|
|
15064
16518
|
source: "wrapper",
|
|
15065
16519
|
// A thrown exception escaped the handler — an unclassified fault.
|
|
15066
|
-
error_type: "server_error"
|
|
15067
|
-
exception: err instanceof Error ? err.message.slice(0, 200) : "unknown"
|
|
16520
|
+
error_type: "server_error"
|
|
15068
16521
|
}
|
|
15069
16522
|
});
|
|
15070
16523
|
throw err;
|
|
@@ -15196,12 +16649,14 @@ function registerAllTools(server2, options) {
|
|
|
15196
16649
|
registerSkillsTools(server2);
|
|
15197
16650
|
registerLoopPulseTools(server2);
|
|
15198
16651
|
registerBanditStateTools(server2);
|
|
16652
|
+
registerLifecycleTools(server2);
|
|
15199
16653
|
if (!options?.skipApps) {
|
|
15200
16654
|
registerContentCalendarApp(server2);
|
|
16655
|
+
registerAnalyticsPulseApp(server2);
|
|
15201
16656
|
}
|
|
15202
16657
|
applyAnnotations(server2);
|
|
15203
16658
|
}
|
|
15204
|
-
var RESPONSE_CHAR_LIMIT;
|
|
16659
|
+
var BASE64_PAYLOAD_KEYS, DATA_URI_PREFIX2, STRICT_BASE64, RESPONSE_CHAR_LIMIT;
|
|
15205
16660
|
var init_register_tools = __esm({
|
|
15206
16661
|
"src/lib/register-tools.ts"() {
|
|
15207
16662
|
"use strict";
|
|
@@ -15243,18 +16698,23 @@ var init_register_tools = __esm({
|
|
|
15243
16698
|
init_niche_research();
|
|
15244
16699
|
init_hyperframes();
|
|
15245
16700
|
init_content_calendar();
|
|
16701
|
+
init_analytics_pulse();
|
|
15246
16702
|
init_connections();
|
|
15247
16703
|
init_harness();
|
|
15248
16704
|
init_hermes();
|
|
15249
16705
|
init_skills();
|
|
15250
16706
|
init_loopPulse();
|
|
15251
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}$/;
|
|
15252
16712
|
RESPONSE_CHAR_LIMIT = 1e5;
|
|
15253
16713
|
}
|
|
15254
16714
|
});
|
|
15255
16715
|
|
|
15256
16716
|
// src/cli/sn/parse.ts
|
|
15257
|
-
import { createHash as
|
|
16717
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
15258
16718
|
function parseSnArgs(argv) {
|
|
15259
16719
|
const parsed = { _: [] };
|
|
15260
16720
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -15284,8 +16744,8 @@ function isEnabledFlag(value) {
|
|
|
15284
16744
|
}
|
|
15285
16745
|
function emitSnResult(payload, asJson) {
|
|
15286
16746
|
if (asJson) {
|
|
15287
|
-
const
|
|
15288
|
-
process.stdout.write(JSON.stringify(
|
|
16747
|
+
const envelope2 = { schema_version: "1", ...payload };
|
|
16748
|
+
process.stdout.write(JSON.stringify(envelope2, null, 2) + "\n");
|
|
15289
16749
|
}
|
|
15290
16750
|
}
|
|
15291
16751
|
function isValidHttpsUrl(value) {
|
|
@@ -15320,7 +16780,7 @@ async function checkUrlReachability(url) {
|
|
|
15320
16780
|
} catch (error) {
|
|
15321
16781
|
return {
|
|
15322
16782
|
ok: false,
|
|
15323
|
-
error:
|
|
16783
|
+
error: "Network request failed."
|
|
15324
16784
|
};
|
|
15325
16785
|
} finally {
|
|
15326
16786
|
clearTimeout(timer);
|
|
@@ -15350,7 +16810,7 @@ function buildPublishIdempotencyKey(input) {
|
|
|
15350
16810
|
title: input.title ?? "",
|
|
15351
16811
|
scheduledAt: input.scheduledAt ?? ""
|
|
15352
16812
|
});
|
|
15353
|
-
return `sn_${
|
|
16813
|
+
return `sn_${createHash2("sha256").update(material).digest("hex").slice(0, 24)}`;
|
|
15354
16814
|
}
|
|
15355
16815
|
function normalizePlatforms(platformsRaw) {
|
|
15356
16816
|
const caseMap = {
|
|
@@ -16410,17 +17870,21 @@ __export(discovery_exports, {
|
|
|
16410
17870
|
handleInfo: () => handleInfo,
|
|
16411
17871
|
handleTools: () => handleTools
|
|
16412
17872
|
});
|
|
17873
|
+
function publicStdioTools() {
|
|
17874
|
+
return TOOL_CATALOG.filter(
|
|
17875
|
+
(t) => !t.internal && !t.hiddenFromPublicCount && t.module !== "apps"
|
|
17876
|
+
);
|
|
17877
|
+
}
|
|
16413
17878
|
async function handleTools(args, asJson) {
|
|
16414
|
-
let tools =
|
|
17879
|
+
let tools = publicStdioTools();
|
|
16415
17880
|
const scope = args.scope;
|
|
16416
17881
|
if (typeof scope === "string") {
|
|
16417
|
-
tools =
|
|
17882
|
+
tools = tools.filter((tool) => tool.scope === scope);
|
|
16418
17883
|
}
|
|
16419
17884
|
const module = args.module;
|
|
16420
17885
|
if (typeof module === "string") {
|
|
16421
|
-
tools =
|
|
17886
|
+
tools = tools.filter((tool) => tool.module === module);
|
|
16422
17887
|
}
|
|
16423
|
-
tools = tools.filter((t) => !t.internal);
|
|
16424
17888
|
if (asJson) {
|
|
16425
17889
|
emitSnResult({ ok: true, command: "tools", toolCount: tools.length, tools }, true);
|
|
16426
17890
|
return;
|
|
@@ -16449,27 +17913,31 @@ Module: ${moduleName} (${moduleTools.length} tools)`);
|
|
|
16449
17913
|
process.exit(0);
|
|
16450
17914
|
}
|
|
16451
17915
|
async function handleInfo(args, asJson) {
|
|
17916
|
+
const stdioTools = publicStdioTools();
|
|
16452
17917
|
const info = {
|
|
16453
17918
|
version: MCP_VERSION,
|
|
16454
|
-
toolCount:
|
|
16455
|
-
modules:
|
|
17919
|
+
toolCount: stdioTools.length,
|
|
17920
|
+
modules: Array.from(new Set(stdioTools.map((tool) => tool.module))).sort(),
|
|
17921
|
+
auth: null
|
|
16456
17922
|
};
|
|
16457
|
-
|
|
16458
|
-
|
|
16459
|
-
|
|
16460
|
-
|
|
16461
|
-
|
|
16462
|
-
|
|
16463
|
-
|
|
16464
|
-
|
|
16465
|
-
|
|
16466
|
-
|
|
16467
|
-
|
|
16468
|
-
|
|
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
|
+
}
|
|
16469
17937
|
}
|
|
17938
|
+
} catch {
|
|
17939
|
+
info.auth = null;
|
|
16470
17940
|
}
|
|
16471
|
-
} catch {
|
|
16472
|
-
info.auth = null;
|
|
16473
17941
|
}
|
|
16474
17942
|
if (info.auth) {
|
|
16475
17943
|
try {
|
|
@@ -16532,7 +18000,11 @@ function getCallHandler() {
|
|
|
16532
18000
|
return cachedCallHandler;
|
|
16533
18001
|
}
|
|
16534
18002
|
function restToolNames() {
|
|
16535
|
-
return new Set(
|
|
18003
|
+
return new Set(
|
|
18004
|
+
TOOL_CATALOG.filter((t) => !t.localOnly && !t.internal && !t.hiddenFromPublicCount).map(
|
|
18005
|
+
(t) => t.name
|
|
18006
|
+
)
|
|
18007
|
+
);
|
|
16536
18008
|
}
|
|
16537
18009
|
async function invokeToolRest(name, args) {
|
|
16538
18010
|
const handler = getCallHandler();
|
|
@@ -17098,7 +18570,7 @@ function printSnUsage() {
|
|
|
17098
18570
|
console.error("");
|
|
17099
18571
|
console.error("Discovery:");
|
|
17100
18572
|
console.error(" tools [--scope <scope>] [--module <module>] [--json]");
|
|
17101
|
-
console.error(" info [--json]");
|
|
18573
|
+
console.error(" info [--offline] [--json]");
|
|
17102
18574
|
console.error("");
|
|
17103
18575
|
console.error("Content:");
|
|
17104
18576
|
console.error(
|
|
@@ -17231,10 +18703,11 @@ var setup_exports = {};
|
|
|
17231
18703
|
__export(setup_exports, {
|
|
17232
18704
|
generatePKCE: () => generatePKCE,
|
|
17233
18705
|
getAppBaseUrl: () => getAppBaseUrl,
|
|
18706
|
+
isValidSetupApiKey: () => isValidSetupApiKey,
|
|
17234
18707
|
runLogout: () => runLogout,
|
|
17235
18708
|
runSetup: () => runSetup
|
|
17236
18709
|
});
|
|
17237
|
-
import { createHash as
|
|
18710
|
+
import { createHash as createHash3, randomBytes as randomBytes2, randomUUID as randomUUID4 } from "node:crypto";
|
|
17238
18711
|
import { createServer } from "node:http";
|
|
17239
18712
|
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
17240
18713
|
import { homedir as homedir3, platform as platform2 } from "node:os";
|
|
@@ -17243,12 +18716,15 @@ function base64url(buffer) {
|
|
|
17243
18716
|
return buffer.toString("base64url");
|
|
17244
18717
|
}
|
|
17245
18718
|
function generatePKCE() {
|
|
17246
|
-
const verifierBytes =
|
|
18719
|
+
const verifierBytes = randomBytes2(32);
|
|
17247
18720
|
const codeVerifier = base64url(verifierBytes);
|
|
17248
|
-
const challengeHash =
|
|
18721
|
+
const challengeHash = createHash3("sha256").update(codeVerifier).digest();
|
|
17249
18722
|
const codeChallenge = base64url(challengeHash);
|
|
17250
18723
|
return { codeVerifier, codeChallenge };
|
|
17251
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
|
+
}
|
|
17252
18728
|
function getAppBaseUrl() {
|
|
17253
18729
|
return process.env.SOCIALNEURON_APP_URL || "https://www.socialneuron.com";
|
|
17254
18730
|
}
|
|
@@ -17310,11 +18786,24 @@ function configureMcpClient(configPath) {
|
|
|
17310
18786
|
return false;
|
|
17311
18787
|
}
|
|
17312
18788
|
}
|
|
17313
|
-
function readBody(req) {
|
|
18789
|
+
function readBody(req, maxBytes = 16 * 1024) {
|
|
17314
18790
|
return new Promise((resolve3, reject) => {
|
|
17315
18791
|
const chunks = [];
|
|
17316
|
-
|
|
17317
|
-
|
|
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
|
+
});
|
|
17318
18807
|
req.on("error", reject);
|
|
17319
18808
|
});
|
|
17320
18809
|
}
|
|
@@ -17327,14 +18816,13 @@ async function completePkceExchange(codeVerifier, state) {
|
|
|
17327
18816
|
body: JSON.stringify({ code_verifier: codeVerifier, state })
|
|
17328
18817
|
});
|
|
17329
18818
|
if (!response.ok) {
|
|
17330
|
-
|
|
17331
|
-
console.error(` PKCE exchange failed: ${text}`);
|
|
18819
|
+
console.error(` PKCE exchange failed (HTTP ${response.status}).`);
|
|
17332
18820
|
return false;
|
|
17333
18821
|
}
|
|
17334
18822
|
const data = await response.json();
|
|
17335
18823
|
return data.success === true;
|
|
17336
|
-
} catch
|
|
17337
|
-
console.error(
|
|
18824
|
+
} catch {
|
|
18825
|
+
console.error(" PKCE exchange failed because the authentication service was unavailable.");
|
|
17338
18826
|
return false;
|
|
17339
18827
|
}
|
|
17340
18828
|
}
|
|
@@ -17345,7 +18833,8 @@ async function runSetup() {
|
|
|
17345
18833
|
console.error("");
|
|
17346
18834
|
console.error(" Privacy Notice:");
|
|
17347
18835
|
console.error(" - Your API key is stored locally in your OS keychain");
|
|
17348
|
-
console.error(" -
|
|
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");
|
|
17349
18838
|
console.error(" - Set DO_NOT_TRACK=1 to disable telemetry");
|
|
17350
18839
|
console.error(" - Data export/delete: https://www.socialneuron.com/settings");
|
|
17351
18840
|
console.error("");
|
|
@@ -17369,7 +18858,6 @@ async function runSetup() {
|
|
|
17369
18858
|
const open = (await import("open")).default;
|
|
17370
18859
|
await open(authorizeUrl.toString());
|
|
17371
18860
|
console.error(" Opening browser for authorization...");
|
|
17372
|
-
console.error(` URL: ${authorizeUrl.toString()}`);
|
|
17373
18861
|
console.error("");
|
|
17374
18862
|
console.error(" Waiting for authorization (timeout: 120s)...");
|
|
17375
18863
|
} catch {
|
|
@@ -17381,12 +18869,20 @@ async function runSetup() {
|
|
|
17381
18869
|
console.error(" Waiting for authorization (timeout: 120s)...");
|
|
17382
18870
|
}
|
|
17383
18871
|
const result = await new Promise((resolve3) => {
|
|
18872
|
+
const callbackOrigin = new URL(baseUrl).origin;
|
|
17384
18873
|
const timeout = setTimeout(() => {
|
|
17385
18874
|
server2.close();
|
|
17386
18875
|
resolve3({ error: "Authorization timed out after 120 seconds." });
|
|
17387
18876
|
}, 12e4);
|
|
17388
18877
|
server2.on("request", async (req, res) => {
|
|
17389
|
-
|
|
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");
|
|
17390
18886
|
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
17391
18887
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
17392
18888
|
res.setHeader("Access-Control-Allow-Private-Network", "true");
|
|
@@ -17404,9 +18900,9 @@ async function runSetup() {
|
|
|
17404
18900
|
res.end(JSON.stringify({ error: "State mismatch" }));
|
|
17405
18901
|
return;
|
|
17406
18902
|
}
|
|
17407
|
-
if (!data.api_key) {
|
|
18903
|
+
if (!isValidSetupApiKey(data.api_key)) {
|
|
17408
18904
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
17409
|
-
res.end(JSON.stringify({ error: "
|
|
18905
|
+
res.end(JSON.stringify({ error: "Invalid api_key" }));
|
|
17410
18906
|
return;
|
|
17411
18907
|
}
|
|
17412
18908
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -17448,9 +18944,9 @@ async function runSetup() {
|
|
|
17448
18944
|
console.error(` Key prefix: ${apiKey.substring(0, 12)}...`);
|
|
17449
18945
|
const configPaths = getConfigPaths();
|
|
17450
18946
|
let configured = false;
|
|
17451
|
-
for (const { path:
|
|
17452
|
-
if (configureMcpClient(
|
|
17453
|
-
console.error(` Configured ${name}: ${
|
|
18947
|
+
for (const { path: path3, name } of configPaths) {
|
|
18948
|
+
if (configureMcpClient(path3)) {
|
|
18949
|
+
console.error(` Configured ${name}: ${path3}`);
|
|
17454
18950
|
configured = true;
|
|
17455
18951
|
}
|
|
17456
18952
|
}
|
|
@@ -17489,12 +18985,12 @@ __export(validation_cache_exports, {
|
|
|
17489
18985
|
readValidationCache: () => readValidationCache,
|
|
17490
18986
|
writeValidationCache: () => writeValidationCache
|
|
17491
18987
|
});
|
|
17492
|
-
import { createHash as
|
|
18988
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
17493
18989
|
import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, chmodSync } from "node:fs";
|
|
17494
18990
|
import { join as join4 } from "node:path";
|
|
17495
18991
|
import { homedir as homedir4 } from "node:os";
|
|
17496
18992
|
function keyFingerprint(apiKey) {
|
|
17497
|
-
return `sha256:${
|
|
18993
|
+
return `sha256:${createHash4("sha256").update(apiKey, "utf8").digest("hex")}`;
|
|
17498
18994
|
}
|
|
17499
18995
|
function readValidationCache(apiKey) {
|
|
17500
18996
|
try {
|
|
@@ -17635,8 +19131,7 @@ async function runLoginDevice() {
|
|
|
17635
19131
|
body: JSON.stringify({})
|
|
17636
19132
|
});
|
|
17637
19133
|
if (!response.ok) {
|
|
17638
|
-
|
|
17639
|
-
console.error(` Error: Failed to create device code. ${text}`);
|
|
19134
|
+
console.error(` Error: Failed to create device code (HTTP ${response.status}).`);
|
|
17640
19135
|
process.exit(1);
|
|
17641
19136
|
}
|
|
17642
19137
|
const data = await response.json();
|
|
@@ -17886,7 +19381,7 @@ async function runHealthCheck(options) {
|
|
|
17886
19381
|
checks.push({
|
|
17887
19382
|
name: "Connectivity",
|
|
17888
19383
|
ok: false,
|
|
17889
|
-
detail:
|
|
19384
|
+
detail: "Failed to reach the authentication service."
|
|
17890
19385
|
});
|
|
17891
19386
|
}
|
|
17892
19387
|
}
|
|
@@ -17910,7 +19405,7 @@ async function runHealthCheck(options) {
|
|
|
17910
19405
|
checks.push({
|
|
17911
19406
|
name: "Connectivity",
|
|
17912
19407
|
ok: false,
|
|
17913
|
-
detail:
|
|
19408
|
+
detail: "Network request failed."
|
|
17914
19409
|
});
|
|
17915
19410
|
}
|
|
17916
19411
|
if (apiKey) {
|
|
@@ -18111,17 +19606,17 @@ import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js
|
|
|
18111
19606
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
18112
19607
|
|
|
18113
19608
|
// src/prompts.ts
|
|
18114
|
-
import { z as
|
|
19609
|
+
import { z as z40 } from "zod";
|
|
18115
19610
|
function registerPrompts(server2) {
|
|
18116
19611
|
server2.prompt(
|
|
18117
19612
|
"create_weekly_content_plan",
|
|
18118
19613
|
"Generate a full week of social media content (7 days, multiple platforms). Returns a structured plan with topics, formats, and posting times.",
|
|
18119
19614
|
{
|
|
18120
|
-
niche:
|
|
18121
|
-
platforms:
|
|
19615
|
+
niche: z40.string().describe('Your content niche or industry (e.g., "fitness coaching", "SaaS marketing")'),
|
|
19616
|
+
platforms: z40.string().optional().describe(
|
|
18122
19617
|
'Comma-separated platforms to target (default: "YouTube, Instagram, TikTok, LinkedIn")'
|
|
18123
19618
|
),
|
|
18124
|
-
tone:
|
|
19619
|
+
tone: z40.string().optional().describe('Brand tone of voice (e.g., "professional", "casual", "bold and edgy")')
|
|
18125
19620
|
},
|
|
18126
19621
|
({ niche, platforms, tone }) => {
|
|
18127
19622
|
const targetPlatforms = platforms || "YouTube, Instagram, TikTok, LinkedIn";
|
|
@@ -18163,8 +19658,8 @@ After building the plan, use \`save_content_plan\` to save it.`
|
|
|
18163
19658
|
"analyze_top_content",
|
|
18164
19659
|
"Analyze your best-performing posts to identify patterns and replicate success. Returns insights on hooks, formats, timing, and topics that resonate.",
|
|
18165
19660
|
{
|
|
18166
|
-
timeframe:
|
|
18167
|
-
platform:
|
|
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")')
|
|
18168
19663
|
},
|
|
18169
19664
|
({ timeframe, platform: platform3 }) => {
|
|
18170
19665
|
const period = timeframe || "30 days";
|
|
@@ -18201,10 +19696,10 @@ Format as a clear, actionable performance report.`
|
|
|
18201
19696
|
"repurpose_content",
|
|
18202
19697
|
"Take one piece of content and transform it into 8-10 pieces across multiple platforms and formats.",
|
|
18203
19698
|
{
|
|
18204
|
-
source:
|
|
19699
|
+
source: z40.string().describe(
|
|
18205
19700
|
"The source content to repurpose \u2014 a URL, transcript, or the content text itself"
|
|
18206
19701
|
),
|
|
18207
|
-
target_platforms:
|
|
19702
|
+
target_platforms: z40.string().optional().describe(
|
|
18208
19703
|
'Comma-separated target platforms (default: "Twitter, LinkedIn, Instagram, TikTok, YouTube")'
|
|
18209
19704
|
)
|
|
18210
19705
|
},
|
|
@@ -18246,9 +19741,9 @@ For each piece, include the platform, format, character count, and suggested pos
|
|
|
18246
19741
|
"setup_brand_voice",
|
|
18247
19742
|
"Define or refine your brand voice profile so all generated content stays on-brand. Walks through tone, audience, values, and style.",
|
|
18248
19743
|
{
|
|
18249
|
-
brand_name:
|
|
18250
|
-
industry:
|
|
18251
|
-
website:
|
|
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")
|
|
18252
19747
|
},
|
|
18253
19748
|
({ brand_name, industry, website }) => {
|
|
18254
19749
|
const industryContext = industry ? ` in the ${industry} space` : "";
|
|
@@ -18652,10 +20147,10 @@ var command = process.argv[2];
|
|
|
18652
20147
|
if (command === "--version" || command === "-v") {
|
|
18653
20148
|
const { readFileSync: readFileSync5 } = await import("node:fs");
|
|
18654
20149
|
const { resolve: resolve3, dirname } = await import("node:path");
|
|
18655
|
-
const { fileURLToPath } = await import("node:url");
|
|
20150
|
+
const { fileURLToPath: fileURLToPath3 } = await import("node:url");
|
|
18656
20151
|
let version = MCP_VERSION;
|
|
18657
20152
|
try {
|
|
18658
|
-
const pkgPath = resolve3(dirname(
|
|
20153
|
+
const pkgPath = resolve3(dirname(fileURLToPath3(import.meta.url)), "..", "package.json");
|
|
18659
20154
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
|
|
18660
20155
|
version = pkg.version;
|
|
18661
20156
|
} catch {
|