@socialneuron/mcp-server 1.6.0 → 1.7.0
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/dist/http.js +2558 -160
- package/dist/index.js +2851 -175
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var MCP_VERSION;
|
|
|
14
14
|
var init_version = __esm({
|
|
15
15
|
"src/lib/version.ts"() {
|
|
16
16
|
"use strict";
|
|
17
|
-
MCP_VERSION = "1.
|
|
17
|
+
MCP_VERSION = "1.7.0";
|
|
18
18
|
}
|
|
19
19
|
});
|
|
20
20
|
|
|
@@ -311,7 +311,7 @@ __export(api_keys_exports, {
|
|
|
311
311
|
async function validateApiKey(apiKey) {
|
|
312
312
|
const supabaseUrl = getSupabaseUrl();
|
|
313
313
|
try {
|
|
314
|
-
const anonKey =
|
|
314
|
+
const anonKey = getCloudAnonKey();
|
|
315
315
|
const response = await fetch(
|
|
316
316
|
`${supabaseUrl}/functions/v1/mcp-auth?action=validate-key-public`,
|
|
317
317
|
{
|
|
@@ -346,13 +346,12 @@ var init_api_keys = __esm({
|
|
|
346
346
|
// src/lib/supabase.ts
|
|
347
347
|
var supabase_exports = {};
|
|
348
348
|
__export(supabase_exports, {
|
|
349
|
-
|
|
350
|
-
CLOUD_SUPABASE_URL: () => CLOUD_SUPABASE_URL,
|
|
349
|
+
fetchCloudConfig: () => fetchCloudConfig,
|
|
351
350
|
getAuthMode: () => getAuthMode,
|
|
352
351
|
getAuthenticatedApiKey: () => getAuthenticatedApiKey,
|
|
353
|
-
getAuthenticatedEmail: () => getAuthenticatedEmail,
|
|
354
352
|
getAuthenticatedExpiresAt: () => getAuthenticatedExpiresAt,
|
|
355
353
|
getAuthenticatedScopes: () => getAuthenticatedScopes,
|
|
354
|
+
getCloudAnonKey: () => getCloudAnonKey,
|
|
356
355
|
getDefaultProjectId: () => getDefaultProjectId,
|
|
357
356
|
getDefaultUserId: () => getDefaultUserId,
|
|
358
357
|
getMcpRunId: () => getMcpRunId,
|
|
@@ -377,11 +376,47 @@ function getSupabaseClient() {
|
|
|
377
376
|
}
|
|
378
377
|
return client2;
|
|
379
378
|
}
|
|
379
|
+
async function fetchCloudConfig() {
|
|
380
|
+
if (_cloudConfig) return _cloudConfig;
|
|
381
|
+
const envUrl = process.env.SOCIALNEURON_CLOUD_SUPABASE_URL || process.env.SUPABASE_URL;
|
|
382
|
+
const envAnon = process.env.SUPABASE_ANON_KEY || process.env.SOCIALNEURON_ANON_KEY || process.env.VITE_SUPABASE_ANON_KEY;
|
|
383
|
+
if (envUrl && envAnon) {
|
|
384
|
+
_cloudConfig = { supabaseUrl: envUrl, anonKey: envAnon };
|
|
385
|
+
return _cloudConfig;
|
|
386
|
+
}
|
|
387
|
+
try {
|
|
388
|
+
const resp = await fetch(CLOUD_CONFIG_URL, {
|
|
389
|
+
signal: AbortSignal.timeout(5e3)
|
|
390
|
+
});
|
|
391
|
+
if (!resp.ok) {
|
|
392
|
+
throw new Error(`Config fetch failed: ${resp.status}`);
|
|
393
|
+
}
|
|
394
|
+
const config = await resp.json();
|
|
395
|
+
_cloudConfig = config;
|
|
396
|
+
return _cloudConfig;
|
|
397
|
+
} catch (err) {
|
|
398
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
399
|
+
throw new Error(
|
|
400
|
+
`Failed to fetch cloud config from ${CLOUD_CONFIG_URL}: ${msg}. Set SUPABASE_URL and SUPABASE_ANON_KEY environment variables as a fallback.`
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
380
404
|
function getSupabaseUrl() {
|
|
381
405
|
if (SUPABASE_URL) return SUPABASE_URL;
|
|
382
406
|
const cloudOverride = process.env.SOCIALNEURON_CLOUD_SUPABASE_URL;
|
|
383
407
|
if (cloudOverride) return cloudOverride;
|
|
384
|
-
return
|
|
408
|
+
if (_cloudConfig) return _cloudConfig.supabaseUrl;
|
|
409
|
+
throw new Error(
|
|
410
|
+
"Supabase URL not configured. Run: npx @socialneuron/mcp-server setup"
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
function getCloudAnonKey() {
|
|
414
|
+
const envAnon = process.env.SUPABASE_ANON_KEY || process.env.SOCIALNEURON_ANON_KEY || process.env.VITE_SUPABASE_ANON_KEY;
|
|
415
|
+
if (envAnon) return envAnon;
|
|
416
|
+
if (_cloudConfig) return _cloudConfig.anonKey;
|
|
417
|
+
throw new Error(
|
|
418
|
+
"Supabase anon key not available. Call fetchCloudConfig() first or set SUPABASE_ANON_KEY."
|
|
419
|
+
);
|
|
385
420
|
}
|
|
386
421
|
function getServiceKey() {
|
|
387
422
|
if (!SUPABASE_SERVICE_KEY) {
|
|
@@ -428,6 +463,12 @@ async function getDefaultProjectId() {
|
|
|
428
463
|
return null;
|
|
429
464
|
}
|
|
430
465
|
async function initializeAuth() {
|
|
466
|
+
if (!SUPABASE_URL) {
|
|
467
|
+
try {
|
|
468
|
+
await fetchCloudConfig();
|
|
469
|
+
} catch {
|
|
470
|
+
}
|
|
471
|
+
}
|
|
431
472
|
const { loadApiKey: loadApiKey2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
|
|
432
473
|
const apiKey = await loadApiKey2();
|
|
433
474
|
if (apiKey) {
|
|
@@ -438,7 +479,6 @@ async function initializeAuth() {
|
|
|
438
479
|
_authMode = "api-key";
|
|
439
480
|
authenticatedUserId = result.userId;
|
|
440
481
|
authenticatedScopes = result.scopes && result.scopes.length > 0 ? result.scopes : ["mcp:read"];
|
|
441
|
-
authenticatedEmail = result.email || null;
|
|
442
482
|
authenticatedExpiresAt = result.expiresAt || null;
|
|
443
483
|
console.error(
|
|
444
484
|
"[MCP] Authenticated via API key (prefix: " + apiKey.substring(0, 6) + "..." + apiKey.slice(-4) + ")"
|
|
@@ -494,9 +534,6 @@ function getMcpRunId() {
|
|
|
494
534
|
function getAuthenticatedScopes() {
|
|
495
535
|
return authenticatedScopes;
|
|
496
536
|
}
|
|
497
|
-
function getAuthenticatedEmail() {
|
|
498
|
-
return authenticatedEmail;
|
|
499
|
-
}
|
|
500
537
|
function getAuthenticatedExpiresAt() {
|
|
501
538
|
return authenticatedExpiresAt;
|
|
502
539
|
}
|
|
@@ -535,7 +572,7 @@ async function logMcpToolInvocation(args) {
|
|
|
535
572
|
captureToolEvent(args).catch(() => {
|
|
536
573
|
});
|
|
537
574
|
}
|
|
538
|
-
var SUPABASE_URL, SUPABASE_SERVICE_KEY, client2, _authMode, authenticatedUserId, authenticatedScopes,
|
|
575
|
+
var SUPABASE_URL, SUPABASE_SERVICE_KEY, client2, _authMode, authenticatedUserId, authenticatedScopes, authenticatedExpiresAt, authenticatedApiKey, MCP_RUN_ID, CLOUD_CONFIG_URL, _cloudConfig, projectIdCache;
|
|
539
576
|
var init_supabase = __esm({
|
|
540
577
|
"src/lib/supabase.ts"() {
|
|
541
578
|
"use strict";
|
|
@@ -547,12 +584,11 @@ var init_supabase = __esm({
|
|
|
547
584
|
_authMode = "service-role";
|
|
548
585
|
authenticatedUserId = null;
|
|
549
586
|
authenticatedScopes = [];
|
|
550
|
-
authenticatedEmail = null;
|
|
551
587
|
authenticatedExpiresAt = null;
|
|
552
588
|
authenticatedApiKey = null;
|
|
553
589
|
MCP_RUN_ID = randomUUID();
|
|
554
|
-
|
|
555
|
-
|
|
590
|
+
CLOUD_CONFIG_URL = process.env.SOCIALNEURON_CONFIG_URL || "https://mcp.socialneuron.com/config";
|
|
591
|
+
_cloudConfig = null;
|
|
556
592
|
projectIdCache = /* @__PURE__ */ new Map();
|
|
557
593
|
}
|
|
558
594
|
});
|
|
@@ -969,6 +1005,24 @@ var init_tool_catalog = __esm({
|
|
|
969
1005
|
module: "brand",
|
|
970
1006
|
scope: "mcp:read"
|
|
971
1007
|
},
|
|
1008
|
+
{
|
|
1009
|
+
name: "get_brand_runtime",
|
|
1010
|
+
description: "Get the full 4-layer brand runtime (messaging, voice, visual, constraints)",
|
|
1011
|
+
module: "brandRuntime",
|
|
1012
|
+
scope: "mcp:read"
|
|
1013
|
+
},
|
|
1014
|
+
{
|
|
1015
|
+
name: "explain_brand_system",
|
|
1016
|
+
description: "Explain brand completeness, confidence, and recommendations",
|
|
1017
|
+
module: "brandRuntime",
|
|
1018
|
+
scope: "mcp:read"
|
|
1019
|
+
},
|
|
1020
|
+
{
|
|
1021
|
+
name: "check_brand_consistency",
|
|
1022
|
+
description: "Check content text for brand voice/vocabulary/claim consistency",
|
|
1023
|
+
module: "brandRuntime",
|
|
1024
|
+
scope: "mcp:read"
|
|
1025
|
+
},
|
|
972
1026
|
{
|
|
973
1027
|
name: "save_brand_profile",
|
|
974
1028
|
description: "Save or update brand profile",
|
|
@@ -1007,6 +1061,12 @@ var init_tool_catalog = __esm({
|
|
|
1007
1061
|
module: "remotion",
|
|
1008
1062
|
scope: "mcp:read"
|
|
1009
1063
|
},
|
|
1064
|
+
{
|
|
1065
|
+
name: "render_template_video",
|
|
1066
|
+
description: "Render a template video in the cloud via async job",
|
|
1067
|
+
module: "remotion",
|
|
1068
|
+
scope: "mcp:write"
|
|
1069
|
+
},
|
|
1010
1070
|
// youtube-analytics
|
|
1011
1071
|
{
|
|
1012
1072
|
name: "fetch_youtube_analytics",
|
|
@@ -1179,6 +1239,58 @@ var init_tool_catalog = __esm({
|
|
|
1179
1239
|
description: "Search and discover available MCP tools",
|
|
1180
1240
|
module: "discovery",
|
|
1181
1241
|
scope: "mcp:read"
|
|
1242
|
+
},
|
|
1243
|
+
// pipeline
|
|
1244
|
+
{
|
|
1245
|
+
name: "check_pipeline_readiness",
|
|
1246
|
+
description: "Pre-flight check before running a content pipeline",
|
|
1247
|
+
module: "pipeline",
|
|
1248
|
+
scope: "mcp:read"
|
|
1249
|
+
},
|
|
1250
|
+
{
|
|
1251
|
+
name: "run_content_pipeline",
|
|
1252
|
+
description: "End-to-end content pipeline: plan \u2192 quality \u2192 approve \u2192 schedule",
|
|
1253
|
+
module: "pipeline",
|
|
1254
|
+
scope: "mcp:autopilot"
|
|
1255
|
+
},
|
|
1256
|
+
{
|
|
1257
|
+
name: "get_pipeline_status",
|
|
1258
|
+
description: "Check status of a pipeline run",
|
|
1259
|
+
module: "pipeline",
|
|
1260
|
+
scope: "mcp:read"
|
|
1261
|
+
},
|
|
1262
|
+
{
|
|
1263
|
+
name: "auto_approve_plan",
|
|
1264
|
+
description: "Batch auto-approve posts meeting quality thresholds",
|
|
1265
|
+
module: "pipeline",
|
|
1266
|
+
scope: "mcp:autopilot"
|
|
1267
|
+
},
|
|
1268
|
+
// suggest
|
|
1269
|
+
{
|
|
1270
|
+
name: "suggest_next_content",
|
|
1271
|
+
description: "Suggest next content topics based on performance data",
|
|
1272
|
+
module: "suggest",
|
|
1273
|
+
scope: "mcp:read"
|
|
1274
|
+
},
|
|
1275
|
+
// digest
|
|
1276
|
+
{
|
|
1277
|
+
name: "generate_performance_digest",
|
|
1278
|
+
description: "Generate a performance summary with trends and recommendations",
|
|
1279
|
+
module: "digest",
|
|
1280
|
+
scope: "mcp:analytics"
|
|
1281
|
+
},
|
|
1282
|
+
{
|
|
1283
|
+
name: "detect_anomalies",
|
|
1284
|
+
description: "Detect significant performance changes (spikes, drops, viral)",
|
|
1285
|
+
module: "digest",
|
|
1286
|
+
scope: "mcp:analytics"
|
|
1287
|
+
},
|
|
1288
|
+
// autopilot (addition)
|
|
1289
|
+
{
|
|
1290
|
+
name: "create_autopilot_config",
|
|
1291
|
+
description: "Create a new autopilot configuration",
|
|
1292
|
+
module: "autopilot",
|
|
1293
|
+
scope: "mcp:autopilot"
|
|
1182
1294
|
}
|
|
1183
1295
|
];
|
|
1184
1296
|
}
|
|
@@ -2369,7 +2481,6 @@ async function handleInfo(args, asJson) {
|
|
|
2369
2481
|
const result = await validateApiKey2(apiKey);
|
|
2370
2482
|
if (result.valid) {
|
|
2371
2483
|
info.auth = {
|
|
2372
|
-
email: result.email || null,
|
|
2373
2484
|
scopes: result.scopes || [],
|
|
2374
2485
|
expiresAt: result.expiresAt || null
|
|
2375
2486
|
};
|
|
@@ -2401,7 +2512,7 @@ async function handleInfo(args, asJson) {
|
|
|
2401
2512
|
console.error("Auth: not configured");
|
|
2402
2513
|
} else if (info.auth) {
|
|
2403
2514
|
const auth = info.auth;
|
|
2404
|
-
console.error(
|
|
2515
|
+
console.error("Auth: authenticated");
|
|
2405
2516
|
console.error(`Scopes: ${auth.scopes.length > 0 ? auth.scopes.join(", ") : "none"}`);
|
|
2406
2517
|
if (auth.expiresAt) {
|
|
2407
2518
|
console.error(`Expires: ${auth.expiresAt}`);
|
|
@@ -2954,7 +3065,7 @@ __export(setup_exports, {
|
|
|
2954
3065
|
runLogout: () => runLogout,
|
|
2955
3066
|
runSetup: () => runSetup
|
|
2956
3067
|
});
|
|
2957
|
-
import { createHash as createHash4, randomBytes, randomUUID as
|
|
3068
|
+
import { createHash as createHash4, randomBytes, randomUUID as randomUUID4 } from "node:crypto";
|
|
2958
3069
|
import {
|
|
2959
3070
|
createServer
|
|
2960
3071
|
} from "node:http";
|
|
@@ -2975,7 +3086,11 @@ function getAppBaseUrl() {
|
|
|
2975
3086
|
return process.env.SOCIALNEURON_APP_URL || "https://www.socialneuron.com";
|
|
2976
3087
|
}
|
|
2977
3088
|
function getDefaultSupabaseUrl() {
|
|
2978
|
-
|
|
3089
|
+
try {
|
|
3090
|
+
return getSupabaseUrl();
|
|
3091
|
+
} catch {
|
|
3092
|
+
return "https://mcp.socialneuron.com";
|
|
3093
|
+
}
|
|
2979
3094
|
}
|
|
2980
3095
|
function getConfigPaths() {
|
|
2981
3096
|
const paths = [];
|
|
@@ -3081,7 +3196,7 @@ async function runSetup() {
|
|
|
3081
3196
|
);
|
|
3082
3197
|
console.error("");
|
|
3083
3198
|
const { codeVerifier, codeChallenge } = generatePKCE();
|
|
3084
|
-
const state =
|
|
3199
|
+
const state = randomUUID4();
|
|
3085
3200
|
const { server: server2, port } = await new Promise((resolve3, reject) => {
|
|
3086
3201
|
const srv = createServer();
|
|
3087
3202
|
srv.listen(0, "127.0.0.1", () => {
|
|
@@ -3239,7 +3354,11 @@ function prompt(question) {
|
|
|
3239
3354
|
});
|
|
3240
3355
|
}
|
|
3241
3356
|
function getDefaultSupabaseUrl2() {
|
|
3242
|
-
|
|
3357
|
+
try {
|
|
3358
|
+
return getSupabaseUrl();
|
|
3359
|
+
} catch {
|
|
3360
|
+
return "https://mcp.socialneuron.com";
|
|
3361
|
+
}
|
|
3243
3362
|
}
|
|
3244
3363
|
async function runLogin(method) {
|
|
3245
3364
|
if (method === "browser") {
|
|
@@ -3280,7 +3399,7 @@ async function runLoginPaste() {
|
|
|
3280
3399
|
await saveSupabaseUrl(getDefaultSupabaseUrl2());
|
|
3281
3400
|
console.error("");
|
|
3282
3401
|
console.error(" API key saved securely.");
|
|
3283
|
-
console.error(` User: ${result.
|
|
3402
|
+
console.error(` User: ${result.userId || "unknown"}`);
|
|
3284
3403
|
console.error(` Scopes: ${result.scopes?.join(", ") || "mcp:full"}`);
|
|
3285
3404
|
if (result.expiresAt) {
|
|
3286
3405
|
const daysLeft = Math.ceil(
|
|
@@ -3429,7 +3548,6 @@ async function runWhoami(options) {
|
|
|
3429
3548
|
if (asJson) {
|
|
3430
3549
|
const payload = {
|
|
3431
3550
|
ok: true,
|
|
3432
|
-
email: result.email || null,
|
|
3433
3551
|
userId: result.userId,
|
|
3434
3552
|
keyPrefix: apiKey.substring(0, 12) + "...",
|
|
3435
3553
|
scopes: result.scopes || ["mcp:full"],
|
|
@@ -3439,7 +3557,6 @@ async function runWhoami(options) {
|
|
|
3439
3557
|
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
3440
3558
|
} else {
|
|
3441
3559
|
console.error("");
|
|
3442
|
-
console.error(` Email: ${result.email || "(not available)"}`);
|
|
3443
3560
|
console.error(` User ID: ${result.userId}`);
|
|
3444
3561
|
console.error(` Key: ${apiKey.substring(0, 12)}...`);
|
|
3445
3562
|
console.error(` Scopes: ${result.scopes?.join(", ") || "mcp:full"}`);
|
|
@@ -3482,7 +3599,7 @@ async function runHealthCheck(options) {
|
|
|
3482
3599
|
checks.push({
|
|
3483
3600
|
name: "Key Valid",
|
|
3484
3601
|
ok: true,
|
|
3485
|
-
detail: `User: ${result.
|
|
3602
|
+
detail: `User: ${result.userId}`
|
|
3486
3603
|
});
|
|
3487
3604
|
checks.push({
|
|
3488
3605
|
name: "Scopes",
|
|
@@ -3595,7 +3712,7 @@ async function runRepl() {
|
|
|
3595
3712
|
Social Neuron CLI v${MCP_VERSION} \u2014 Interactive Mode
|
|
3596
3713
|
`);
|
|
3597
3714
|
process.stderr.write("Type a command, .help for help, or .exit to quit.\n\n");
|
|
3598
|
-
let
|
|
3715
|
+
let authUserId = null;
|
|
3599
3716
|
try {
|
|
3600
3717
|
const { loadApiKey: loadApiKey2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
|
|
3601
3718
|
const { validateApiKey: validateApiKey2 } = await Promise.resolve().then(() => (init_api_keys(), api_keys_exports));
|
|
@@ -3603,8 +3720,8 @@ Social Neuron CLI v${MCP_VERSION} \u2014 Interactive Mode
|
|
|
3603
3720
|
if (key) {
|
|
3604
3721
|
const result = await validateApiKey2(key);
|
|
3605
3722
|
if (result.valid) {
|
|
3606
|
-
|
|
3607
|
-
process.stderr.write(` Authenticated
|
|
3723
|
+
authUserId = result.userId || null;
|
|
3724
|
+
process.stderr.write(` Authenticated (user: ${authUserId || "unknown"})
|
|
3608
3725
|
|
|
3609
3726
|
`);
|
|
3610
3727
|
}
|
|
@@ -3638,7 +3755,7 @@ Social Neuron CLI v${MCP_VERSION} \u2014 Interactive Mode
|
|
|
3638
3755
|
".exit",
|
|
3639
3756
|
".clear"
|
|
3640
3757
|
];
|
|
3641
|
-
const promptStr =
|
|
3758
|
+
const promptStr = authUserId ? `sn[${authUserId.substring(0, 8)}]> ` : "sn> ";
|
|
3642
3759
|
const rl = createInterface2({
|
|
3643
3760
|
input: process.stdin,
|
|
3644
3761
|
output: process.stderr,
|
|
@@ -3757,6 +3874,9 @@ var TOOL_SCOPES = {
|
|
|
3757
3874
|
get_best_posting_times: "mcp:read",
|
|
3758
3875
|
extract_brand: "mcp:read",
|
|
3759
3876
|
get_brand_profile: "mcp:read",
|
|
3877
|
+
get_brand_runtime: "mcp:read",
|
|
3878
|
+
explain_brand_system: "mcp:read",
|
|
3879
|
+
check_brand_consistency: "mcp:read",
|
|
3760
3880
|
get_ideation_context: "mcp:read",
|
|
3761
3881
|
get_credit_balance: "mcp:read",
|
|
3762
3882
|
get_budget_status: "mcp:read",
|
|
@@ -3772,6 +3892,7 @@ var TOOL_SCOPES = {
|
|
|
3772
3892
|
generate_image: "mcp:write",
|
|
3773
3893
|
check_status: "mcp:read",
|
|
3774
3894
|
render_demo_video: "mcp:write",
|
|
3895
|
+
render_template_video: "mcp:write",
|
|
3775
3896
|
save_brand_profile: "mcp:write",
|
|
3776
3897
|
update_platform_voice: "mcp:write",
|
|
3777
3898
|
create_storyboard: "mcp:write",
|
|
@@ -3810,7 +3931,19 @@ var TOOL_SCOPES = {
|
|
|
3810
3931
|
// mcp:read (usage is read-only)
|
|
3811
3932
|
get_mcp_usage: "mcp:read",
|
|
3812
3933
|
list_plan_approvals: "mcp:read",
|
|
3813
|
-
search_tools: "mcp:read"
|
|
3934
|
+
search_tools: "mcp:read",
|
|
3935
|
+
// mcp:read (pipeline readiness + status are read-only)
|
|
3936
|
+
check_pipeline_readiness: "mcp:read",
|
|
3937
|
+
get_pipeline_status: "mcp:read",
|
|
3938
|
+
// mcp:autopilot (pipeline orchestration + approval automation)
|
|
3939
|
+
run_content_pipeline: "mcp:autopilot",
|
|
3940
|
+
auto_approve_plan: "mcp:autopilot",
|
|
3941
|
+
create_autopilot_config: "mcp:autopilot",
|
|
3942
|
+
// mcp:read (suggestions are read-only, no credit cost)
|
|
3943
|
+
suggest_next_content: "mcp:read",
|
|
3944
|
+
// mcp:analytics (digest and anomalies are analytics-scoped)
|
|
3945
|
+
generate_performance_digest: "mcp:analytics",
|
|
3946
|
+
detect_anomalies: "mcp:analytics"
|
|
3814
3947
|
};
|
|
3815
3948
|
function hasScope(userScopes, required) {
|
|
3816
3949
|
if (userScopes.includes(required)) return true;
|
|
@@ -3821,6 +3954,147 @@ function hasScope(userScopes, required) {
|
|
|
3821
3954
|
return false;
|
|
3822
3955
|
}
|
|
3823
3956
|
|
|
3957
|
+
// src/lib/tool-annotations.ts
|
|
3958
|
+
var ACRONYMS = {
|
|
3959
|
+
youtube: "YouTube",
|
|
3960
|
+
tiktok: "TikTok",
|
|
3961
|
+
mcp: "MCP",
|
|
3962
|
+
url: "URL",
|
|
3963
|
+
ai: "AI",
|
|
3964
|
+
api: "API",
|
|
3965
|
+
dm: "DM",
|
|
3966
|
+
id: "ID"
|
|
3967
|
+
};
|
|
3968
|
+
function toTitle(name) {
|
|
3969
|
+
return name.split("_").map((w) => ACRONYMS[w] ?? w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
3970
|
+
}
|
|
3971
|
+
var SCOPE_DEFAULTS = {
|
|
3972
|
+
"mcp:read": {
|
|
3973
|
+
readOnlyHint: true,
|
|
3974
|
+
destructiveHint: false,
|
|
3975
|
+
idempotentHint: true,
|
|
3976
|
+
openWorldHint: false
|
|
3977
|
+
},
|
|
3978
|
+
"mcp:write": {
|
|
3979
|
+
readOnlyHint: false,
|
|
3980
|
+
destructiveHint: true,
|
|
3981
|
+
idempotentHint: false,
|
|
3982
|
+
openWorldHint: false
|
|
3983
|
+
},
|
|
3984
|
+
"mcp:distribute": {
|
|
3985
|
+
readOnlyHint: false,
|
|
3986
|
+
destructiveHint: true,
|
|
3987
|
+
idempotentHint: false,
|
|
3988
|
+
openWorldHint: true
|
|
3989
|
+
},
|
|
3990
|
+
"mcp:analytics": {
|
|
3991
|
+
readOnlyHint: true,
|
|
3992
|
+
destructiveHint: false,
|
|
3993
|
+
idempotentHint: true,
|
|
3994
|
+
openWorldHint: false
|
|
3995
|
+
},
|
|
3996
|
+
"mcp:comments": {
|
|
3997
|
+
readOnlyHint: false,
|
|
3998
|
+
destructiveHint: true,
|
|
3999
|
+
idempotentHint: false,
|
|
4000
|
+
openWorldHint: true
|
|
4001
|
+
},
|
|
4002
|
+
"mcp:autopilot": {
|
|
4003
|
+
readOnlyHint: false,
|
|
4004
|
+
destructiveHint: true,
|
|
4005
|
+
idempotentHint: false,
|
|
4006
|
+
openWorldHint: false
|
|
4007
|
+
}
|
|
4008
|
+
};
|
|
4009
|
+
var OVERRIDES = {
|
|
4010
|
+
// Destructive tools
|
|
4011
|
+
delete_comment: { destructiveHint: true },
|
|
4012
|
+
moderate_comment: { destructiveHint: true },
|
|
4013
|
+
// Read-only tools in non-read scopes (must also clear destructiveHint from scope default)
|
|
4014
|
+
list_comments: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
4015
|
+
list_autopilot_configs: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
4016
|
+
get_autopilot_status: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
4017
|
+
check_status: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
4018
|
+
get_content_plan: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
4019
|
+
list_plan_approvals: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
4020
|
+
// Analytics tool that triggers side effects (data refresh)
|
|
4021
|
+
refresh_platform_analytics: { readOnlyHint: false, idempotentHint: true },
|
|
4022
|
+
// Write tools that are idempotent
|
|
4023
|
+
save_brand_profile: { idempotentHint: true },
|
|
4024
|
+
update_platform_voice: { idempotentHint: true },
|
|
4025
|
+
update_autopilot_config: { idempotentHint: true },
|
|
4026
|
+
update_content_plan: { idempotentHint: true },
|
|
4027
|
+
respond_plan_approval: { idempotentHint: true },
|
|
4028
|
+
// Distribution is open-world (publishes to external platforms)
|
|
4029
|
+
schedule_post: { openWorldHint: true },
|
|
4030
|
+
schedule_content_plan: { openWorldHint: true },
|
|
4031
|
+
// Extraction reads external URLs
|
|
4032
|
+
extract_url_content: { openWorldHint: true },
|
|
4033
|
+
extract_brand: { openWorldHint: true },
|
|
4034
|
+
// Pipeline: read-only tools
|
|
4035
|
+
check_pipeline_readiness: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
4036
|
+
get_pipeline_status: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
4037
|
+
// Pipeline: orchestration tools (non-idempotent, may schedule externally)
|
|
4038
|
+
run_content_pipeline: { openWorldHint: true },
|
|
4039
|
+
auto_approve_plan: { idempotentHint: true },
|
|
4040
|
+
// Suggest: read-only
|
|
4041
|
+
suggest_next_content: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
4042
|
+
// Digest/Anomalies: read-only analytics
|
|
4043
|
+
generate_performance_digest: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
4044
|
+
detect_anomalies: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }
|
|
4045
|
+
};
|
|
4046
|
+
function buildAnnotationsMap() {
|
|
4047
|
+
const map = /* @__PURE__ */ new Map();
|
|
4048
|
+
for (const [toolName, scope] of Object.entries(TOOL_SCOPES)) {
|
|
4049
|
+
const defaults = SCOPE_DEFAULTS[scope];
|
|
4050
|
+
if (!defaults) {
|
|
4051
|
+
map.set(toolName, {
|
|
4052
|
+
title: toTitle(toolName),
|
|
4053
|
+
readOnlyHint: false,
|
|
4054
|
+
destructiveHint: true,
|
|
4055
|
+
idempotentHint: false,
|
|
4056
|
+
openWorldHint: true
|
|
4057
|
+
});
|
|
4058
|
+
continue;
|
|
4059
|
+
}
|
|
4060
|
+
const overrides = OVERRIDES[toolName] ?? {};
|
|
4061
|
+
map.set(toolName, {
|
|
4062
|
+
title: toTitle(toolName),
|
|
4063
|
+
readOnlyHint: overrides.readOnlyHint ?? defaults.readOnlyHint,
|
|
4064
|
+
destructiveHint: overrides.destructiveHint ?? defaults.destructiveHint,
|
|
4065
|
+
idempotentHint: overrides.idempotentHint ?? defaults.idempotentHint,
|
|
4066
|
+
openWorldHint: overrides.openWorldHint ?? defaults.openWorldHint
|
|
4067
|
+
});
|
|
4068
|
+
}
|
|
4069
|
+
return map;
|
|
4070
|
+
}
|
|
4071
|
+
function applyAnnotations(server2) {
|
|
4072
|
+
const annotations = buildAnnotationsMap();
|
|
4073
|
+
const registeredTools = server2._registeredTools;
|
|
4074
|
+
if (!registeredTools || typeof registeredTools !== "object") {
|
|
4075
|
+
console.warn("[annotations] Could not access _registeredTools \u2014 annotations not applied");
|
|
4076
|
+
return;
|
|
4077
|
+
}
|
|
4078
|
+
const entries = Object.entries(registeredTools);
|
|
4079
|
+
let applied = 0;
|
|
4080
|
+
for (const [toolName, tool] of entries) {
|
|
4081
|
+
const ann = annotations.get(toolName);
|
|
4082
|
+
if (ann && typeof tool.update === "function") {
|
|
4083
|
+
tool.update({
|
|
4084
|
+
annotations: {
|
|
4085
|
+
title: ann.title,
|
|
4086
|
+
readOnlyHint: ann.readOnlyHint,
|
|
4087
|
+
destructiveHint: ann.destructiveHint,
|
|
4088
|
+
idempotentHint: ann.idempotentHint,
|
|
4089
|
+
openWorldHint: ann.openWorldHint
|
|
4090
|
+
}
|
|
4091
|
+
});
|
|
4092
|
+
applied++;
|
|
4093
|
+
}
|
|
4094
|
+
}
|
|
4095
|
+
console.log(`[annotations] Applied annotations to ${applied}/${entries.length} tools`);
|
|
4096
|
+
}
|
|
4097
|
+
|
|
3824
4098
|
// src/tools/ideation.ts
|
|
3825
4099
|
init_edge_function();
|
|
3826
4100
|
import { z } from "zod";
|
|
@@ -3829,6 +4103,8 @@ import { z } from "zod";
|
|
|
3829
4103
|
var CATEGORY_CONFIGS = {
|
|
3830
4104
|
posting: { maxTokens: 30, refillRate: 30 / 60 },
|
|
3831
4105
|
// 30 req/min
|
|
4106
|
+
generation: { maxTokens: 20, refillRate: 20 / 60 },
|
|
4107
|
+
// 20 req/min — AI content generation (mcp:write)
|
|
3832
4108
|
screenshot: { maxTokens: 10, refillRate: 10 / 60 },
|
|
3833
4109
|
// 10 req/min
|
|
3834
4110
|
read: { maxTokens: 60, refillRate: 60 / 60 }
|
|
@@ -8257,6 +8533,7 @@ import { z as z7 } from "zod";
|
|
|
8257
8533
|
import { resolve as resolve2 } from "node:path";
|
|
8258
8534
|
import { mkdir as mkdir2 } from "node:fs/promises";
|
|
8259
8535
|
init_supabase();
|
|
8536
|
+
init_edge_function();
|
|
8260
8537
|
var COMPOSITIONS = [
|
|
8261
8538
|
{
|
|
8262
8539
|
id: "CaptionedClip",
|
|
@@ -8361,6 +8638,22 @@ var COMPOSITIONS = [
|
|
|
8361
8638
|
durationInFrames: 450,
|
|
8362
8639
|
fps: 30,
|
|
8363
8640
|
description: "Product ad - 15s ultra-short"
|
|
8641
|
+
},
|
|
8642
|
+
{
|
|
8643
|
+
id: "DataVizDashboard",
|
|
8644
|
+
width: 1080,
|
|
8645
|
+
height: 1920,
|
|
8646
|
+
durationInFrames: 450,
|
|
8647
|
+
fps: 30,
|
|
8648
|
+
description: "Animated data dashboard - KPIs, bar chart, donut chart, line chart (15s, 9:16)"
|
|
8649
|
+
},
|
|
8650
|
+
{
|
|
8651
|
+
id: "ReviewsTestimonial",
|
|
8652
|
+
width: 1080,
|
|
8653
|
+
height: 1920,
|
|
8654
|
+
durationInFrames: 600,
|
|
8655
|
+
fps: 30,
|
|
8656
|
+
description: "Customer review testimonial with star animations and review carousel (dynamic duration, 9:16)"
|
|
8364
8657
|
}
|
|
8365
8658
|
];
|
|
8366
8659
|
function registerRemotionTools(server2) {
|
|
@@ -8368,13 +8661,6 @@ function registerRemotionTools(server2) {
|
|
|
8368
8661
|
"list_compositions",
|
|
8369
8662
|
"List all available Remotion video compositions defined in Social Neuron. Returns composition IDs, dimensions, duration, and descriptions. Use this to discover what videos can be rendered with render_demo_video.",
|
|
8370
8663
|
{},
|
|
8371
|
-
{
|
|
8372
|
-
title: "List Compositions",
|
|
8373
|
-
readOnlyHint: true,
|
|
8374
|
-
destructiveHint: false,
|
|
8375
|
-
idempotentHint: true,
|
|
8376
|
-
openWorldHint: false
|
|
8377
|
-
},
|
|
8378
8664
|
async () => {
|
|
8379
8665
|
const lines = [`${COMPOSITIONS.length} Remotion compositions available:`, ""];
|
|
8380
8666
|
for (const comp of COMPOSITIONS) {
|
|
@@ -8403,13 +8689,6 @@ function registerRemotionTools(server2) {
|
|
|
8403
8689
|
"JSON string of input props to pass to the composition. Each composition accepts different props. Omit for defaults."
|
|
8404
8690
|
)
|
|
8405
8691
|
},
|
|
8406
|
-
{
|
|
8407
|
-
title: "Render Demo Video",
|
|
8408
|
-
readOnlyHint: false,
|
|
8409
|
-
destructiveHint: false,
|
|
8410
|
-
idempotentHint: false,
|
|
8411
|
-
openWorldHint: false
|
|
8412
|
-
},
|
|
8413
8692
|
async ({ composition_id, output_format, props }) => {
|
|
8414
8693
|
const startedAt = Date.now();
|
|
8415
8694
|
const userId = await getDefaultUserId();
|
|
@@ -8541,6 +8820,134 @@ function registerRemotionTools(server2) {
|
|
|
8541
8820
|
}
|
|
8542
8821
|
}
|
|
8543
8822
|
);
|
|
8823
|
+
server2.tool(
|
|
8824
|
+
"render_template_video",
|
|
8825
|
+
"Render a Remotion template video in the cloud. Creates an async render job that is processed by the production worker, uploaded to R2, and tracked via async_jobs. Returns a job ID that can be polled with check_status. Costs credits based on video duration (3 base + 0.1/sec). Use list_compositions to see available template IDs.",
|
|
8826
|
+
{
|
|
8827
|
+
composition_id: z7.string().describe(
|
|
8828
|
+
'Remotion composition ID. Examples: "DataVizDashboard", "ReviewsTestimonial", "CaptionedClip". Use list_compositions to see all available IDs.'
|
|
8829
|
+
),
|
|
8830
|
+
input_props: z7.string().describe(
|
|
8831
|
+
"JSON string of input props for the composition. Each composition has different required props. For DataVizDashboard: {title, kpis, barData, donutData, lineData}. For ReviewsTestimonial: {businessName, overallRating, totalReviews, reviews}."
|
|
8832
|
+
),
|
|
8833
|
+
aspect_ratio: z7.enum(["9:16", "1:1", "16:9"]).optional().describe('Output aspect ratio. Defaults to "9:16" (vertical).')
|
|
8834
|
+
},
|
|
8835
|
+
async ({ composition_id, input_props, aspect_ratio }) => {
|
|
8836
|
+
const startedAt = Date.now();
|
|
8837
|
+
const userId = await getDefaultUserId();
|
|
8838
|
+
const rateLimit = checkRateLimit("generation", `render_template:${userId}`);
|
|
8839
|
+
if (!rateLimit.allowed) {
|
|
8840
|
+
await logMcpToolInvocation({
|
|
8841
|
+
toolName: "render_template_video",
|
|
8842
|
+
status: "rate_limited",
|
|
8843
|
+
durationMs: Date.now() - startedAt,
|
|
8844
|
+
details: { retryAfter: rateLimit.retryAfter }
|
|
8845
|
+
});
|
|
8846
|
+
return {
|
|
8847
|
+
content: [
|
|
8848
|
+
{
|
|
8849
|
+
type: "text",
|
|
8850
|
+
text: `Rate limit exceeded. Retry in ~${rateLimit.retryAfter}s.`
|
|
8851
|
+
}
|
|
8852
|
+
],
|
|
8853
|
+
isError: true
|
|
8854
|
+
};
|
|
8855
|
+
}
|
|
8856
|
+
const comp = COMPOSITIONS.find((c) => c.id === composition_id);
|
|
8857
|
+
if (!comp) {
|
|
8858
|
+
await logMcpToolInvocation({
|
|
8859
|
+
toolName: "render_template_video",
|
|
8860
|
+
status: "error",
|
|
8861
|
+
durationMs: Date.now() - startedAt,
|
|
8862
|
+
details: { error: "Unknown composition", compositionId: composition_id }
|
|
8863
|
+
});
|
|
8864
|
+
return {
|
|
8865
|
+
content: [
|
|
8866
|
+
{
|
|
8867
|
+
type: "text",
|
|
8868
|
+
text: `Unknown composition "${composition_id}". Available: ${COMPOSITIONS.map((c) => c.id).join(", ")}`
|
|
8869
|
+
}
|
|
8870
|
+
],
|
|
8871
|
+
isError: true
|
|
8872
|
+
};
|
|
8873
|
+
}
|
|
8874
|
+
let inputProps;
|
|
8875
|
+
try {
|
|
8876
|
+
inputProps = JSON.parse(input_props);
|
|
8877
|
+
} catch {
|
|
8878
|
+
await logMcpToolInvocation({
|
|
8879
|
+
toolName: "render_template_video",
|
|
8880
|
+
status: "error",
|
|
8881
|
+
durationMs: Date.now() - startedAt,
|
|
8882
|
+
details: { error: "Invalid input_props JSON" }
|
|
8883
|
+
});
|
|
8884
|
+
return {
|
|
8885
|
+
content: [{ type: "text", text: `Invalid JSON in input_props.` }],
|
|
8886
|
+
isError: true
|
|
8887
|
+
};
|
|
8888
|
+
}
|
|
8889
|
+
try {
|
|
8890
|
+
const { data, error } = await callEdgeFunction("create-remotion-job", {
|
|
8891
|
+
compositionId: composition_id,
|
|
8892
|
+
inputProps,
|
|
8893
|
+
outputs: [
|
|
8894
|
+
{
|
|
8895
|
+
aspectRatio: aspect_ratio || "9:16",
|
|
8896
|
+
resolution: "1080p",
|
|
8897
|
+
codec: "h264"
|
|
8898
|
+
}
|
|
8899
|
+
]
|
|
8900
|
+
});
|
|
8901
|
+
if (error || !data?.success) {
|
|
8902
|
+
throw new Error(error || data?.error || "Failed to create render job");
|
|
8903
|
+
}
|
|
8904
|
+
await logMcpToolInvocation({
|
|
8905
|
+
toolName: "render_template_video",
|
|
8906
|
+
status: "success",
|
|
8907
|
+
durationMs: Date.now() - startedAt,
|
|
8908
|
+
details: {
|
|
8909
|
+
compositionId: composition_id,
|
|
8910
|
+
jobId: data.jobId,
|
|
8911
|
+
creditsCharged: data.creditsCharged
|
|
8912
|
+
}
|
|
8913
|
+
});
|
|
8914
|
+
return {
|
|
8915
|
+
content: [
|
|
8916
|
+
{
|
|
8917
|
+
type: "text",
|
|
8918
|
+
text: [
|
|
8919
|
+
`Render job created successfully.`,
|
|
8920
|
+
` Composition: ${composition_id}`,
|
|
8921
|
+
` Job ID: ${data.jobId}`,
|
|
8922
|
+
` Credits charged: ${data.creditsCharged}`,
|
|
8923
|
+
` Estimated duration: ${data.estimatedDurationSeconds}s`,
|
|
8924
|
+
` Content ID: ${data.contentHistoryId}`,
|
|
8925
|
+
``,
|
|
8926
|
+
`The video is rendering in the cloud. Use check_status with job_id="${data.jobId}" to poll for completion. When done, the result_url will contain the R2 video URL.`
|
|
8927
|
+
].join("\n")
|
|
8928
|
+
}
|
|
8929
|
+
]
|
|
8930
|
+
};
|
|
8931
|
+
} catch (err) {
|
|
8932
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
8933
|
+
await logMcpToolInvocation({
|
|
8934
|
+
toolName: "render_template_video",
|
|
8935
|
+
status: "error",
|
|
8936
|
+
durationMs: Date.now() - startedAt,
|
|
8937
|
+
details: { error: message, compositionId: composition_id }
|
|
8938
|
+
});
|
|
8939
|
+
return {
|
|
8940
|
+
content: [
|
|
8941
|
+
{
|
|
8942
|
+
type: "text",
|
|
8943
|
+
text: `Failed to create render job: ${message}`
|
|
8944
|
+
}
|
|
8945
|
+
],
|
|
8946
|
+
isError: true
|
|
8947
|
+
};
|
|
8948
|
+
}
|
|
8949
|
+
}
|
|
8950
|
+
);
|
|
8544
8951
|
}
|
|
8545
8952
|
|
|
8546
8953
|
// src/tools/insights.ts
|
|
@@ -9957,9 +10364,9 @@ ${"=".repeat(40)}
|
|
|
9957
10364
|
}
|
|
9958
10365
|
|
|
9959
10366
|
// src/tools/autopilot.ts
|
|
9960
|
-
|
|
9961
|
-
import { z as z15 } from "zod";
|
|
10367
|
+
init_edge_function();
|
|
9962
10368
|
init_version();
|
|
10369
|
+
import { z as z15 } from "zod";
|
|
9963
10370
|
function asEnvelope11(data) {
|
|
9964
10371
|
return {
|
|
9965
10372
|
_meta: {
|
|
@@ -9977,46 +10384,35 @@ function registerAutopilotTools(server2) {
|
|
|
9977
10384
|
active_only: z15.boolean().optional().describe("If true, only return active configs. Defaults to false (show all)."),
|
|
9978
10385
|
response_format: z15.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
9979
10386
|
},
|
|
9980
|
-
{
|
|
9981
|
-
title: "List Autopilot Configs",
|
|
9982
|
-
readOnlyHint: true,
|
|
9983
|
-
destructiveHint: false,
|
|
9984
|
-
idempotentHint: true,
|
|
9985
|
-
openWorldHint: false
|
|
9986
|
-
},
|
|
9987
10387
|
async ({ active_only, response_format }) => {
|
|
9988
10388
|
const format = response_format ?? "text";
|
|
9989
|
-
const
|
|
9990
|
-
|
|
9991
|
-
|
|
9992
|
-
|
|
9993
|
-
|
|
9994
|
-
if (active_only) {
|
|
9995
|
-
query = query.eq("is_active", true);
|
|
9996
|
-
}
|
|
9997
|
-
const { data: configs, error } = await query;
|
|
9998
|
-
if (error) {
|
|
10389
|
+
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
10390
|
+
action: "list-autopilot-configs",
|
|
10391
|
+
active_only: active_only ?? false
|
|
10392
|
+
});
|
|
10393
|
+
if (efError) {
|
|
9999
10394
|
return {
|
|
10000
10395
|
content: [
|
|
10001
10396
|
{
|
|
10002
10397
|
type: "text",
|
|
10003
|
-
text: `Error fetching autopilot configs: ${
|
|
10398
|
+
text: `Error fetching autopilot configs: ${efError}`
|
|
10004
10399
|
}
|
|
10005
10400
|
],
|
|
10006
10401
|
isError: true
|
|
10007
10402
|
};
|
|
10008
10403
|
}
|
|
10404
|
+
const configs = result?.configs ?? [];
|
|
10009
10405
|
if (format === "json") {
|
|
10010
10406
|
return {
|
|
10011
10407
|
content: [
|
|
10012
10408
|
{
|
|
10013
10409
|
type: "text",
|
|
10014
|
-
text: JSON.stringify(asEnvelope11(configs
|
|
10410
|
+
text: JSON.stringify(asEnvelope11(configs), null, 2)
|
|
10015
10411
|
}
|
|
10016
10412
|
]
|
|
10017
10413
|
};
|
|
10018
10414
|
}
|
|
10019
|
-
if (
|
|
10415
|
+
if (configs.length === 0) {
|
|
10020
10416
|
return {
|
|
10021
10417
|
content: [
|
|
10022
10418
|
{
|
|
@@ -10061,18 +10457,11 @@ ${"=".repeat(40)}
|
|
|
10061
10457
|
{
|
|
10062
10458
|
config_id: z15.string().uuid().describe("The autopilot config ID to update."),
|
|
10063
10459
|
is_active: z15.boolean().optional().describe("Enable or disable this autopilot config."),
|
|
10064
|
-
schedule_days: z15.array(z15.enum(["mon", "tue", "wed", "thu", "fri", "sat", "sun"])
|
|
10460
|
+
schedule_days: z15.array(z15.enum(["mon", "tue", "wed", "thu", "fri", "sat", "sun"])).optional().describe('Days of the week to run (e.g., ["mon", "wed", "fri"]).'),
|
|
10065
10461
|
schedule_time: z15.string().optional().describe('Time to run in HH:MM format (24h, user timezone). E.g., "09:00".'),
|
|
10066
10462
|
max_credits_per_run: z15.number().optional().describe("Maximum credits per execution."),
|
|
10067
10463
|
max_credits_per_week: z15.number().optional().describe("Maximum credits per week.")
|
|
10068
10464
|
},
|
|
10069
|
-
{
|
|
10070
|
-
title: "Update Autopilot Config",
|
|
10071
|
-
readOnlyHint: false,
|
|
10072
|
-
destructiveHint: false,
|
|
10073
|
-
idempotentHint: true,
|
|
10074
|
-
openWorldHint: false
|
|
10075
|
-
},
|
|
10076
10465
|
async ({
|
|
10077
10466
|
config_id,
|
|
10078
10467
|
is_active,
|
|
@@ -10081,22 +10470,7 @@ ${"=".repeat(40)}
|
|
|
10081
10470
|
max_credits_per_run,
|
|
10082
10471
|
max_credits_per_week
|
|
10083
10472
|
}) => {
|
|
10084
|
-
|
|
10085
|
-
const userId = await getDefaultUserId();
|
|
10086
|
-
const updates = {};
|
|
10087
|
-
if (is_active !== void 0) updates.is_active = is_active;
|
|
10088
|
-
if (max_credits_per_run !== void 0) updates.max_credits_per_run = max_credits_per_run;
|
|
10089
|
-
if (max_credits_per_week !== void 0) updates.max_credits_per_week = max_credits_per_week;
|
|
10090
|
-
if (schedule_days || schedule_time) {
|
|
10091
|
-
const { data: existing } = await supabase.from("autopilot_configs").select("schedule_config").eq("id", config_id).eq("user_id", userId).single();
|
|
10092
|
-
const existingSchedule = existing?.schedule_config || {};
|
|
10093
|
-
updates.schedule_config = {
|
|
10094
|
-
...existingSchedule,
|
|
10095
|
-
...schedule_days ? { days: schedule_days } : {},
|
|
10096
|
-
...schedule_time ? { time: schedule_time } : {}
|
|
10097
|
-
};
|
|
10098
|
-
}
|
|
10099
|
-
if (Object.keys(updates).length === 0) {
|
|
10473
|
+
if (is_active === void 0 && !schedule_days && !schedule_time && max_credits_per_run === void 0 && max_credits_per_week === void 0) {
|
|
10100
10474
|
return {
|
|
10101
10475
|
content: [
|
|
10102
10476
|
{
|
|
@@ -10106,18 +10480,37 @@ ${"=".repeat(40)}
|
|
|
10106
10480
|
]
|
|
10107
10481
|
};
|
|
10108
10482
|
}
|
|
10109
|
-
const { data:
|
|
10110
|
-
|
|
10483
|
+
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
10484
|
+
action: "update-autopilot-config",
|
|
10485
|
+
config_id,
|
|
10486
|
+
is_active,
|
|
10487
|
+
schedule_days,
|
|
10488
|
+
schedule_time,
|
|
10489
|
+
max_credits_per_run,
|
|
10490
|
+
max_credits_per_week
|
|
10491
|
+
});
|
|
10492
|
+
if (efError) {
|
|
10111
10493
|
return {
|
|
10112
10494
|
content: [
|
|
10113
10495
|
{
|
|
10114
10496
|
type: "text",
|
|
10115
|
-
text: `Error updating config: ${
|
|
10497
|
+
text: `Error updating config: ${efError}`
|
|
10116
10498
|
}
|
|
10117
10499
|
],
|
|
10118
10500
|
isError: true
|
|
10119
10501
|
};
|
|
10120
10502
|
}
|
|
10503
|
+
const updated = result?.updated;
|
|
10504
|
+
if (!updated) {
|
|
10505
|
+
return {
|
|
10506
|
+
content: [
|
|
10507
|
+
{
|
|
10508
|
+
type: "text",
|
|
10509
|
+
text: result?.message || "No changes applied."
|
|
10510
|
+
}
|
|
10511
|
+
]
|
|
10512
|
+
};
|
|
10513
|
+
}
|
|
10121
10514
|
return {
|
|
10122
10515
|
content: [
|
|
10123
10516
|
{
|
|
@@ -10136,26 +10529,19 @@ Schedule: ${JSON.stringify(updated.schedule_config)}`
|
|
|
10136
10529
|
{
|
|
10137
10530
|
response_format: z15.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
10138
10531
|
},
|
|
10139
|
-
{
|
|
10140
|
-
title: "Get Autopilot Status",
|
|
10141
|
-
readOnlyHint: true,
|
|
10142
|
-
destructiveHint: false,
|
|
10143
|
-
idempotentHint: true,
|
|
10144
|
-
openWorldHint: false
|
|
10145
|
-
},
|
|
10146
10532
|
async ({ response_format }) => {
|
|
10147
10533
|
const format = response_format ?? "text";
|
|
10148
|
-
const
|
|
10149
|
-
|
|
10150
|
-
|
|
10151
|
-
|
|
10152
|
-
|
|
10153
|
-
|
|
10154
|
-
|
|
10534
|
+
const { data: result, error: efError } = await callEdgeFunction("mcp-data", { action: "autopilot-status" });
|
|
10535
|
+
if (efError) {
|
|
10536
|
+
return {
|
|
10537
|
+
content: [{ type: "text", text: `Error fetching autopilot status: ${efError}` }],
|
|
10538
|
+
isError: true
|
|
10539
|
+
};
|
|
10540
|
+
}
|
|
10155
10541
|
const statusData = {
|
|
10156
|
-
activeConfigs:
|
|
10157
|
-
recentRuns:
|
|
10158
|
-
pendingApprovals:
|
|
10542
|
+
activeConfigs: result?.activeConfigs ?? 0,
|
|
10543
|
+
recentRuns: [],
|
|
10544
|
+
pendingApprovals: result?.pendingApprovals ?? 0
|
|
10159
10545
|
};
|
|
10160
10546
|
if (format === "json") {
|
|
10161
10547
|
return {
|
|
@@ -10176,23 +10562,94 @@ ${"=".repeat(40)}
|
|
|
10176
10562
|
text += `Pending Approvals: ${statusData.pendingApprovals}
|
|
10177
10563
|
|
|
10178
10564
|
`;
|
|
10179
|
-
|
|
10180
|
-
text += `Recent Runs:
|
|
10181
|
-
`;
|
|
10182
|
-
for (const run of statusData.recentRuns) {
|
|
10183
|
-
text += ` ${run.id.substring(0, 8)}... \u2014 ${run.status} (${run.started_at})
|
|
10184
|
-
`;
|
|
10185
|
-
}
|
|
10186
|
-
} else {
|
|
10187
|
-
text += `No recent runs.
|
|
10565
|
+
text += `No recent runs.
|
|
10188
10566
|
`;
|
|
10189
|
-
}
|
|
10190
10567
|
return {
|
|
10191
10568
|
content: [{ type: "text", text }]
|
|
10192
10569
|
};
|
|
10193
10570
|
}
|
|
10194
10571
|
);
|
|
10195
|
-
|
|
10572
|
+
server2.tool(
|
|
10573
|
+
"create_autopilot_config",
|
|
10574
|
+
"Create a new autopilot configuration for automated content pipeline execution. Defines schedule, credit budgets, and approval mode.",
|
|
10575
|
+
{
|
|
10576
|
+
name: z15.string().min(1).max(100).describe("Name for this autopilot config"),
|
|
10577
|
+
project_id: z15.string().uuid().describe("Project to run autopilot for"),
|
|
10578
|
+
mode: z15.enum(["recipe", "pipeline"]).default("pipeline").describe("Mode: recipe (legacy) or pipeline (new orchestration)"),
|
|
10579
|
+
schedule_days: z15.array(z15.enum(["mon", "tue", "wed", "thu", "fri", "sat", "sun"])).min(1).describe("Days of the week to run"),
|
|
10580
|
+
schedule_time: z15.string().describe('Time to run in HH:MM format (24h). E.g., "09:00"'),
|
|
10581
|
+
timezone: z15.string().optional().describe('Timezone (e.g., "America/New_York"). Defaults to UTC.'),
|
|
10582
|
+
max_credits_per_run: z15.number().min(0).optional().describe("Maximum credits per execution"),
|
|
10583
|
+
max_credits_per_week: z15.number().min(0).optional().describe("Maximum credits per week"),
|
|
10584
|
+
approval_mode: z15.enum(["auto", "review_all", "review_low_confidence"]).default("review_low_confidence").describe("How to handle post approvals"),
|
|
10585
|
+
is_active: z15.boolean().default(true).describe("Whether to activate immediately"),
|
|
10586
|
+
response_format: z15.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
|
|
10587
|
+
},
|
|
10588
|
+
async ({
|
|
10589
|
+
name,
|
|
10590
|
+
project_id,
|
|
10591
|
+
mode,
|
|
10592
|
+
schedule_days,
|
|
10593
|
+
schedule_time,
|
|
10594
|
+
timezone,
|
|
10595
|
+
max_credits_per_run,
|
|
10596
|
+
max_credits_per_week,
|
|
10597
|
+
approval_mode,
|
|
10598
|
+
is_active,
|
|
10599
|
+
response_format
|
|
10600
|
+
}) => {
|
|
10601
|
+
const format = response_format ?? "text";
|
|
10602
|
+
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
10603
|
+
action: "create-autopilot-config",
|
|
10604
|
+
name,
|
|
10605
|
+
projectId: project_id,
|
|
10606
|
+
mode,
|
|
10607
|
+
schedule_days,
|
|
10608
|
+
schedule_time,
|
|
10609
|
+
timezone,
|
|
10610
|
+
max_credits_per_run,
|
|
10611
|
+
max_credits_per_week,
|
|
10612
|
+
approval_mode,
|
|
10613
|
+
is_active
|
|
10614
|
+
});
|
|
10615
|
+
if (efError) {
|
|
10616
|
+
return {
|
|
10617
|
+
content: [
|
|
10618
|
+
{
|
|
10619
|
+
type: "text",
|
|
10620
|
+
text: `Error creating autopilot config: ${efError}`
|
|
10621
|
+
}
|
|
10622
|
+
],
|
|
10623
|
+
isError: true
|
|
10624
|
+
};
|
|
10625
|
+
}
|
|
10626
|
+
const created = result?.created;
|
|
10627
|
+
if (!created) {
|
|
10628
|
+
return {
|
|
10629
|
+
content: [{ type: "text", text: "Failed to create config." }],
|
|
10630
|
+
isError: true
|
|
10631
|
+
};
|
|
10632
|
+
}
|
|
10633
|
+
if (format === "json") {
|
|
10634
|
+
return {
|
|
10635
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope11(created), null, 2) }]
|
|
10636
|
+
};
|
|
10637
|
+
}
|
|
10638
|
+
return {
|
|
10639
|
+
content: [
|
|
10640
|
+
{
|
|
10641
|
+
type: "text",
|
|
10642
|
+
text: `Autopilot config created: ${created.id}
|
|
10643
|
+
Name: ${name}
|
|
10644
|
+
Mode: ${mode}
|
|
10645
|
+
Schedule: ${schedule_days.join(", ")} @ ${schedule_time}
|
|
10646
|
+
Active: ${is_active}`
|
|
10647
|
+
}
|
|
10648
|
+
]
|
|
10649
|
+
};
|
|
10650
|
+
}
|
|
10651
|
+
);
|
|
10652
|
+
}
|
|
10196
10653
|
|
|
10197
10654
|
// src/tools/extraction.ts
|
|
10198
10655
|
init_edge_function();
|
|
@@ -11874,76 +12331,2293 @@ function registerDiscoveryTools(server2) {
|
|
|
11874
12331
|
);
|
|
11875
12332
|
}
|
|
11876
12333
|
|
|
11877
|
-
// src/
|
|
11878
|
-
|
|
11879
|
-
|
|
11880
|
-
|
|
11881
|
-
|
|
11882
|
-
|
|
11883
|
-
|
|
11884
|
-
|
|
11885
|
-
|
|
11886
|
-
|
|
11887
|
-
|
|
11888
|
-
|
|
11889
|
-
|
|
12334
|
+
// src/tools/pipeline.ts
|
|
12335
|
+
init_edge_function();
|
|
12336
|
+
init_supabase();
|
|
12337
|
+
init_quality();
|
|
12338
|
+
init_version();
|
|
12339
|
+
import { z as z21 } from "zod";
|
|
12340
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
12341
|
+
|
|
12342
|
+
// src/lib/parse-utils.ts
|
|
12343
|
+
function extractJsonArray2(text) {
|
|
12344
|
+
try {
|
|
12345
|
+
const parsed = JSON.parse(text);
|
|
12346
|
+
if (Array.isArray(parsed)) return parsed;
|
|
12347
|
+
for (const key of ["posts", "plan", "content", "items", "results"]) {
|
|
12348
|
+
if (parsed[key] && Array.isArray(parsed[key])) return parsed[key];
|
|
12349
|
+
}
|
|
12350
|
+
} catch {
|
|
12351
|
+
}
|
|
12352
|
+
const match = text.match(/\[[\s\S]*\]/);
|
|
12353
|
+
if (match) {
|
|
12354
|
+
try {
|
|
12355
|
+
return JSON.parse(match[0]);
|
|
12356
|
+
} catch {
|
|
12357
|
+
}
|
|
12358
|
+
}
|
|
12359
|
+
return null;
|
|
12360
|
+
}
|
|
12361
|
+
|
|
12362
|
+
// src/tools/pipeline.ts
|
|
12363
|
+
function asEnvelope16(data) {
|
|
12364
|
+
return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
|
|
12365
|
+
}
|
|
12366
|
+
var PLATFORM_ENUM2 = z21.enum([
|
|
12367
|
+
"youtube",
|
|
12368
|
+
"tiktok",
|
|
12369
|
+
"instagram",
|
|
12370
|
+
"twitter",
|
|
12371
|
+
"linkedin",
|
|
12372
|
+
"facebook",
|
|
12373
|
+
"threads",
|
|
12374
|
+
"bluesky"
|
|
12375
|
+
]);
|
|
12376
|
+
var BASE_PLAN_CREDITS = 15;
|
|
12377
|
+
var SOURCE_EXTRACTION_CREDITS = 5;
|
|
12378
|
+
function registerPipelineTools(server2) {
|
|
12379
|
+
server2.tool(
|
|
12380
|
+
"check_pipeline_readiness",
|
|
12381
|
+
"Pre-flight check before run_content_pipeline. Verifies: sufficient credits for estimated_posts, active OAuth on target platforms, brand profile exists, no stale insights. Returns pass/fail with specific issues to fix before running the pipeline.",
|
|
12382
|
+
{
|
|
12383
|
+
project_id: z21.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
|
|
12384
|
+
platforms: z21.array(PLATFORM_ENUM2).min(1).describe("Target platforms to check"),
|
|
12385
|
+
estimated_posts: z21.number().min(1).max(50).default(5).describe("Estimated posts to generate"),
|
|
12386
|
+
response_format: z21.enum(["text", "json"]).optional().describe("Response format")
|
|
12387
|
+
},
|
|
12388
|
+
async ({ project_id, platforms, estimated_posts, response_format }) => {
|
|
12389
|
+
const format = response_format ?? "text";
|
|
12390
|
+
const startedAt = Date.now();
|
|
12391
|
+
try {
|
|
12392
|
+
const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
|
|
12393
|
+
const estimatedCost = BASE_PLAN_CREDITS + estimated_posts * 2;
|
|
12394
|
+
const { data: readiness, error: readinessError } = await callEdgeFunction(
|
|
12395
|
+
"mcp-data",
|
|
12396
|
+
{
|
|
12397
|
+
action: "pipeline-readiness",
|
|
12398
|
+
platforms,
|
|
12399
|
+
estimated_posts,
|
|
12400
|
+
...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
|
|
12401
|
+
},
|
|
12402
|
+
{ timeoutMs: 15e3 }
|
|
12403
|
+
);
|
|
12404
|
+
if (readinessError || !readiness) {
|
|
12405
|
+
throw new Error(readinessError ?? "No response from mcp-data");
|
|
12406
|
+
}
|
|
12407
|
+
const credits = readiness.credits;
|
|
12408
|
+
const connectedPlatforms = readiness.connected_platforms;
|
|
12409
|
+
const missingPlatforms = readiness.missing_platforms;
|
|
12410
|
+
const hasBrand = readiness.has_brand;
|
|
12411
|
+
const pendingApprovals = readiness.pending_approvals;
|
|
12412
|
+
const insightAge = readiness.insight_age;
|
|
12413
|
+
const insightsFresh = readiness.insights_fresh;
|
|
12414
|
+
const blockers = [];
|
|
12415
|
+
const warnings = [];
|
|
12416
|
+
if (credits < estimatedCost) {
|
|
12417
|
+
blockers.push(`Insufficient credits: ${credits} available, ~${estimatedCost} needed`);
|
|
12418
|
+
}
|
|
12419
|
+
if (missingPlatforms.length > 0) {
|
|
12420
|
+
blockers.push(`Missing connected accounts: ${missingPlatforms.join(", ")}`);
|
|
12421
|
+
}
|
|
12422
|
+
if (!hasBrand) {
|
|
12423
|
+
warnings.push("No brand profile found. Content will use generic voice.");
|
|
12424
|
+
}
|
|
12425
|
+
if (pendingApprovals > 0) {
|
|
12426
|
+
warnings.push(`${pendingApprovals} pending approval(s) from previous runs.`);
|
|
12427
|
+
}
|
|
12428
|
+
if (!insightsFresh) {
|
|
12429
|
+
warnings.push(
|
|
12430
|
+
insightAge === null ? "No performance insights available. Pipeline will skip optimization." : `Insights are ${insightAge} days old. Consider refreshing analytics.`
|
|
12431
|
+
);
|
|
12432
|
+
}
|
|
12433
|
+
const result = {
|
|
12434
|
+
ready: blockers.length === 0,
|
|
12435
|
+
checks: {
|
|
12436
|
+
credits: {
|
|
12437
|
+
available: credits,
|
|
12438
|
+
estimated_cost: estimatedCost,
|
|
12439
|
+
sufficient: credits >= estimatedCost
|
|
12440
|
+
},
|
|
12441
|
+
connected_accounts: { platforms: connectedPlatforms, missing: missingPlatforms },
|
|
12442
|
+
brand_profile: { exists: hasBrand },
|
|
12443
|
+
pending_approvals: { count: pendingApprovals },
|
|
12444
|
+
insights_available: {
|
|
12445
|
+
count: readiness.latest_insight ? 1 : 0,
|
|
12446
|
+
fresh: insightsFresh,
|
|
12447
|
+
last_generated_at: readiness.latest_insight?.generated_at ?? null
|
|
12448
|
+
}
|
|
12449
|
+
},
|
|
12450
|
+
blockers,
|
|
12451
|
+
warnings
|
|
12452
|
+
};
|
|
12453
|
+
const durationMs = Date.now() - startedAt;
|
|
12454
|
+
logMcpToolInvocation({
|
|
12455
|
+
toolName: "check_pipeline_readiness",
|
|
12456
|
+
status: "success",
|
|
12457
|
+
durationMs,
|
|
12458
|
+
details: { ready: result.ready, blockers: blockers.length, warnings: warnings.length }
|
|
12459
|
+
});
|
|
12460
|
+
if (format === "json") {
|
|
12461
|
+
return {
|
|
12462
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope16(result), null, 2) }]
|
|
12463
|
+
};
|
|
12464
|
+
}
|
|
12465
|
+
const lines = [];
|
|
12466
|
+
lines.push(`Pipeline Readiness: ${result.ready ? "READY" : "NOT READY"}`);
|
|
12467
|
+
lines.push("=".repeat(40));
|
|
12468
|
+
lines.push(
|
|
12469
|
+
`Credits: ${credits} available, ~${estimatedCost} needed \u2014 ${credits >= estimatedCost ? "OK" : "BLOCKED"}`
|
|
12470
|
+
);
|
|
12471
|
+
lines.push(
|
|
12472
|
+
`Accounts: ${connectedPlatforms.length} connected${missingPlatforms.length > 0 ? ` (missing: ${missingPlatforms.join(", ")})` : " \u2014 OK"}`
|
|
12473
|
+
);
|
|
12474
|
+
lines.push(`Brand: ${hasBrand ? "OK" : "Missing (will use generic voice)"}`);
|
|
12475
|
+
lines.push(`Pending Approvals: ${pendingApprovals}`);
|
|
12476
|
+
lines.push(
|
|
12477
|
+
`Insights: ${insightsFresh ? "Fresh" : insightAge === null ? "None available" : `${insightAge} days old`}`
|
|
12478
|
+
);
|
|
12479
|
+
if (blockers.length > 0) {
|
|
12480
|
+
lines.push("");
|
|
12481
|
+
lines.push("BLOCKERS:");
|
|
12482
|
+
for (const b of blockers) lines.push(` \u2717 ${b}`);
|
|
12483
|
+
}
|
|
12484
|
+
if (warnings.length > 0) {
|
|
12485
|
+
lines.push("");
|
|
12486
|
+
lines.push("WARNINGS:");
|
|
12487
|
+
for (const w of warnings) lines.push(` ! ${w}`);
|
|
12488
|
+
}
|
|
12489
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
12490
|
+
} catch (err) {
|
|
12491
|
+
const durationMs = Date.now() - startedAt;
|
|
12492
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
12493
|
+
logMcpToolInvocation({
|
|
12494
|
+
toolName: "check_pipeline_readiness",
|
|
12495
|
+
status: "error",
|
|
12496
|
+
durationMs,
|
|
12497
|
+
details: { error: message }
|
|
12498
|
+
});
|
|
12499
|
+
return {
|
|
12500
|
+
content: [{ type: "text", text: `Readiness check failed: ${message}` }],
|
|
12501
|
+
isError: true
|
|
12502
|
+
};
|
|
12503
|
+
}
|
|
12504
|
+
}
|
|
12505
|
+
);
|
|
12506
|
+
server2.tool(
|
|
12507
|
+
"run_content_pipeline",
|
|
12508
|
+
"Run the full content pipeline: research trends \u2192 generate plan \u2192 quality check \u2192 auto-approve \u2192 schedule posts. Chains all stages in one call for maximum efficiency. Set dry_run=true to preview the plan without publishing. Check check_pipeline_readiness first to verify credits, OAuth, and brand profile are ready.",
|
|
12509
|
+
{
|
|
12510
|
+
project_id: z21.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
|
|
12511
|
+
topic: z21.string().optional().describe("Content topic (required if no source_url)"),
|
|
12512
|
+
source_url: z21.string().optional().describe("URL to extract content from"),
|
|
12513
|
+
platforms: z21.array(PLATFORM_ENUM2).min(1).describe("Target platforms"),
|
|
12514
|
+
days: z21.number().min(1).max(7).default(5).describe("Days to plan"),
|
|
12515
|
+
posts_per_day: z21.number().min(1).max(3).default(1).describe("Posts per platform per day"),
|
|
12516
|
+
approval_mode: z21.enum(["auto", "review_all", "review_low_confidence"]).default("review_low_confidence").describe(
|
|
12517
|
+
"auto: approve all passing quality. review_all: flag everything. review_low_confidence: auto-approve high scorers."
|
|
12518
|
+
),
|
|
12519
|
+
auto_approve_threshold: z21.number().min(0).max(35).default(28).describe(
|
|
12520
|
+
"Quality score threshold for auto-approval (used in auto/review_low_confidence modes)"
|
|
12521
|
+
),
|
|
12522
|
+
max_credits: z21.number().optional().describe("Credit budget cap"),
|
|
12523
|
+
dry_run: z21.boolean().default(false).describe("If true, skip scheduling and return plan only"),
|
|
12524
|
+
skip_stages: z21.array(z21.enum(["research", "quality", "schedule"])).optional().describe("Stages to skip"),
|
|
12525
|
+
response_format: z21.enum(["text", "json"]).default("json")
|
|
12526
|
+
},
|
|
12527
|
+
async ({
|
|
12528
|
+
project_id,
|
|
12529
|
+
topic,
|
|
12530
|
+
source_url,
|
|
12531
|
+
platforms,
|
|
12532
|
+
days,
|
|
12533
|
+
posts_per_day,
|
|
12534
|
+
approval_mode,
|
|
12535
|
+
auto_approve_threshold,
|
|
12536
|
+
max_credits,
|
|
12537
|
+
dry_run,
|
|
12538
|
+
skip_stages,
|
|
12539
|
+
response_format
|
|
12540
|
+
}) => {
|
|
12541
|
+
const startedAt = Date.now();
|
|
12542
|
+
const pipelineId = randomUUID3();
|
|
12543
|
+
const stagesCompleted = [];
|
|
12544
|
+
const stagesSkipped = [];
|
|
12545
|
+
const errors = [];
|
|
12546
|
+
let creditsUsed = 0;
|
|
12547
|
+
if (!topic && !source_url) {
|
|
12548
|
+
return {
|
|
12549
|
+
content: [{ type: "text", text: "Either topic or source_url is required." }],
|
|
12550
|
+
isError: true
|
|
12551
|
+
};
|
|
12552
|
+
}
|
|
12553
|
+
const skipSet = new Set(skip_stages ?? []);
|
|
12554
|
+
try {
|
|
12555
|
+
const resolvedProjectId = project_id ?? await getDefaultProjectId() ?? void 0;
|
|
12556
|
+
const estimatedCost = BASE_PLAN_CREDITS + (source_url ? SOURCE_EXTRACTION_CREDITS : 0);
|
|
12557
|
+
const { data: budgetData } = await callEdgeFunction(
|
|
12558
|
+
"mcp-data",
|
|
12559
|
+
{
|
|
12560
|
+
action: "run-pipeline",
|
|
12561
|
+
plan_status: "budget-check",
|
|
12562
|
+
...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
|
|
12563
|
+
},
|
|
12564
|
+
{ timeoutMs: 1e4 }
|
|
12565
|
+
);
|
|
12566
|
+
const availableCredits = budgetData?.credits ?? 0;
|
|
12567
|
+
const creditLimit = max_credits ?? availableCredits;
|
|
12568
|
+
if (availableCredits < estimatedCost) {
|
|
11890
12569
|
return {
|
|
11891
12570
|
content: [
|
|
11892
12571
|
{
|
|
11893
12572
|
type: "text",
|
|
11894
|
-
text: `
|
|
12573
|
+
text: `Insufficient credits: ${availableCredits} available, ~${estimatedCost} needed.`
|
|
11895
12574
|
}
|
|
11896
12575
|
],
|
|
11897
12576
|
isError: true
|
|
11898
12577
|
};
|
|
11899
12578
|
}
|
|
11900
|
-
|
|
11901
|
-
|
|
12579
|
+
stagesCompleted.push("budget_check");
|
|
12580
|
+
await callEdgeFunction(
|
|
12581
|
+
"mcp-data",
|
|
12582
|
+
{
|
|
12583
|
+
action: "run-pipeline",
|
|
12584
|
+
plan_status: "create",
|
|
12585
|
+
pipeline_id: pipelineId,
|
|
12586
|
+
config: {
|
|
12587
|
+
topic,
|
|
12588
|
+
source_url,
|
|
12589
|
+
platforms,
|
|
12590
|
+
days,
|
|
12591
|
+
posts_per_day,
|
|
12592
|
+
approval_mode,
|
|
12593
|
+
auto_approve_threshold,
|
|
12594
|
+
dry_run,
|
|
12595
|
+
skip_stages: skip_stages ?? []
|
|
12596
|
+
},
|
|
12597
|
+
current_stage: "planning",
|
|
12598
|
+
...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
|
|
12599
|
+
},
|
|
12600
|
+
{ timeoutMs: 1e4 }
|
|
12601
|
+
);
|
|
12602
|
+
const resolvedTopic = topic ?? source_url ?? "Content plan";
|
|
12603
|
+
const { data: planData, error: planError } = await callEdgeFunction(
|
|
12604
|
+
"social-neuron-ai",
|
|
12605
|
+
{
|
|
12606
|
+
type: "generation",
|
|
12607
|
+
prompt: buildPlanPrompt(resolvedTopic, platforms, days, posts_per_day, source_url),
|
|
12608
|
+
model: "gemini-2.5-flash",
|
|
12609
|
+
responseFormat: "json",
|
|
12610
|
+
...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
|
|
12611
|
+
},
|
|
12612
|
+
{ timeoutMs: 6e4 }
|
|
12613
|
+
);
|
|
12614
|
+
if (planError || !planData) {
|
|
12615
|
+
errors.push({ stage: "planning", message: planError ?? "No AI response" });
|
|
12616
|
+
await callEdgeFunction(
|
|
12617
|
+
"mcp-data",
|
|
12618
|
+
{
|
|
12619
|
+
action: "run-pipeline",
|
|
12620
|
+
plan_status: "update",
|
|
12621
|
+
pipeline_id: pipelineId,
|
|
12622
|
+
status: "failed",
|
|
12623
|
+
stages_completed: stagesCompleted,
|
|
12624
|
+
errors,
|
|
12625
|
+
current_stage: null,
|
|
12626
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
12627
|
+
},
|
|
12628
|
+
{ timeoutMs: 1e4 }
|
|
12629
|
+
);
|
|
11902
12630
|
return {
|
|
11903
12631
|
content: [
|
|
11904
|
-
{
|
|
11905
|
-
type: "text",
|
|
11906
|
-
text: `Permission denied: '${name}' requires scope '${requiredScope}'. Generate a new key with the required scope at https://socialneuron.com/settings/developer`
|
|
11907
|
-
}
|
|
12632
|
+
{ type: "text", text: `Planning failed: ${planError ?? "No AI response"}` }
|
|
11908
12633
|
],
|
|
11909
12634
|
isError: true
|
|
11910
12635
|
};
|
|
11911
12636
|
}
|
|
11912
|
-
|
|
11913
|
-
|
|
11914
|
-
|
|
11915
|
-
|
|
11916
|
-
|
|
11917
|
-
|
|
11918
|
-
|
|
11919
|
-
|
|
11920
|
-
|
|
11921
|
-
|
|
11922
|
-
|
|
11923
|
-
|
|
11924
|
-
|
|
11925
|
-
|
|
11926
|
-
|
|
11927
|
-
|
|
11928
|
-
|
|
11929
|
-
|
|
11930
|
-
|
|
11931
|
-
|
|
11932
|
-
|
|
11933
|
-
|
|
11934
|
-
|
|
11935
|
-
|
|
11936
|
-
|
|
11937
|
-
|
|
11938
|
-
|
|
11939
|
-
|
|
11940
|
-
|
|
11941
|
-
|
|
11942
|
-
|
|
11943
|
-
|
|
11944
|
-
|
|
11945
|
-
|
|
11946
|
-
|
|
12637
|
+
creditsUsed += BASE_PLAN_CREDITS;
|
|
12638
|
+
if (!dry_run) {
|
|
12639
|
+
try {
|
|
12640
|
+
await callEdgeFunction(
|
|
12641
|
+
"mcp-data",
|
|
12642
|
+
{
|
|
12643
|
+
action: "run-pipeline",
|
|
12644
|
+
plan_status: "deduct-credits",
|
|
12645
|
+
credits_used: BASE_PLAN_CREDITS,
|
|
12646
|
+
reason: `Pipeline ${pipelineId.slice(0, 8)}: content plan generation`
|
|
12647
|
+
},
|
|
12648
|
+
{ timeoutMs: 1e4 }
|
|
12649
|
+
);
|
|
12650
|
+
} catch (deductErr) {
|
|
12651
|
+
errors.push({
|
|
12652
|
+
stage: "planning",
|
|
12653
|
+
message: `Credit deduction failed: ${deductErr instanceof Error ? deductErr.message : String(deductErr)}`
|
|
12654
|
+
});
|
|
12655
|
+
}
|
|
12656
|
+
}
|
|
12657
|
+
stagesCompleted.push("planning");
|
|
12658
|
+
const rawText = String(planData.text ?? planData.content ?? "");
|
|
12659
|
+
const postsArray = extractJsonArray2(rawText);
|
|
12660
|
+
const posts = (postsArray ?? []).map((p) => ({
|
|
12661
|
+
id: String(p.id ?? randomUUID3().slice(0, 8)),
|
|
12662
|
+
day: Number(p.day ?? 1),
|
|
12663
|
+
date: String(p.date ?? ""),
|
|
12664
|
+
platform: String(p.platform ?? ""),
|
|
12665
|
+
content_type: p.content_type ?? "caption",
|
|
12666
|
+
caption: String(p.caption ?? ""),
|
|
12667
|
+
title: p.title ? String(p.title) : void 0,
|
|
12668
|
+
hashtags: Array.isArray(p.hashtags) ? p.hashtags.map(String) : void 0,
|
|
12669
|
+
hook: String(p.hook ?? ""),
|
|
12670
|
+
angle: String(p.angle ?? ""),
|
|
12671
|
+
visual_direction: p.visual_direction ? String(p.visual_direction) : void 0,
|
|
12672
|
+
media_type: p.media_type ? String(p.media_type) : void 0
|
|
12673
|
+
}));
|
|
12674
|
+
let postsApproved = 0;
|
|
12675
|
+
let postsFlagged = 0;
|
|
12676
|
+
if (!skipSet.has("quality")) {
|
|
12677
|
+
for (const post of posts) {
|
|
12678
|
+
const quality = evaluateQuality({
|
|
12679
|
+
caption: post.caption,
|
|
12680
|
+
title: post.title,
|
|
12681
|
+
platforms: [post.platform],
|
|
12682
|
+
threshold: auto_approve_threshold
|
|
12683
|
+
});
|
|
12684
|
+
post.quality = {
|
|
12685
|
+
score: quality.total,
|
|
12686
|
+
max_score: quality.maxTotal,
|
|
12687
|
+
passed: quality.passed,
|
|
12688
|
+
blockers: quality.blockers
|
|
12689
|
+
};
|
|
12690
|
+
if (approval_mode === "auto" && quality.passed) {
|
|
12691
|
+
post.status = "approved";
|
|
12692
|
+
postsApproved++;
|
|
12693
|
+
} else if (approval_mode === "review_low_confidence") {
|
|
12694
|
+
if (quality.total >= auto_approve_threshold && quality.blockers.length === 0) {
|
|
12695
|
+
post.status = "approved";
|
|
12696
|
+
postsApproved++;
|
|
12697
|
+
} else {
|
|
12698
|
+
post.status = "needs_edit";
|
|
12699
|
+
postsFlagged++;
|
|
12700
|
+
}
|
|
12701
|
+
} else {
|
|
12702
|
+
post.status = "pending";
|
|
12703
|
+
postsFlagged++;
|
|
12704
|
+
}
|
|
12705
|
+
}
|
|
12706
|
+
stagesCompleted.push("quality_check");
|
|
12707
|
+
} else {
|
|
12708
|
+
stagesSkipped.push("quality_check");
|
|
12709
|
+
for (const post of posts) {
|
|
12710
|
+
post.status = "approved";
|
|
12711
|
+
postsApproved++;
|
|
12712
|
+
}
|
|
12713
|
+
}
|
|
12714
|
+
const planId = randomUUID3();
|
|
12715
|
+
if (resolvedProjectId) {
|
|
12716
|
+
const startDate = /* @__PURE__ */ new Date();
|
|
12717
|
+
startDate.setDate(startDate.getDate() + 1);
|
|
12718
|
+
const endDate = new Date(startDate);
|
|
12719
|
+
endDate.setDate(endDate.getDate() + days - 1);
|
|
12720
|
+
await callEdgeFunction(
|
|
12721
|
+
"mcp-data",
|
|
12722
|
+
{
|
|
12723
|
+
action: "run-pipeline",
|
|
12724
|
+
plan_status: "persist-plan",
|
|
12725
|
+
pipeline_id: pipelineId,
|
|
12726
|
+
plan_id: planId,
|
|
12727
|
+
topic: resolvedTopic,
|
|
12728
|
+
status: postsFlagged > 0 ? "in_review" : "approved",
|
|
12729
|
+
plan_payload: {
|
|
12730
|
+
plan_id: planId,
|
|
12731
|
+
topic: resolvedTopic,
|
|
12732
|
+
platforms,
|
|
12733
|
+
posts,
|
|
12734
|
+
start_date: startDate.toISOString().split("T")[0],
|
|
12735
|
+
end_date: endDate.toISOString().split("T")[0],
|
|
12736
|
+
estimated_credits: estimatedCost,
|
|
12737
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
12738
|
+
},
|
|
12739
|
+
...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
|
|
12740
|
+
},
|
|
12741
|
+
{ timeoutMs: 1e4 }
|
|
12742
|
+
);
|
|
12743
|
+
}
|
|
12744
|
+
stagesCompleted.push("persist_plan");
|
|
12745
|
+
if (postsFlagged > 0 && resolvedProjectId) {
|
|
12746
|
+
const userId = await getDefaultUserId();
|
|
12747
|
+
const resolvedApprovalRows = posts.filter((p) => p.status !== "approved").map((post) => ({
|
|
12748
|
+
plan_id: planId,
|
|
12749
|
+
post_id: post.id,
|
|
12750
|
+
project_id: resolvedProjectId,
|
|
12751
|
+
user_id: userId,
|
|
12752
|
+
status: "pending",
|
|
12753
|
+
original_post: post,
|
|
12754
|
+
auto_approved: false
|
|
12755
|
+
}));
|
|
12756
|
+
if (resolvedApprovalRows.length > 0) {
|
|
12757
|
+
await callEdgeFunction(
|
|
12758
|
+
"mcp-data",
|
|
12759
|
+
{
|
|
12760
|
+
action: "run-pipeline",
|
|
12761
|
+
plan_status: "upsert-approvals",
|
|
12762
|
+
posts: resolvedApprovalRows
|
|
12763
|
+
},
|
|
12764
|
+
{ timeoutMs: 1e4 }
|
|
12765
|
+
);
|
|
12766
|
+
}
|
|
12767
|
+
}
|
|
12768
|
+
if (postsApproved > 0 && resolvedProjectId) {
|
|
12769
|
+
const userId = await getDefaultUserId();
|
|
12770
|
+
const autoApprovedRows = posts.filter((p) => p.status === "approved").map((post) => ({
|
|
12771
|
+
plan_id: planId,
|
|
12772
|
+
post_id: post.id,
|
|
12773
|
+
project_id: resolvedProjectId,
|
|
12774
|
+
user_id: userId,
|
|
12775
|
+
status: "approved",
|
|
12776
|
+
original_post: post,
|
|
12777
|
+
auto_approved: true
|
|
12778
|
+
}));
|
|
12779
|
+
if (autoApprovedRows.length > 0) {
|
|
12780
|
+
await callEdgeFunction(
|
|
12781
|
+
"mcp-data",
|
|
12782
|
+
{
|
|
12783
|
+
action: "run-pipeline",
|
|
12784
|
+
plan_status: "upsert-approvals",
|
|
12785
|
+
posts: autoApprovedRows
|
|
12786
|
+
},
|
|
12787
|
+
{ timeoutMs: 1e4 }
|
|
12788
|
+
);
|
|
12789
|
+
}
|
|
12790
|
+
}
|
|
12791
|
+
let postsScheduled = 0;
|
|
12792
|
+
if (!dry_run && !skipSet.has("schedule") && postsApproved > 0) {
|
|
12793
|
+
const approvedPosts = posts.filter((p) => p.status === "approved");
|
|
12794
|
+
for (const post of approvedPosts) {
|
|
12795
|
+
if (creditsUsed >= creditLimit) {
|
|
12796
|
+
errors.push({ stage: "schedule", message: "Credit limit reached" });
|
|
12797
|
+
break;
|
|
12798
|
+
}
|
|
12799
|
+
try {
|
|
12800
|
+
const { error: schedError } = await callEdgeFunction(
|
|
12801
|
+
"schedule-post",
|
|
12802
|
+
{
|
|
12803
|
+
platform: post.platform,
|
|
12804
|
+
caption: post.caption,
|
|
12805
|
+
title: post.title,
|
|
12806
|
+
hashtags: post.hashtags,
|
|
12807
|
+
media_url: post.media_url,
|
|
12808
|
+
scheduled_at: post.schedule_at,
|
|
12809
|
+
...resolvedProjectId ? { projectId: resolvedProjectId, project_id: resolvedProjectId } : {}
|
|
12810
|
+
},
|
|
12811
|
+
{ timeoutMs: 15e3 }
|
|
12812
|
+
);
|
|
12813
|
+
if (schedError) {
|
|
12814
|
+
errors.push({
|
|
12815
|
+
stage: "schedule",
|
|
12816
|
+
message: `Failed to schedule ${post.id}: ${schedError}`
|
|
12817
|
+
});
|
|
12818
|
+
} else {
|
|
12819
|
+
postsScheduled++;
|
|
12820
|
+
}
|
|
12821
|
+
} catch (schedErr) {
|
|
12822
|
+
errors.push({
|
|
12823
|
+
stage: "schedule",
|
|
12824
|
+
message: `Failed to schedule ${post.id}: ${schedErr instanceof Error ? schedErr.message : String(schedErr)}`
|
|
12825
|
+
});
|
|
12826
|
+
}
|
|
12827
|
+
}
|
|
12828
|
+
stagesCompleted.push("schedule");
|
|
12829
|
+
} else if (dry_run) {
|
|
12830
|
+
stagesSkipped.push("schedule");
|
|
12831
|
+
} else if (skipSet.has("schedule")) {
|
|
12832
|
+
stagesSkipped.push("schedule");
|
|
12833
|
+
}
|
|
12834
|
+
const finalStatus = errors.length > 0 && stagesCompleted.length <= 2 ? "failed" : postsFlagged > 0 ? "awaiting_approval" : "completed";
|
|
12835
|
+
await callEdgeFunction(
|
|
12836
|
+
"mcp-data",
|
|
12837
|
+
{
|
|
12838
|
+
action: "run-pipeline",
|
|
12839
|
+
plan_status: "update",
|
|
12840
|
+
pipeline_id: pipelineId,
|
|
12841
|
+
status: finalStatus,
|
|
12842
|
+
plan_id: planId,
|
|
12843
|
+
stages_completed: stagesCompleted,
|
|
12844
|
+
stages_skipped: stagesSkipped,
|
|
12845
|
+
current_stage: null,
|
|
12846
|
+
posts_generated: posts.length,
|
|
12847
|
+
posts_approved: postsApproved,
|
|
12848
|
+
posts_scheduled: postsScheduled,
|
|
12849
|
+
posts_flagged: postsFlagged,
|
|
12850
|
+
credits_used: creditsUsed,
|
|
12851
|
+
errors,
|
|
12852
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
12853
|
+
},
|
|
12854
|
+
{ timeoutMs: 1e4 }
|
|
12855
|
+
);
|
|
12856
|
+
const durationMs = Date.now() - startedAt;
|
|
12857
|
+
logMcpToolInvocation({
|
|
12858
|
+
toolName: "run_content_pipeline",
|
|
12859
|
+
status: "success",
|
|
12860
|
+
durationMs,
|
|
12861
|
+
details: {
|
|
12862
|
+
pipeline_id: pipelineId,
|
|
12863
|
+
posts: posts.length,
|
|
12864
|
+
approved: postsApproved,
|
|
12865
|
+
scheduled: postsScheduled,
|
|
12866
|
+
flagged: postsFlagged
|
|
12867
|
+
}
|
|
12868
|
+
});
|
|
12869
|
+
const resultPayload = {
|
|
12870
|
+
pipeline_id: pipelineId,
|
|
12871
|
+
stages_completed: stagesCompleted,
|
|
12872
|
+
stages_skipped: stagesSkipped,
|
|
12873
|
+
plan_id: planId,
|
|
12874
|
+
posts_generated: posts.length,
|
|
12875
|
+
posts_approved: postsApproved,
|
|
12876
|
+
posts_scheduled: postsScheduled,
|
|
12877
|
+
posts_flagged: postsFlagged,
|
|
12878
|
+
credits_used: creditsUsed,
|
|
12879
|
+
credits_remaining: availableCredits - creditsUsed,
|
|
12880
|
+
dry_run,
|
|
12881
|
+
next_action: postsFlagged > 0 ? `Review ${postsFlagged} flagged post(s) with list_plan_approvals and respond_plan_approval.` : postsScheduled > 0 ? "All posts scheduled. Monitor with get_pipeline_status." : "Pipeline complete.",
|
|
12882
|
+
errors: errors.length > 0 ? errors : void 0
|
|
12883
|
+
};
|
|
12884
|
+
if (response_format === "json") {
|
|
12885
|
+
return {
|
|
12886
|
+
content: [
|
|
12887
|
+
{ type: "text", text: JSON.stringify(asEnvelope16(resultPayload), null, 2) }
|
|
12888
|
+
]
|
|
12889
|
+
};
|
|
12890
|
+
}
|
|
12891
|
+
const lines = [];
|
|
12892
|
+
lines.push(`Pipeline ${pipelineId.slice(0, 8)}... ${finalStatus.toUpperCase()}`);
|
|
12893
|
+
lines.push("=".repeat(40));
|
|
12894
|
+
lines.push(`Posts generated: ${posts.length}`);
|
|
12895
|
+
lines.push(`Posts approved: ${postsApproved}`);
|
|
12896
|
+
lines.push(`Posts scheduled: ${postsScheduled}`);
|
|
12897
|
+
lines.push(`Posts flagged for review: ${postsFlagged}`);
|
|
12898
|
+
lines.push(`Credits used: ${creditsUsed}`);
|
|
12899
|
+
lines.push(`Stages: ${stagesCompleted.join(" \u2192 ")}`);
|
|
12900
|
+
if (stagesSkipped.length > 0) {
|
|
12901
|
+
lines.push(`Skipped: ${stagesSkipped.join(", ")}`);
|
|
12902
|
+
}
|
|
12903
|
+
if (errors.length > 0) {
|
|
12904
|
+
lines.push("");
|
|
12905
|
+
lines.push("Errors:");
|
|
12906
|
+
for (const e of errors) lines.push(` [${e.stage}] ${e.message}`);
|
|
12907
|
+
}
|
|
12908
|
+
lines.push("");
|
|
12909
|
+
lines.push(`Next: ${resultPayload.next_action}`);
|
|
12910
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
12911
|
+
} catch (err) {
|
|
12912
|
+
const durationMs = Date.now() - startedAt;
|
|
12913
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
12914
|
+
logMcpToolInvocation({
|
|
12915
|
+
toolName: "run_content_pipeline",
|
|
12916
|
+
status: "error",
|
|
12917
|
+
durationMs,
|
|
12918
|
+
details: { error: message }
|
|
12919
|
+
});
|
|
12920
|
+
try {
|
|
12921
|
+
await callEdgeFunction(
|
|
12922
|
+
"mcp-data",
|
|
12923
|
+
{
|
|
12924
|
+
action: "run-pipeline",
|
|
12925
|
+
plan_status: "update",
|
|
12926
|
+
pipeline_id: pipelineId,
|
|
12927
|
+
status: "failed",
|
|
12928
|
+
stages_completed: stagesCompleted,
|
|
12929
|
+
errors: [...errors, { stage: "unknown", message }],
|
|
12930
|
+
current_stage: null,
|
|
12931
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
12932
|
+
},
|
|
12933
|
+
{ timeoutMs: 1e4 }
|
|
12934
|
+
);
|
|
12935
|
+
} catch {
|
|
12936
|
+
}
|
|
12937
|
+
return {
|
|
12938
|
+
content: [{ type: "text", text: `Pipeline failed: ${message}` }],
|
|
12939
|
+
isError: true
|
|
12940
|
+
};
|
|
12941
|
+
}
|
|
12942
|
+
}
|
|
12943
|
+
);
|
|
12944
|
+
server2.tool(
|
|
12945
|
+
"get_pipeline_status",
|
|
12946
|
+
"Check status of a pipeline run, including stages completed, pending approvals, and scheduled posts.",
|
|
12947
|
+
{
|
|
12948
|
+
pipeline_id: z21.string().uuid().optional().describe("Pipeline run ID (omit for latest)"),
|
|
12949
|
+
response_format: z21.enum(["text", "json"]).optional()
|
|
12950
|
+
},
|
|
12951
|
+
async ({ pipeline_id, response_format }) => {
|
|
12952
|
+
const format = response_format ?? "text";
|
|
12953
|
+
const { data: result, error: fetchError } = await callEdgeFunction(
|
|
12954
|
+
"mcp-data",
|
|
12955
|
+
{
|
|
12956
|
+
action: "get-pipeline-status",
|
|
12957
|
+
...pipeline_id ? { pipeline_id } : {}
|
|
12958
|
+
},
|
|
12959
|
+
{ timeoutMs: 1e4 }
|
|
12960
|
+
);
|
|
12961
|
+
if (fetchError) {
|
|
12962
|
+
return {
|
|
12963
|
+
content: [{ type: "text", text: `Error: ${fetchError}` }],
|
|
12964
|
+
isError: true
|
|
12965
|
+
};
|
|
12966
|
+
}
|
|
12967
|
+
const data = result?.pipeline;
|
|
12968
|
+
if (!data) {
|
|
12969
|
+
return {
|
|
12970
|
+
content: [{ type: "text", text: "No pipeline runs found." }]
|
|
12971
|
+
};
|
|
12972
|
+
}
|
|
12973
|
+
if (format === "json") {
|
|
12974
|
+
return {
|
|
12975
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope16(data), null, 2) }]
|
|
12976
|
+
};
|
|
12977
|
+
}
|
|
12978
|
+
const lines = [];
|
|
12979
|
+
lines.push(
|
|
12980
|
+
`Pipeline ${String(data.id).slice(0, 8)}... \u2014 ${String(data.status).toUpperCase()}`
|
|
12981
|
+
);
|
|
12982
|
+
lines.push("=".repeat(40));
|
|
12983
|
+
lines.push(`Started: ${data.started_at}`);
|
|
12984
|
+
if (data.completed_at) lines.push(`Completed: ${data.completed_at}`);
|
|
12985
|
+
lines.push(
|
|
12986
|
+
`Stages: ${(Array.isArray(data.stages_completed) ? data.stages_completed : []).join(" \u2192 ") || "none"}`
|
|
12987
|
+
);
|
|
12988
|
+
if (Array.isArray(data.stages_skipped) && data.stages_skipped.length > 0) {
|
|
12989
|
+
lines.push(`Skipped: ${data.stages_skipped.join(", ")}`);
|
|
12990
|
+
}
|
|
12991
|
+
lines.push(
|
|
12992
|
+
`Posts: ${data.posts_generated} generated, ${data.posts_approved} approved, ${data.posts_scheduled} scheduled, ${data.posts_flagged} flagged`
|
|
12993
|
+
);
|
|
12994
|
+
lines.push(`Credits used: ${data.credits_used}`);
|
|
12995
|
+
if (data.plan_id) lines.push(`Plan ID: ${data.plan_id}`);
|
|
12996
|
+
const errs = data.errors;
|
|
12997
|
+
if (errs && errs.length > 0) {
|
|
12998
|
+
lines.push("");
|
|
12999
|
+
lines.push("Errors:");
|
|
13000
|
+
for (const e of errs) lines.push(` [${e.stage}] ${e.message}`);
|
|
13001
|
+
}
|
|
13002
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
13003
|
+
}
|
|
13004
|
+
);
|
|
13005
|
+
server2.tool(
|
|
13006
|
+
"auto_approve_plan",
|
|
13007
|
+
"Batch auto-approve posts in a content plan that meet quality thresholds. Posts below the threshold are flagged for manual review.",
|
|
13008
|
+
{
|
|
13009
|
+
plan_id: z21.string().uuid().describe("Content plan ID"),
|
|
13010
|
+
quality_threshold: z21.number().min(0).max(35).default(26).describe("Minimum quality score to auto-approve"),
|
|
13011
|
+
response_format: z21.enum(["text", "json"]).default("json")
|
|
13012
|
+
},
|
|
13013
|
+
async ({ plan_id, quality_threshold, response_format }) => {
|
|
13014
|
+
const startedAt = Date.now();
|
|
13015
|
+
try {
|
|
13016
|
+
const { data: loadResult, error: loadError } = await callEdgeFunction("mcp-data", { action: "auto-approve-plan", plan_id }, { timeoutMs: 1e4 });
|
|
13017
|
+
if (loadError) {
|
|
13018
|
+
return {
|
|
13019
|
+
content: [{ type: "text", text: `Failed to load plan: ${loadError}` }],
|
|
13020
|
+
isError: true
|
|
13021
|
+
};
|
|
13022
|
+
}
|
|
13023
|
+
const stored = loadResult?.plan;
|
|
13024
|
+
if (!stored?.plan_payload) {
|
|
13025
|
+
return {
|
|
13026
|
+
content: [
|
|
13027
|
+
{ type: "text", text: `No content plan found for plan_id=${plan_id}` }
|
|
13028
|
+
],
|
|
13029
|
+
isError: true
|
|
13030
|
+
};
|
|
13031
|
+
}
|
|
13032
|
+
const plan = stored.plan_payload;
|
|
13033
|
+
const posts = Array.isArray(plan.posts) ? plan.posts : [];
|
|
13034
|
+
let autoApproved = 0;
|
|
13035
|
+
let flagged = 0;
|
|
13036
|
+
let rejected = 0;
|
|
13037
|
+
const details = [];
|
|
13038
|
+
for (const post of posts) {
|
|
13039
|
+
const quality = evaluateQuality({
|
|
13040
|
+
caption: post.caption,
|
|
13041
|
+
title: post.title,
|
|
13042
|
+
platforms: [post.platform],
|
|
13043
|
+
threshold: quality_threshold
|
|
13044
|
+
});
|
|
13045
|
+
if (quality.total >= quality_threshold && quality.blockers.length === 0) {
|
|
13046
|
+
post.status = "approved";
|
|
13047
|
+
post.quality = {
|
|
13048
|
+
score: quality.total,
|
|
13049
|
+
max_score: quality.maxTotal,
|
|
13050
|
+
passed: true,
|
|
13051
|
+
blockers: []
|
|
13052
|
+
};
|
|
13053
|
+
autoApproved++;
|
|
13054
|
+
details.push({ post_id: post.id, action: "approved", score: quality.total });
|
|
13055
|
+
} else if (quality.total >= quality_threshold - 5) {
|
|
13056
|
+
post.status = "needs_edit";
|
|
13057
|
+
post.quality = {
|
|
13058
|
+
score: quality.total,
|
|
13059
|
+
max_score: quality.maxTotal,
|
|
13060
|
+
passed: false,
|
|
13061
|
+
blockers: quality.blockers
|
|
13062
|
+
};
|
|
13063
|
+
flagged++;
|
|
13064
|
+
details.push({ post_id: post.id, action: "flagged", score: quality.total });
|
|
13065
|
+
} else {
|
|
13066
|
+
post.status = "rejected";
|
|
13067
|
+
post.quality = {
|
|
13068
|
+
score: quality.total,
|
|
13069
|
+
max_score: quality.maxTotal,
|
|
13070
|
+
passed: false,
|
|
13071
|
+
blockers: quality.blockers
|
|
13072
|
+
};
|
|
13073
|
+
rejected++;
|
|
13074
|
+
details.push({ post_id: post.id, action: "rejected", score: quality.total });
|
|
13075
|
+
}
|
|
13076
|
+
}
|
|
13077
|
+
const newStatus = flagged === 0 && rejected === 0 ? "approved" : "in_review";
|
|
13078
|
+
const userId = await getDefaultUserId();
|
|
13079
|
+
const resolvedRows = posts.map((post) => ({
|
|
13080
|
+
plan_id,
|
|
13081
|
+
post_id: post.id,
|
|
13082
|
+
project_id: stored.project_id,
|
|
13083
|
+
user_id: userId,
|
|
13084
|
+
status: post.status === "approved" ? "approved" : post.status === "rejected" ? "rejected" : "pending",
|
|
13085
|
+
original_post: post,
|
|
13086
|
+
auto_approved: post.status === "approved"
|
|
13087
|
+
}));
|
|
13088
|
+
await callEdgeFunction(
|
|
13089
|
+
"mcp-data",
|
|
13090
|
+
{
|
|
13091
|
+
action: "auto-approve-plan",
|
|
13092
|
+
plan_id,
|
|
13093
|
+
plan_status: newStatus,
|
|
13094
|
+
plan_payload: { ...plan, posts },
|
|
13095
|
+
posts: resolvedRows
|
|
13096
|
+
},
|
|
13097
|
+
{ timeoutMs: 1e4 }
|
|
13098
|
+
);
|
|
13099
|
+
const durationMs = Date.now() - startedAt;
|
|
13100
|
+
logMcpToolInvocation({
|
|
13101
|
+
toolName: "auto_approve_plan",
|
|
13102
|
+
status: "success",
|
|
13103
|
+
durationMs,
|
|
13104
|
+
details: { plan_id, auto_approved: autoApproved, flagged, rejected }
|
|
13105
|
+
});
|
|
13106
|
+
const resultPayload = {
|
|
13107
|
+
plan_id,
|
|
13108
|
+
auto_approved: autoApproved,
|
|
13109
|
+
flagged_for_review: flagged,
|
|
13110
|
+
rejected,
|
|
13111
|
+
details,
|
|
13112
|
+
plan_status: newStatus
|
|
13113
|
+
};
|
|
13114
|
+
if (response_format === "json") {
|
|
13115
|
+
return {
|
|
13116
|
+
content: [
|
|
13117
|
+
{ type: "text", text: JSON.stringify(asEnvelope16(resultPayload), null, 2) }
|
|
13118
|
+
]
|
|
13119
|
+
};
|
|
13120
|
+
}
|
|
13121
|
+
const lines = [];
|
|
13122
|
+
lines.push(`Auto-Approve Results for Plan ${plan_id.slice(0, 8)}...`);
|
|
13123
|
+
lines.push("=".repeat(40));
|
|
13124
|
+
lines.push(`Auto-approved: ${autoApproved}`);
|
|
13125
|
+
lines.push(`Flagged for review: ${flagged}`);
|
|
13126
|
+
lines.push(`Rejected: ${rejected}`);
|
|
13127
|
+
lines.push(`Plan status: ${newStatus}`);
|
|
13128
|
+
if (details.length > 0) {
|
|
13129
|
+
lines.push("");
|
|
13130
|
+
for (const d of details) {
|
|
13131
|
+
lines.push(` ${d.post_id}: ${d.action} (score: ${d.score}/35)`);
|
|
13132
|
+
}
|
|
13133
|
+
}
|
|
13134
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
13135
|
+
} catch (err) {
|
|
13136
|
+
const durationMs = Date.now() - startedAt;
|
|
13137
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
13138
|
+
logMcpToolInvocation({
|
|
13139
|
+
toolName: "auto_approve_plan",
|
|
13140
|
+
status: "error",
|
|
13141
|
+
durationMs,
|
|
13142
|
+
details: { error: message }
|
|
13143
|
+
});
|
|
13144
|
+
return {
|
|
13145
|
+
content: [{ type: "text", text: `Auto-approve failed: ${message}` }],
|
|
13146
|
+
isError: true
|
|
13147
|
+
};
|
|
13148
|
+
}
|
|
13149
|
+
}
|
|
13150
|
+
);
|
|
13151
|
+
}
|
|
13152
|
+
function sanitizeTopic(raw) {
|
|
13153
|
+
return raw.replace(/[\x00-\x1f\x7f]/g, "").slice(0, 500);
|
|
13154
|
+
}
|
|
13155
|
+
function buildPlanPrompt(topic, platforms, days, postsPerDay, sourceUrl) {
|
|
13156
|
+
const safeTopic = sanitizeTopic(topic);
|
|
13157
|
+
const parts = [
|
|
13158
|
+
`Generate a ${days}-day content plan for "${safeTopic}" across platforms: ${platforms.join(", ")}.`,
|
|
13159
|
+
`${postsPerDay} post(s) per platform per day.`,
|
|
13160
|
+
sourceUrl ? `Source material URL: ${sourceUrl}` : "",
|
|
13161
|
+
"",
|
|
13162
|
+
"For each post, return a JSON object with:",
|
|
13163
|
+
" id, day, date, platform, content_type, caption, title, hashtags, hook, angle, visual_direction, media_type",
|
|
13164
|
+
"",
|
|
13165
|
+
"Return ONLY a JSON array. No surrounding text."
|
|
13166
|
+
];
|
|
13167
|
+
return parts.filter(Boolean).join("\n");
|
|
13168
|
+
}
|
|
13169
|
+
|
|
13170
|
+
// src/tools/suggest.ts
|
|
13171
|
+
init_edge_function();
|
|
13172
|
+
init_supabase();
|
|
13173
|
+
init_version();
|
|
13174
|
+
import { z as z22 } from "zod";
|
|
13175
|
+
function asEnvelope17(data) {
|
|
13176
|
+
return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
|
|
13177
|
+
}
|
|
13178
|
+
function registerSuggestTools(server2) {
|
|
13179
|
+
server2.tool(
|
|
13180
|
+
"suggest_next_content",
|
|
13181
|
+
"Suggest next content topics based on performance insights, past content, and competitor patterns. No AI call, no credit cost \u2014 purely data-driven recommendations.",
|
|
13182
|
+
{
|
|
13183
|
+
project_id: z22.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
|
|
13184
|
+
count: z22.number().min(1).max(10).default(3).describe("Number of suggestions to return"),
|
|
13185
|
+
response_format: z22.enum(["text", "json"]).optional()
|
|
13186
|
+
},
|
|
13187
|
+
async ({ project_id, count, response_format }) => {
|
|
13188
|
+
const format = response_format ?? "text";
|
|
13189
|
+
const startedAt = Date.now();
|
|
13190
|
+
try {
|
|
13191
|
+
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
13192
|
+
action: "suggest-content",
|
|
13193
|
+
projectId: project_id
|
|
13194
|
+
});
|
|
13195
|
+
if (efError) throw new Error(efError);
|
|
13196
|
+
const insights = result?.insights ?? [];
|
|
13197
|
+
const recentContent = result?.recentContent ?? [];
|
|
13198
|
+
const swipeItems = result?.swipeItems ?? [];
|
|
13199
|
+
const hookInsights = insights.filter(
|
|
13200
|
+
(i) => i.insight_type === "top_hooks" || i.insight_type === "winning_hooks"
|
|
13201
|
+
);
|
|
13202
|
+
const recentTopics = new Set(
|
|
13203
|
+
recentContent.map((c) => c.topic?.toLowerCase()).filter(Boolean)
|
|
13204
|
+
);
|
|
13205
|
+
const dataQuality = insights.length >= 10 ? "strong" : insights.length >= 3 ? "moderate" : "weak";
|
|
13206
|
+
const latestInsightDate = insights[0]?.generated_at ?? null;
|
|
13207
|
+
const suggestions = [];
|
|
13208
|
+
for (const insight of hookInsights.slice(0, Math.ceil(count / 2))) {
|
|
13209
|
+
const data = insight.insight_data;
|
|
13210
|
+
const hooks = Array.isArray(data.hooks) ? data.hooks : Array.isArray(data.top_hooks) ? data.top_hooks : [];
|
|
13211
|
+
for (const hook of hooks.slice(0, 2)) {
|
|
13212
|
+
const hookStr = typeof hook === "string" ? hook : String(hook.text ?? hook);
|
|
13213
|
+
if (suggestions.length >= count) break;
|
|
13214
|
+
suggestions.push({
|
|
13215
|
+
topic: `Content inspired by winning hook: "${hookStr.slice(0, 80)}"`,
|
|
13216
|
+
platform: String(data.platform ?? "tiktok"),
|
|
13217
|
+
content_type: "caption",
|
|
13218
|
+
rationale: "This hook pattern performed well in your past content.",
|
|
13219
|
+
confidence: insight.confidence_score ?? 0.7,
|
|
13220
|
+
based_on: ["performance_insights", "hook_analysis"],
|
|
13221
|
+
suggested_hook: hookStr.slice(0, 120),
|
|
13222
|
+
suggested_angle: "Apply this hook style to a fresh topic in your niche."
|
|
13223
|
+
});
|
|
13224
|
+
}
|
|
13225
|
+
}
|
|
13226
|
+
for (const swipe of swipeItems.slice(0, Math.ceil(count / 3))) {
|
|
13227
|
+
if (suggestions.length >= count) break;
|
|
13228
|
+
const title = swipe.title ?? "";
|
|
13229
|
+
if (recentTopics.has(title.toLowerCase())) continue;
|
|
13230
|
+
suggestions.push({
|
|
13231
|
+
topic: `Competitor-inspired: "${title.slice(0, 80)}"`,
|
|
13232
|
+
platform: swipe.platform ?? "instagram",
|
|
13233
|
+
content_type: "caption",
|
|
13234
|
+
rationale: `High-performing competitor content (score: ${swipe.engagement_score ?? "N/A"}).`,
|
|
13235
|
+
confidence: 0.6,
|
|
13236
|
+
based_on: ["niche_swipe_file", "competitor_analysis"],
|
|
13237
|
+
suggested_hook: swipe.hook ?? `Your take on: ${title.slice(0, 60)}`,
|
|
13238
|
+
suggested_angle: "Put your unique spin on this trending topic."
|
|
13239
|
+
});
|
|
13240
|
+
}
|
|
13241
|
+
if (suggestions.length < count) {
|
|
13242
|
+
const recentFormats = recentContent.map((c) => c.content_type).filter(Boolean);
|
|
13243
|
+
const formatCounts = {};
|
|
13244
|
+
for (const f of recentFormats) {
|
|
13245
|
+
formatCounts[f] = (formatCounts[f] ?? 0) + 1;
|
|
13246
|
+
}
|
|
13247
|
+
const allFormats = ["script", "caption", "blog", "hook"];
|
|
13248
|
+
const underusedFormats = allFormats.filter(
|
|
13249
|
+
(f) => (formatCounts[f] ?? 0) < recentFormats.length / allFormats.length * 0.5
|
|
13250
|
+
);
|
|
13251
|
+
for (const fmt of underusedFormats.slice(0, count - suggestions.length)) {
|
|
13252
|
+
suggestions.push({
|
|
13253
|
+
topic: `Try a ${fmt} format \u2014 you haven't used it recently`,
|
|
13254
|
+
platform: "linkedin",
|
|
13255
|
+
content_type: fmt,
|
|
13256
|
+
rationale: `You've posted ${formatCounts[fmt] ?? 0} ${fmt}(s) recently vs ${recentFormats.length} total posts. Diversifying formats can reach new audiences.`,
|
|
13257
|
+
confidence: 0.5,
|
|
13258
|
+
based_on: ["content_history", "format_analysis"],
|
|
13259
|
+
suggested_hook: `Experiment with ${fmt} content for your audience.`,
|
|
13260
|
+
suggested_angle: "Format diversification to increase reach."
|
|
13261
|
+
});
|
|
13262
|
+
}
|
|
13263
|
+
}
|
|
13264
|
+
if (suggestions.length < count) {
|
|
13265
|
+
suggestions.push({
|
|
13266
|
+
topic: "Share a behind-the-scenes look at your process",
|
|
13267
|
+
platform: "instagram",
|
|
13268
|
+
content_type: "caption",
|
|
13269
|
+
rationale: "Behind-the-scenes content consistently drives engagement across platforms.",
|
|
13270
|
+
confidence: 0.4,
|
|
13271
|
+
based_on: ["general_best_practices"],
|
|
13272
|
+
suggested_hook: "Here's what it actually takes to...",
|
|
13273
|
+
suggested_angle: "Authenticity and transparency."
|
|
13274
|
+
});
|
|
13275
|
+
}
|
|
13276
|
+
const durationMs = Date.now() - startedAt;
|
|
13277
|
+
logMcpToolInvocation({
|
|
13278
|
+
toolName: "suggest_next_content",
|
|
13279
|
+
status: "success",
|
|
13280
|
+
durationMs,
|
|
13281
|
+
details: {
|
|
13282
|
+
suggestions: suggestions.length,
|
|
13283
|
+
data_quality: dataQuality,
|
|
13284
|
+
insights_count: insights.length
|
|
13285
|
+
}
|
|
13286
|
+
});
|
|
13287
|
+
const resultPayload = {
|
|
13288
|
+
suggestions: suggestions.slice(0, count),
|
|
13289
|
+
data_quality: dataQuality,
|
|
13290
|
+
last_analysis_at: latestInsightDate
|
|
13291
|
+
};
|
|
13292
|
+
if (format === "json") {
|
|
13293
|
+
return {
|
|
13294
|
+
content: [
|
|
13295
|
+
{ type: "text", text: JSON.stringify(asEnvelope17(resultPayload), null, 2) }
|
|
13296
|
+
]
|
|
13297
|
+
};
|
|
13298
|
+
}
|
|
13299
|
+
const lines = [];
|
|
13300
|
+
lines.push(`Content Suggestions (${suggestions.length})`);
|
|
13301
|
+
lines.push(`Data Quality: ${dataQuality} | Last analysis: ${latestInsightDate ?? "never"}`);
|
|
13302
|
+
lines.push("=".repeat(40));
|
|
13303
|
+
for (let i = 0; i < suggestions.length; i++) {
|
|
13304
|
+
const s = suggestions[i];
|
|
13305
|
+
lines.push(`
|
|
13306
|
+
${i + 1}. ${s.topic}`);
|
|
13307
|
+
lines.push(` Platform: ${s.platform} | Type: ${s.content_type}`);
|
|
13308
|
+
lines.push(` Hook: "${s.suggested_hook}"`);
|
|
13309
|
+
lines.push(` Angle: ${s.suggested_angle}`);
|
|
13310
|
+
lines.push(` Rationale: ${s.rationale}`);
|
|
13311
|
+
lines.push(` Confidence: ${Math.round(s.confidence * 100)}%`);
|
|
13312
|
+
lines.push(` Based on: ${s.based_on.join(", ")}`);
|
|
13313
|
+
}
|
|
13314
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
13315
|
+
} catch (err) {
|
|
13316
|
+
const durationMs = Date.now() - startedAt;
|
|
13317
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
13318
|
+
logMcpToolInvocation({
|
|
13319
|
+
toolName: "suggest_next_content",
|
|
13320
|
+
status: "error",
|
|
13321
|
+
durationMs,
|
|
13322
|
+
details: { error: message }
|
|
13323
|
+
});
|
|
13324
|
+
return {
|
|
13325
|
+
content: [{ type: "text", text: `Suggestion failed: ${message}` }],
|
|
13326
|
+
isError: true
|
|
13327
|
+
};
|
|
13328
|
+
}
|
|
13329
|
+
}
|
|
13330
|
+
);
|
|
13331
|
+
}
|
|
13332
|
+
|
|
13333
|
+
// src/tools/digest.ts
|
|
13334
|
+
init_edge_function();
|
|
13335
|
+
init_supabase();
|
|
13336
|
+
import { z as z23 } from "zod";
|
|
13337
|
+
|
|
13338
|
+
// src/lib/anomaly-detector.ts
|
|
13339
|
+
var SENSITIVITY_THRESHOLDS = {
|
|
13340
|
+
low: 50,
|
|
13341
|
+
medium: 30,
|
|
13342
|
+
high: 15
|
|
13343
|
+
};
|
|
13344
|
+
var VIRAL_MULTIPLIER = 10;
|
|
13345
|
+
function aggregateByPlatform(data) {
|
|
13346
|
+
const map = /* @__PURE__ */ new Map();
|
|
13347
|
+
for (const d of data) {
|
|
13348
|
+
const existing = map.get(d.platform) ?? { views: 0, engagement: 0, posts: 0 };
|
|
13349
|
+
existing.views += d.views;
|
|
13350
|
+
existing.engagement += d.engagement;
|
|
13351
|
+
existing.posts += d.posts;
|
|
13352
|
+
map.set(d.platform, existing);
|
|
13353
|
+
}
|
|
13354
|
+
return map;
|
|
13355
|
+
}
|
|
13356
|
+
function pctChange(current, previous) {
|
|
13357
|
+
if (previous === 0) return current > 0 ? 100 : 0;
|
|
13358
|
+
return (current - previous) / previous * 100;
|
|
13359
|
+
}
|
|
13360
|
+
function detectAnomalies(currentData, previousData, sensitivity = "medium", averageViewsPerPost) {
|
|
13361
|
+
const threshold = SENSITIVITY_THRESHOLDS[sensitivity];
|
|
13362
|
+
const anomalies = [];
|
|
13363
|
+
const currentAgg = aggregateByPlatform(currentData);
|
|
13364
|
+
const previousAgg = aggregateByPlatform(previousData);
|
|
13365
|
+
const currentDates = currentData.map((d) => d.date).sort();
|
|
13366
|
+
const period = {
|
|
13367
|
+
current_start: currentDates[0] ?? "",
|
|
13368
|
+
current_end: currentDates[currentDates.length - 1] ?? ""
|
|
13369
|
+
};
|
|
13370
|
+
const allPlatforms = /* @__PURE__ */ new Set([...currentAgg.keys(), ...previousAgg.keys()]);
|
|
13371
|
+
for (const platform3 of allPlatforms) {
|
|
13372
|
+
const current = currentAgg.get(platform3) ?? { views: 0, engagement: 0, posts: 0 };
|
|
13373
|
+
const previous = previousAgg.get(platform3) ?? { views: 0, engagement: 0, posts: 0 };
|
|
13374
|
+
const viewsChange = pctChange(current.views, previous.views);
|
|
13375
|
+
if (Math.abs(viewsChange) >= threshold) {
|
|
13376
|
+
const isSpike = viewsChange > 0;
|
|
13377
|
+
anomalies.push({
|
|
13378
|
+
type: isSpike ? "spike" : "drop",
|
|
13379
|
+
metric: "views",
|
|
13380
|
+
platform: platform3,
|
|
13381
|
+
magnitude: Math.round(viewsChange * 10) / 10,
|
|
13382
|
+
period,
|
|
13383
|
+
affected_posts: [],
|
|
13384
|
+
confidence: Math.min(1, Math.abs(viewsChange) / 100),
|
|
13385
|
+
suggested_action: isSpike ? `Views up ${Math.abs(Math.round(viewsChange))}% on ${platform3}. Analyze what worked and double down.` : `Views down ${Math.abs(Math.round(viewsChange))}% on ${platform3}. Review content strategy and posting frequency.`
|
|
13386
|
+
});
|
|
13387
|
+
}
|
|
13388
|
+
const engagementChange = pctChange(current.engagement, previous.engagement);
|
|
13389
|
+
if (Math.abs(engagementChange) >= threshold) {
|
|
13390
|
+
const isSpike = engagementChange > 0;
|
|
13391
|
+
anomalies.push({
|
|
13392
|
+
type: isSpike ? "spike" : "drop",
|
|
13393
|
+
metric: "engagement",
|
|
13394
|
+
platform: platform3,
|
|
13395
|
+
magnitude: Math.round(engagementChange * 10) / 10,
|
|
13396
|
+
period,
|
|
13397
|
+
affected_posts: [],
|
|
13398
|
+
confidence: Math.min(1, Math.abs(engagementChange) / 100),
|
|
13399
|
+
suggested_action: isSpike ? `Engagement up ${Math.abs(Math.round(engagementChange))}% on ${platform3}. Replicate this content style.` : `Engagement down ${Math.abs(Math.round(engagementChange))}% on ${platform3}. Test different hooks and CTAs.`
|
|
13400
|
+
});
|
|
13401
|
+
}
|
|
13402
|
+
const avgViews = averageViewsPerPost ?? (previous.posts > 0 ? previous.views / previous.posts : 0);
|
|
13403
|
+
if (avgViews > 0 && current.posts > 0) {
|
|
13404
|
+
const currentAvgViews = current.views / current.posts;
|
|
13405
|
+
if (currentAvgViews > avgViews * VIRAL_MULTIPLIER) {
|
|
13406
|
+
anomalies.push({
|
|
13407
|
+
type: "viral",
|
|
13408
|
+
metric: "views",
|
|
13409
|
+
platform: platform3,
|
|
13410
|
+
magnitude: Math.round(currentAvgViews / avgViews * 100) / 100,
|
|
13411
|
+
period,
|
|
13412
|
+
affected_posts: [],
|
|
13413
|
+
confidence: 0.9,
|
|
13414
|
+
suggested_action: `Viral content detected on ${platform3}! Average views per post is ${Math.round(currentAvgViews / avgViews)}x normal. Engage with comments and create follow-up content.`
|
|
13415
|
+
});
|
|
13416
|
+
}
|
|
13417
|
+
}
|
|
13418
|
+
const prevEngRate = previous.views > 0 ? previous.engagement / previous.views : 0;
|
|
13419
|
+
const currEngRate = current.views > 0 ? current.engagement / current.views : 0;
|
|
13420
|
+
const rateChange = pctChange(currEngRate, prevEngRate);
|
|
13421
|
+
if (Math.abs(rateChange) >= threshold && current.posts >= 2 && previous.posts >= 2) {
|
|
13422
|
+
anomalies.push({
|
|
13423
|
+
type: "trend_shift",
|
|
13424
|
+
metric: "engagement_rate",
|
|
13425
|
+
platform: platform3,
|
|
13426
|
+
magnitude: Math.round(rateChange * 10) / 10,
|
|
13427
|
+
period,
|
|
13428
|
+
affected_posts: [],
|
|
13429
|
+
confidence: Math.min(1, Math.min(current.posts, previous.posts) / 5),
|
|
13430
|
+
suggested_action: rateChange > 0 ? `Engagement rate improving on ${platform3}. Current audience is more responsive.` : `Engagement rate declining on ${platform3} despite views. Content may not be resonating \u2014 test new formats.`
|
|
13431
|
+
});
|
|
13432
|
+
}
|
|
13433
|
+
}
|
|
13434
|
+
anomalies.sort((a, b) => Math.abs(b.magnitude) - Math.abs(a.magnitude));
|
|
13435
|
+
return anomalies;
|
|
13436
|
+
}
|
|
13437
|
+
|
|
13438
|
+
// src/tools/digest.ts
|
|
13439
|
+
init_version();
|
|
13440
|
+
function asEnvelope18(data) {
|
|
13441
|
+
return { _meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, data };
|
|
13442
|
+
}
|
|
13443
|
+
var PLATFORM_ENUM3 = z23.enum([
|
|
13444
|
+
"youtube",
|
|
13445
|
+
"tiktok",
|
|
13446
|
+
"instagram",
|
|
13447
|
+
"twitter",
|
|
13448
|
+
"linkedin",
|
|
13449
|
+
"facebook",
|
|
13450
|
+
"threads",
|
|
13451
|
+
"bluesky"
|
|
13452
|
+
]);
|
|
13453
|
+
function registerDigestTools(server2) {
|
|
13454
|
+
server2.tool(
|
|
13455
|
+
"generate_performance_digest",
|
|
13456
|
+
"Generate a performance summary for a time period. Includes metrics, trends vs previous period, top/bottom performers, platform breakdown, and actionable recommendations. No AI call, no credit cost.",
|
|
13457
|
+
{
|
|
13458
|
+
project_id: z23.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
|
|
13459
|
+
period: z23.enum(["7d", "14d", "30d"]).default("7d").describe("Time period to analyze"),
|
|
13460
|
+
include_recommendations: z23.boolean().default(true),
|
|
13461
|
+
response_format: z23.enum(["text", "json"]).optional()
|
|
13462
|
+
},
|
|
13463
|
+
async ({ project_id, period, include_recommendations, response_format }) => {
|
|
13464
|
+
const format = response_format ?? "text";
|
|
13465
|
+
const startedAt = Date.now();
|
|
13466
|
+
try {
|
|
13467
|
+
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
13468
|
+
action: "performance-digest",
|
|
13469
|
+
period,
|
|
13470
|
+
projectId: project_id
|
|
13471
|
+
});
|
|
13472
|
+
if (efError) throw new Error(efError);
|
|
13473
|
+
const currentData = result?.currentData ?? [];
|
|
13474
|
+
const previousData = result?.previousData ?? [];
|
|
13475
|
+
const totalViews = currentData.reduce((sum, d) => sum + (d.views ?? 0), 0);
|
|
13476
|
+
const totalEngagement = currentData.reduce((sum, d) => sum + (d.engagement ?? 0), 0);
|
|
13477
|
+
const postIds = new Set(currentData.map((d) => d.post_id));
|
|
13478
|
+
const totalPosts = postIds.size;
|
|
13479
|
+
const avgEngRate = totalViews > 0 ? totalEngagement / totalViews * 100 : 0;
|
|
13480
|
+
const postMetrics = /* @__PURE__ */ new Map();
|
|
13481
|
+
for (const d of currentData) {
|
|
13482
|
+
const existing = postMetrics.get(d.post_id) ?? {
|
|
13483
|
+
views: 0,
|
|
13484
|
+
engagement: 0,
|
|
13485
|
+
platform: d.platform
|
|
13486
|
+
};
|
|
13487
|
+
existing.views += d.views ?? 0;
|
|
13488
|
+
existing.engagement += d.engagement ?? 0;
|
|
13489
|
+
postMetrics.set(d.post_id, existing);
|
|
13490
|
+
}
|
|
13491
|
+
let best = null;
|
|
13492
|
+
let worst = null;
|
|
13493
|
+
for (const [id, metrics] of postMetrics) {
|
|
13494
|
+
if (!best || metrics.views > best.views) {
|
|
13495
|
+
best = {
|
|
13496
|
+
id,
|
|
13497
|
+
platform: metrics.platform,
|
|
13498
|
+
title: null,
|
|
13499
|
+
views: metrics.views,
|
|
13500
|
+
engagement: metrics.engagement
|
|
13501
|
+
};
|
|
13502
|
+
}
|
|
13503
|
+
if (!worst || metrics.views < worst.views) {
|
|
13504
|
+
worst = {
|
|
13505
|
+
id,
|
|
13506
|
+
platform: metrics.platform,
|
|
13507
|
+
title: null,
|
|
13508
|
+
views: metrics.views,
|
|
13509
|
+
engagement: metrics.engagement
|
|
13510
|
+
};
|
|
13511
|
+
}
|
|
13512
|
+
}
|
|
13513
|
+
const platformMap = /* @__PURE__ */ new Map();
|
|
13514
|
+
for (const d of currentData) {
|
|
13515
|
+
const existing = platformMap.get(d.platform) ?? { posts: 0, views: 0, engagement: 0 };
|
|
13516
|
+
existing.views += d.views ?? 0;
|
|
13517
|
+
existing.engagement += d.engagement ?? 0;
|
|
13518
|
+
platformMap.set(d.platform, existing);
|
|
13519
|
+
}
|
|
13520
|
+
const platformPosts = /* @__PURE__ */ new Map();
|
|
13521
|
+
for (const d of currentData) {
|
|
13522
|
+
if (!platformPosts.has(d.platform)) platformPosts.set(d.platform, /* @__PURE__ */ new Set());
|
|
13523
|
+
platformPosts.get(d.platform).add(d.post_id);
|
|
13524
|
+
}
|
|
13525
|
+
for (const [platform3, postSet] of platformPosts) {
|
|
13526
|
+
const existing = platformMap.get(platform3);
|
|
13527
|
+
if (existing) existing.posts = postSet.size;
|
|
13528
|
+
}
|
|
13529
|
+
const platformBreakdown = [...platformMap.entries()].map(([platform3, m]) => ({
|
|
13530
|
+
platform: platform3,
|
|
13531
|
+
...m
|
|
13532
|
+
}));
|
|
13533
|
+
const periodMap = { "7d": 7, "14d": 14, "30d": 30 };
|
|
13534
|
+
const periodDays = periodMap[period] ?? 7;
|
|
13535
|
+
const now = /* @__PURE__ */ new Date();
|
|
13536
|
+
const currentStart = new Date(now);
|
|
13537
|
+
currentStart.setDate(currentStart.getDate() - periodDays);
|
|
13538
|
+
const prevViews = previousData.reduce((sum, d) => sum + (d.views ?? 0), 0);
|
|
13539
|
+
const prevEngagement = previousData.reduce((sum, d) => sum + (d.engagement ?? 0), 0);
|
|
13540
|
+
const viewsChangePct = prevViews > 0 ? (totalViews - prevViews) / prevViews * 100 : totalViews > 0 ? 100 : 0;
|
|
13541
|
+
const engChangePct = prevEngagement > 0 ? (totalEngagement - prevEngagement) / prevEngagement * 100 : totalEngagement > 0 ? 100 : 0;
|
|
13542
|
+
const recommendations = [];
|
|
13543
|
+
if (include_recommendations) {
|
|
13544
|
+
if (viewsChangePct < -10) {
|
|
13545
|
+
recommendations.push("Views declining \u2014 experiment with new hooks and posting times.");
|
|
13546
|
+
}
|
|
13547
|
+
if (avgEngRate < 2) {
|
|
13548
|
+
recommendations.push(
|
|
13549
|
+
"Engagement rate below 2% \u2014 try more interactive content (questions, polls, CTAs)."
|
|
13550
|
+
);
|
|
13551
|
+
}
|
|
13552
|
+
if (totalPosts < periodDays / 2) {
|
|
13553
|
+
recommendations.push(
|
|
13554
|
+
`Only ${totalPosts} posts in ${periodDays} days \u2014 increase posting frequency.`
|
|
13555
|
+
);
|
|
13556
|
+
}
|
|
13557
|
+
if (platformBreakdown.length === 1) {
|
|
13558
|
+
recommendations.push(
|
|
13559
|
+
"Only posting on one platform \u2014 diversify to reach new audiences."
|
|
13560
|
+
);
|
|
13561
|
+
}
|
|
13562
|
+
if (viewsChangePct > 20) {
|
|
13563
|
+
recommendations.push(
|
|
13564
|
+
"Views growing well! Analyze top performers and replicate those patterns."
|
|
13565
|
+
);
|
|
13566
|
+
}
|
|
13567
|
+
if (engChangePct > 20 && viewsChangePct > 0) {
|
|
13568
|
+
recommendations.push(
|
|
13569
|
+
"Both views and engagement growing \u2014 current strategy is working."
|
|
13570
|
+
);
|
|
13571
|
+
}
|
|
13572
|
+
if (recommendations.length === 0) {
|
|
13573
|
+
recommendations.push(
|
|
13574
|
+
"Performance is stable. Continue current strategy and monitor weekly."
|
|
13575
|
+
);
|
|
13576
|
+
}
|
|
13577
|
+
}
|
|
13578
|
+
const digest = {
|
|
13579
|
+
period,
|
|
13580
|
+
period_start: currentStart.toISOString().split("T")[0],
|
|
13581
|
+
period_end: now.toISOString().split("T")[0],
|
|
13582
|
+
metrics: {
|
|
13583
|
+
total_posts: totalPosts,
|
|
13584
|
+
total_views: totalViews,
|
|
13585
|
+
total_engagement: totalEngagement,
|
|
13586
|
+
avg_engagement_rate: Math.round(avgEngRate * 100) / 100,
|
|
13587
|
+
best_performing: best,
|
|
13588
|
+
worst_performing: worst,
|
|
13589
|
+
platform_breakdown: platformBreakdown
|
|
13590
|
+
},
|
|
13591
|
+
trends: {
|
|
13592
|
+
views: {
|
|
13593
|
+
direction: viewsChangePct > 5 ? "up" : viewsChangePct < -5 ? "down" : "flat",
|
|
13594
|
+
change_pct: Math.round(viewsChangePct * 10) / 10
|
|
13595
|
+
},
|
|
13596
|
+
engagement: {
|
|
13597
|
+
direction: engChangePct > 5 ? "up" : engChangePct < -5 ? "down" : "flat",
|
|
13598
|
+
change_pct: Math.round(engChangePct * 10) / 10
|
|
13599
|
+
}
|
|
13600
|
+
},
|
|
13601
|
+
recommendations,
|
|
13602
|
+
winning_patterns: {
|
|
13603
|
+
hook_types: [],
|
|
13604
|
+
content_formats: [],
|
|
13605
|
+
posting_times: []
|
|
13606
|
+
}
|
|
13607
|
+
};
|
|
13608
|
+
const durationMs = Date.now() - startedAt;
|
|
13609
|
+
logMcpToolInvocation({
|
|
13610
|
+
toolName: "generate_performance_digest",
|
|
13611
|
+
status: "success",
|
|
13612
|
+
durationMs,
|
|
13613
|
+
details: { period, posts: totalPosts, views: totalViews }
|
|
13614
|
+
});
|
|
13615
|
+
if (format === "json") {
|
|
13616
|
+
return {
|
|
13617
|
+
content: [{ type: "text", text: JSON.stringify(asEnvelope18(digest), null, 2) }]
|
|
13618
|
+
};
|
|
13619
|
+
}
|
|
13620
|
+
const lines = [];
|
|
13621
|
+
lines.push(`Performance Digest (${period})`);
|
|
13622
|
+
lines.push(`Period: ${digest.period_start} to ${digest.period_end}`);
|
|
13623
|
+
lines.push("=".repeat(40));
|
|
13624
|
+
lines.push(`Posts: ${totalPosts}`);
|
|
13625
|
+
lines.push(
|
|
13626
|
+
`Views: ${totalViews.toLocaleString()} (${viewsChangePct >= 0 ? "+" : ""}${Math.round(viewsChangePct)}% vs prev period)`
|
|
13627
|
+
);
|
|
13628
|
+
lines.push(
|
|
13629
|
+
`Engagement: ${totalEngagement.toLocaleString()} (${engChangePct >= 0 ? "+" : ""}${Math.round(engChangePct)}% vs prev period)`
|
|
13630
|
+
);
|
|
13631
|
+
lines.push(`Avg Engagement Rate: ${digest.metrics.avg_engagement_rate}%`);
|
|
13632
|
+
if (best) {
|
|
13633
|
+
lines.push(
|
|
13634
|
+
`
|
|
13635
|
+
Best: ${best.id.slice(0, 8)}... (${best.platform}) \u2014 ${best.views.toLocaleString()} views`
|
|
13636
|
+
);
|
|
13637
|
+
}
|
|
13638
|
+
if (worst && totalPosts > 1) {
|
|
13639
|
+
lines.push(
|
|
13640
|
+
`Worst: ${worst.id.slice(0, 8)}... (${worst.platform}) \u2014 ${worst.views.toLocaleString()} views`
|
|
13641
|
+
);
|
|
13642
|
+
}
|
|
13643
|
+
if (platformBreakdown.length > 0) {
|
|
13644
|
+
lines.push("\nPlatform Breakdown:");
|
|
13645
|
+
for (const p of platformBreakdown) {
|
|
13646
|
+
lines.push(
|
|
13647
|
+
` ${p.platform}: ${p.posts} posts, ${p.views.toLocaleString()} views, ${p.engagement.toLocaleString()} engagement`
|
|
13648
|
+
);
|
|
13649
|
+
}
|
|
13650
|
+
}
|
|
13651
|
+
if (recommendations.length > 0) {
|
|
13652
|
+
lines.push("\nRecommendations:");
|
|
13653
|
+
for (const r of recommendations) lines.push(` \u2022 ${r}`);
|
|
13654
|
+
}
|
|
13655
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
13656
|
+
} catch (err) {
|
|
13657
|
+
const durationMs = Date.now() - startedAt;
|
|
13658
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
13659
|
+
logMcpToolInvocation({
|
|
13660
|
+
toolName: "generate_performance_digest",
|
|
13661
|
+
status: "error",
|
|
13662
|
+
durationMs,
|
|
13663
|
+
details: { error: message }
|
|
13664
|
+
});
|
|
13665
|
+
return {
|
|
13666
|
+
content: [{ type: "text", text: `Digest failed: ${message}` }],
|
|
13667
|
+
isError: true
|
|
13668
|
+
};
|
|
13669
|
+
}
|
|
13670
|
+
}
|
|
13671
|
+
);
|
|
13672
|
+
server2.tool(
|
|
13673
|
+
"detect_anomalies",
|
|
13674
|
+
"Detect significant performance changes: spikes, drops, viral content, trend shifts. Compares current period against previous equal-length period. No AI call, no credit cost.",
|
|
13675
|
+
{
|
|
13676
|
+
project_id: z23.string().uuid().optional().describe("Project ID (auto-detected if omitted)"),
|
|
13677
|
+
days: z23.number().min(7).max(90).default(14).describe("Days to analyze"),
|
|
13678
|
+
sensitivity: z23.enum(["low", "medium", "high"]).default("medium").describe("Detection sensitivity: low=50%+, medium=30%+, high=15%+ changes"),
|
|
13679
|
+
platforms: z23.array(PLATFORM_ENUM3).optional().describe("Filter to specific platforms"),
|
|
13680
|
+
response_format: z23.enum(["text", "json"]).optional()
|
|
13681
|
+
},
|
|
13682
|
+
async ({ project_id, days, sensitivity, platforms, response_format }) => {
|
|
13683
|
+
const format = response_format ?? "text";
|
|
13684
|
+
const startedAt = Date.now();
|
|
13685
|
+
try {
|
|
13686
|
+
const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
|
|
13687
|
+
action: "detect-anomalies",
|
|
13688
|
+
days,
|
|
13689
|
+
platforms,
|
|
13690
|
+
projectId: project_id
|
|
13691
|
+
});
|
|
13692
|
+
if (efError) throw new Error(efError);
|
|
13693
|
+
const toMetricData = (data) => {
|
|
13694
|
+
const dayMap = /* @__PURE__ */ new Map();
|
|
13695
|
+
for (const d of data) {
|
|
13696
|
+
const date = d.captured_at.split("T")[0];
|
|
13697
|
+
const key = `${date}-${d.platform}`;
|
|
13698
|
+
const existing = dayMap.get(key) ?? {
|
|
13699
|
+
date,
|
|
13700
|
+
platform: d.platform,
|
|
13701
|
+
views: 0,
|
|
13702
|
+
engagement: 0,
|
|
13703
|
+
posts: 0
|
|
13704
|
+
};
|
|
13705
|
+
existing.views += d.views ?? 0;
|
|
13706
|
+
existing.engagement += d.engagement ?? 0;
|
|
13707
|
+
existing.posts += 1;
|
|
13708
|
+
dayMap.set(key, existing);
|
|
13709
|
+
}
|
|
13710
|
+
return [...dayMap.values()];
|
|
13711
|
+
};
|
|
13712
|
+
const currentMetrics = toMetricData(result?.currentData ?? []);
|
|
13713
|
+
const previousMetrics = toMetricData(result?.previousData ?? []);
|
|
13714
|
+
const anomalies = detectAnomalies(
|
|
13715
|
+
currentMetrics,
|
|
13716
|
+
previousMetrics,
|
|
13717
|
+
sensitivity
|
|
13718
|
+
);
|
|
13719
|
+
const durationMs = Date.now() - startedAt;
|
|
13720
|
+
logMcpToolInvocation({
|
|
13721
|
+
toolName: "detect_anomalies",
|
|
13722
|
+
status: "success",
|
|
13723
|
+
durationMs,
|
|
13724
|
+
details: { days, sensitivity, anomalies_found: anomalies.length }
|
|
13725
|
+
});
|
|
13726
|
+
const summary = anomalies.length === 0 ? `No significant anomalies detected in the last ${days} days.` : `Found ${anomalies.length} anomal${anomalies.length === 1 ? "y" : "ies"} in the last ${days} days.`;
|
|
13727
|
+
const resultPayload = { anomalies, summary };
|
|
13728
|
+
if (format === "json") {
|
|
13729
|
+
return {
|
|
13730
|
+
content: [
|
|
13731
|
+
{ type: "text", text: JSON.stringify(asEnvelope18(resultPayload), null, 2) }
|
|
13732
|
+
]
|
|
13733
|
+
};
|
|
13734
|
+
}
|
|
13735
|
+
const lines = [];
|
|
13736
|
+
lines.push(`Anomaly Detection (${days} days, ${sensitivity} sensitivity)`);
|
|
13737
|
+
lines.push("=".repeat(40));
|
|
13738
|
+
lines.push(summary);
|
|
13739
|
+
if (anomalies.length > 0) {
|
|
13740
|
+
lines.push("");
|
|
13741
|
+
for (let i = 0; i < anomalies.length; i++) {
|
|
13742
|
+
const a = anomalies[i];
|
|
13743
|
+
const arrow = a.magnitude > 0 ? "\u2191" : "\u2193";
|
|
13744
|
+
const magnitudeStr = a.type === "viral" ? `${a.magnitude}x average` : `${Math.abs(a.magnitude)}% change`;
|
|
13745
|
+
lines.push(`${i + 1}. [${a.type.toUpperCase()}] ${a.metric} on ${a.platform}`);
|
|
13746
|
+
lines.push(
|
|
13747
|
+
` ${arrow} ${magnitudeStr} | Confidence: ${Math.round(a.confidence * 100)}%`
|
|
13748
|
+
);
|
|
13749
|
+
lines.push(` \u2192 ${a.suggested_action}`);
|
|
13750
|
+
}
|
|
13751
|
+
}
|
|
13752
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
13753
|
+
} catch (err) {
|
|
13754
|
+
const durationMs = Date.now() - startedAt;
|
|
13755
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
13756
|
+
logMcpToolInvocation({
|
|
13757
|
+
toolName: "detect_anomalies",
|
|
13758
|
+
status: "error",
|
|
13759
|
+
durationMs,
|
|
13760
|
+
details: { error: message }
|
|
13761
|
+
});
|
|
13762
|
+
return {
|
|
13763
|
+
content: [{ type: "text", text: `Anomaly detection failed: ${message}` }],
|
|
13764
|
+
isError: true
|
|
13765
|
+
};
|
|
13766
|
+
}
|
|
13767
|
+
}
|
|
13768
|
+
);
|
|
13769
|
+
}
|
|
13770
|
+
|
|
13771
|
+
// src/tools/brandRuntime.ts
|
|
13772
|
+
init_edge_function();
|
|
13773
|
+
init_supabase();
|
|
13774
|
+
init_version();
|
|
13775
|
+
import { z as z24 } from "zod";
|
|
13776
|
+
function asEnvelope19(data) {
|
|
13777
|
+
return {
|
|
13778
|
+
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
13779
|
+
data
|
|
13780
|
+
};
|
|
13781
|
+
}
|
|
13782
|
+
function registerBrandRuntimeTools(server2) {
|
|
13783
|
+
server2.tool(
|
|
13784
|
+
"get_brand_runtime",
|
|
13785
|
+
"Get the full brand runtime for a project. Returns the 4-layer brand system: messaging (value props, pillars, proof points), voice (tone, vocabulary, avoid patterns), visual (palette, typography, composition), and operating constraints (audience, archetype). Also returns extraction confidence metadata.",
|
|
13786
|
+
{
|
|
13787
|
+
project_id: z24.string().optional().describe("Project ID. Defaults to current project.")
|
|
13788
|
+
},
|
|
13789
|
+
async ({ project_id }) => {
|
|
13790
|
+
const projectId = project_id || await getDefaultProjectId();
|
|
13791
|
+
const { data: result, error: efError } = await callEdgeFunction("mcp-data", { action: "brand-profile", projectId });
|
|
13792
|
+
if (efError || !result?.success) {
|
|
13793
|
+
return {
|
|
13794
|
+
content: [
|
|
13795
|
+
{
|
|
13796
|
+
type: "text",
|
|
13797
|
+
text: `Error: ${efError || result?.error || "Failed to fetch brand profile"}`
|
|
13798
|
+
}
|
|
13799
|
+
],
|
|
13800
|
+
isError: true
|
|
13801
|
+
};
|
|
13802
|
+
}
|
|
13803
|
+
const data = result.profile;
|
|
13804
|
+
if (!data?.profile_data) {
|
|
13805
|
+
return {
|
|
13806
|
+
content: [
|
|
13807
|
+
{
|
|
13808
|
+
type: "text",
|
|
13809
|
+
text: "No brand profile found for this project. Use extract_brand to create one."
|
|
13810
|
+
}
|
|
13811
|
+
]
|
|
13812
|
+
};
|
|
13813
|
+
}
|
|
13814
|
+
const profile = data.profile_data;
|
|
13815
|
+
const meta = data.extraction_metadata || {};
|
|
13816
|
+
const runtime = {
|
|
13817
|
+
name: profile.name || "",
|
|
13818
|
+
industry: profile.industryClassification || "",
|
|
13819
|
+
positioning: profile.competitivePositioning || "",
|
|
13820
|
+
messaging: {
|
|
13821
|
+
valuePropositions: profile.valuePropositions || [],
|
|
13822
|
+
messagingPillars: profile.messagingPillars || [],
|
|
13823
|
+
contentPillars: (profile.contentPillars || []).map(
|
|
13824
|
+
(p) => `${p.name} (${Math.round(p.weight * 100)}%)`
|
|
13825
|
+
),
|
|
13826
|
+
socialProof: profile.socialProof || { testimonials: [], awards: [], pressMentions: [] }
|
|
13827
|
+
},
|
|
13828
|
+
voice: {
|
|
13829
|
+
tone: profile.voiceProfile?.tone || [],
|
|
13830
|
+
style: profile.voiceProfile?.style || [],
|
|
13831
|
+
avoidPatterns: profile.voiceProfile?.avoidPatterns || [],
|
|
13832
|
+
preferredTerms: profile.vocabularyRules?.preferredTerms || [],
|
|
13833
|
+
bannedTerms: profile.vocabularyRules?.bannedTerms || []
|
|
13834
|
+
},
|
|
13835
|
+
visual: {
|
|
13836
|
+
colorPalette: profile.colorPalette || {},
|
|
13837
|
+
logoUrl: profile.logoUrl || null,
|
|
13838
|
+
referenceFrameUrl: data.default_style_ref_url || null
|
|
13839
|
+
},
|
|
13840
|
+
audience: profile.targetAudience || {},
|
|
13841
|
+
confidence: {
|
|
13842
|
+
overall: meta.overallConfidence || 0,
|
|
13843
|
+
provider: meta.scrapingProvider || "unknown",
|
|
13844
|
+
pagesScraped: meta.pagesScraped || 0
|
|
13845
|
+
}
|
|
13846
|
+
};
|
|
13847
|
+
const envelope = asEnvelope19(runtime);
|
|
13848
|
+
return {
|
|
13849
|
+
content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
|
|
13850
|
+
};
|
|
13851
|
+
}
|
|
13852
|
+
);
|
|
13853
|
+
server2.tool(
|
|
13854
|
+
"explain_brand_system",
|
|
13855
|
+
"Explains what brand data is available vs missing for a project. Returns a human-readable summary of completeness, confidence levels, and recommendations for improving the brand profile.",
|
|
13856
|
+
{
|
|
13857
|
+
project_id: z24.string().optional().describe("Project ID. Defaults to current project.")
|
|
13858
|
+
},
|
|
13859
|
+
async ({ project_id }) => {
|
|
13860
|
+
const projectId = project_id || await getDefaultProjectId();
|
|
13861
|
+
const { data: result, error: efError } = await callEdgeFunction("mcp-data", { action: "brand-profile", projectId });
|
|
13862
|
+
if (efError || !result?.success) {
|
|
13863
|
+
return {
|
|
13864
|
+
content: [
|
|
13865
|
+
{ type: "text", text: "No brand profile found. Run extract_brand first." }
|
|
13866
|
+
]
|
|
13867
|
+
};
|
|
13868
|
+
}
|
|
13869
|
+
const row = result.profile;
|
|
13870
|
+
if (!row?.profile_data) {
|
|
13871
|
+
return {
|
|
13872
|
+
content: [
|
|
13873
|
+
{ type: "text", text: "No brand profile found. Run extract_brand first." }
|
|
13874
|
+
]
|
|
13875
|
+
};
|
|
13876
|
+
}
|
|
13877
|
+
const p = row.profile_data;
|
|
13878
|
+
const meta = row.extraction_metadata || {};
|
|
13879
|
+
const sections = [
|
|
13880
|
+
{
|
|
13881
|
+
name: "Identity",
|
|
13882
|
+
fields: [p.name, p.tagline, p.industryClassification, p.competitivePositioning],
|
|
13883
|
+
total: 4
|
|
13884
|
+
},
|
|
13885
|
+
{
|
|
13886
|
+
name: "Voice",
|
|
13887
|
+
fields: [
|
|
13888
|
+
p.voiceProfile?.tone?.length,
|
|
13889
|
+
p.voiceProfile?.style?.length,
|
|
13890
|
+
p.voiceProfile?.avoidPatterns?.length
|
|
13891
|
+
],
|
|
13892
|
+
total: 3
|
|
13893
|
+
},
|
|
13894
|
+
{
|
|
13895
|
+
name: "Audience",
|
|
13896
|
+
fields: [
|
|
13897
|
+
p.targetAudience?.demographics?.ageRange,
|
|
13898
|
+
p.targetAudience?.psychographics?.painPoints?.length
|
|
13899
|
+
],
|
|
13900
|
+
total: 2
|
|
13901
|
+
},
|
|
13902
|
+
{
|
|
13903
|
+
name: "Messaging",
|
|
13904
|
+
fields: [
|
|
13905
|
+
p.valuePropositions?.length,
|
|
13906
|
+
p.messagingPillars?.length,
|
|
13907
|
+
p.contentPillars?.length
|
|
13908
|
+
],
|
|
13909
|
+
total: 3
|
|
13910
|
+
},
|
|
13911
|
+
{
|
|
13912
|
+
name: "Visual",
|
|
13913
|
+
fields: [
|
|
13914
|
+
p.logoUrl,
|
|
13915
|
+
p.colorPalette?.primary !== "#000000" ? p.colorPalette?.primary : null,
|
|
13916
|
+
p.typography
|
|
13917
|
+
],
|
|
13918
|
+
total: 3
|
|
13919
|
+
},
|
|
13920
|
+
{
|
|
13921
|
+
name: "Vocabulary",
|
|
13922
|
+
fields: [
|
|
13923
|
+
p.vocabularyRules?.preferredTerms?.length,
|
|
13924
|
+
p.vocabularyRules?.bannedTerms?.length
|
|
13925
|
+
],
|
|
13926
|
+
total: 2
|
|
13927
|
+
},
|
|
13928
|
+
{
|
|
13929
|
+
name: "Video Rules",
|
|
13930
|
+
fields: [p.videoBrandRules?.pacing, p.videoBrandRules?.colorGrading],
|
|
13931
|
+
total: 2
|
|
13932
|
+
}
|
|
13933
|
+
];
|
|
13934
|
+
const lines = [`Brand System Report: ${p.name || "Unknown"}`, ""];
|
|
13935
|
+
for (const section of sections) {
|
|
13936
|
+
const filled = section.fields.filter((f) => f != null && f !== "" && f !== 0).length;
|
|
13937
|
+
const pct = Math.round(filled / section.total * 100);
|
|
13938
|
+
const icon = pct >= 80 ? "OK" : pct >= 50 ? "PARTIAL" : "MISSING";
|
|
13939
|
+
lines.push(`[${icon}] ${section.name}: ${filled}/${section.total} (${pct}%)`);
|
|
13940
|
+
}
|
|
13941
|
+
lines.push("");
|
|
13942
|
+
lines.push(`Extraction confidence: ${Math.round((meta.overallConfidence || 0) * 100)}%`);
|
|
13943
|
+
lines.push(
|
|
13944
|
+
`Scraping: ${meta.pagesScraped || 0} pages via ${meta.scrapingProvider || "unknown"}`
|
|
13945
|
+
);
|
|
13946
|
+
const recs = [];
|
|
13947
|
+
if (!p.contentPillars?.length) recs.push("Add content pillars for focused ideation");
|
|
13948
|
+
if (!p.vocabularyRules?.preferredTerms?.length)
|
|
13949
|
+
recs.push("Add preferred terms for vocabulary consistency");
|
|
13950
|
+
if (!p.videoBrandRules?.pacing)
|
|
13951
|
+
recs.push("Add video brand rules (pacing, color grading) for storyboard consistency");
|
|
13952
|
+
if (!p.logoUrl) recs.push("Upload a logo for deterministic brand overlay");
|
|
13953
|
+
if ((meta.overallConfidence || 0) < 0.6)
|
|
13954
|
+
recs.push("Re-extract with premium mode for higher confidence");
|
|
13955
|
+
if (recs.length > 0) {
|
|
13956
|
+
lines.push("");
|
|
13957
|
+
lines.push("Recommendations:");
|
|
13958
|
+
recs.forEach((r) => lines.push(` - ${r}`));
|
|
13959
|
+
}
|
|
13960
|
+
return {
|
|
13961
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
13962
|
+
};
|
|
13963
|
+
}
|
|
13964
|
+
);
|
|
13965
|
+
server2.tool(
|
|
13966
|
+
"check_brand_consistency",
|
|
13967
|
+
"Check if content text is consistent with the brand voice, vocabulary, messaging, and factual claims. Returns per-dimension scores (0-100) and specific issues found. Use this to validate scripts, captions, or post copy before publishing.",
|
|
13968
|
+
{
|
|
13969
|
+
content: z24.string().describe("The content text to check for brand consistency."),
|
|
13970
|
+
project_id: z24.string().optional().describe("Project ID. Defaults to current project.")
|
|
13971
|
+
},
|
|
13972
|
+
async ({ content, project_id }) => {
|
|
13973
|
+
const projectId = project_id || await getDefaultProjectId();
|
|
13974
|
+
const { data: result, error: efError } = await callEdgeFunction("mcp-data", { action: "brand-profile", projectId });
|
|
13975
|
+
const row = !efError && result?.success ? result.profile : null;
|
|
13976
|
+
if (!row?.profile_data) {
|
|
13977
|
+
return {
|
|
13978
|
+
content: [
|
|
13979
|
+
{
|
|
13980
|
+
type: "text",
|
|
13981
|
+
text: "No brand profile found. Cannot check consistency without brand data."
|
|
13982
|
+
}
|
|
13983
|
+
],
|
|
13984
|
+
isError: true
|
|
13985
|
+
};
|
|
13986
|
+
}
|
|
13987
|
+
const profile = row.profile_data;
|
|
13988
|
+
const contentLower = content.toLowerCase();
|
|
13989
|
+
const issues = [];
|
|
13990
|
+
let score = 70;
|
|
13991
|
+
const banned = profile.vocabularyRules?.bannedTerms || [];
|
|
13992
|
+
const bannedFound = banned.filter((t) => contentLower.includes(t.toLowerCase()));
|
|
13993
|
+
if (bannedFound.length > 0) {
|
|
13994
|
+
score -= bannedFound.length * 15;
|
|
13995
|
+
issues.push(`Banned terms found: ${bannedFound.join(", ")}`);
|
|
13996
|
+
}
|
|
13997
|
+
const avoid = profile.voiceProfile?.avoidPatterns || [];
|
|
13998
|
+
const avoidFound = avoid.filter((p) => contentLower.includes(p.toLowerCase()));
|
|
13999
|
+
if (avoidFound.length > 0) {
|
|
14000
|
+
score -= avoidFound.length * 10;
|
|
14001
|
+
issues.push(`Avoid patterns found: ${avoidFound.join(", ")}`);
|
|
14002
|
+
}
|
|
14003
|
+
const preferred = profile.vocabularyRules?.preferredTerms || [];
|
|
14004
|
+
const prefUsed = preferred.filter((t) => contentLower.includes(t.toLowerCase()));
|
|
14005
|
+
score += Math.min(15, prefUsed.length * 5);
|
|
14006
|
+
const fabPatterns = [
|
|
14007
|
+
{ regex: /\b\d+[,.]?\d*\s*(%|percent)/gi, label: "unverified percentage" },
|
|
14008
|
+
{ regex: /\b(award[- ]?winning|best[- ]selling|#\s*1)\b/gi, label: "unverified ranking" },
|
|
14009
|
+
{ regex: /\b(guaranteed|proven to|studies show)\b/gi, label: "unverified claim" }
|
|
14010
|
+
];
|
|
14011
|
+
for (const { regex, label } of fabPatterns) {
|
|
14012
|
+
regex.lastIndex = 0;
|
|
14013
|
+
if (regex.test(content)) {
|
|
14014
|
+
score -= 10;
|
|
14015
|
+
issues.push(`Potential ${label} detected`);
|
|
14016
|
+
}
|
|
14017
|
+
}
|
|
14018
|
+
score = Math.max(0, Math.min(100, score));
|
|
14019
|
+
const checkResult = {
|
|
14020
|
+
score,
|
|
14021
|
+
passed: score >= 60,
|
|
14022
|
+
issues,
|
|
14023
|
+
preferredTermsUsed: prefUsed,
|
|
14024
|
+
bannedTermsFound: bannedFound
|
|
14025
|
+
};
|
|
14026
|
+
const envelope = asEnvelope19(checkResult);
|
|
14027
|
+
return {
|
|
14028
|
+
content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }]
|
|
14029
|
+
};
|
|
14030
|
+
}
|
|
14031
|
+
);
|
|
14032
|
+
}
|
|
14033
|
+
|
|
14034
|
+
// src/lib/register-tools.ts
|
|
14035
|
+
function applyScopeEnforcement(server2, scopeResolver) {
|
|
14036
|
+
const originalTool = server2.tool.bind(server2);
|
|
14037
|
+
server2.tool = function wrappedTool(...args) {
|
|
14038
|
+
const name = args[0];
|
|
14039
|
+
const requiredScope = TOOL_SCOPES[name];
|
|
14040
|
+
const handlerIndex = args.findIndex(
|
|
14041
|
+
(a, i) => i > 0 && typeof a === "function"
|
|
14042
|
+
);
|
|
14043
|
+
if (handlerIndex !== -1) {
|
|
14044
|
+
const originalHandler = args[handlerIndex];
|
|
14045
|
+
args[handlerIndex] = async function scopeEnforcedHandler(...handlerArgs) {
|
|
14046
|
+
if (!requiredScope) {
|
|
14047
|
+
return {
|
|
14048
|
+
content: [
|
|
14049
|
+
{
|
|
14050
|
+
type: "text",
|
|
14051
|
+
text: `Permission denied: '${name}' has no scope defined. Contact support.`
|
|
14052
|
+
}
|
|
14053
|
+
],
|
|
14054
|
+
isError: true
|
|
14055
|
+
};
|
|
14056
|
+
}
|
|
14057
|
+
const userScopes = scopeResolver();
|
|
14058
|
+
if (!hasScope(userScopes, requiredScope)) {
|
|
14059
|
+
return {
|
|
14060
|
+
content: [
|
|
14061
|
+
{
|
|
14062
|
+
type: "text",
|
|
14063
|
+
text: `Permission denied: '${name}' requires scope '${requiredScope}'. Generate a new key with the required scope at https://socialneuron.com/settings/developer`
|
|
14064
|
+
}
|
|
14065
|
+
],
|
|
14066
|
+
isError: true
|
|
14067
|
+
};
|
|
14068
|
+
}
|
|
14069
|
+
const result = await originalHandler(...handlerArgs);
|
|
14070
|
+
return truncateResponse(result);
|
|
14071
|
+
};
|
|
14072
|
+
}
|
|
14073
|
+
return originalTool(...args);
|
|
14074
|
+
};
|
|
14075
|
+
}
|
|
14076
|
+
var RESPONSE_CHAR_LIMIT = 1e5;
|
|
14077
|
+
function truncateResponse(result) {
|
|
14078
|
+
if (!result?.content || !Array.isArray(result.content)) return result;
|
|
14079
|
+
let totalChars = 0;
|
|
14080
|
+
for (const part of result.content) {
|
|
14081
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
14082
|
+
totalChars += part.text.length;
|
|
14083
|
+
}
|
|
14084
|
+
}
|
|
14085
|
+
if (totalChars <= RESPONSE_CHAR_LIMIT) return result;
|
|
14086
|
+
let remaining = RESPONSE_CHAR_LIMIT;
|
|
14087
|
+
const truncated = [];
|
|
14088
|
+
for (const part of result.content) {
|
|
14089
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
14090
|
+
if (remaining <= 0) continue;
|
|
14091
|
+
if (part.text.length <= remaining) {
|
|
14092
|
+
truncated.push(part);
|
|
14093
|
+
remaining -= part.text.length;
|
|
14094
|
+
} else {
|
|
14095
|
+
truncated.push({
|
|
14096
|
+
...part,
|
|
14097
|
+
text: part.text.slice(0, remaining) + `
|
|
14098
|
+
|
|
14099
|
+
[Response truncated: ${totalChars.toLocaleString()} chars exceeded ${RESPONSE_CHAR_LIMIT.toLocaleString()} limit. Use filters to narrow your query.]`
|
|
14100
|
+
});
|
|
14101
|
+
remaining = 0;
|
|
14102
|
+
}
|
|
14103
|
+
} else {
|
|
14104
|
+
truncated.push(part);
|
|
14105
|
+
}
|
|
14106
|
+
}
|
|
14107
|
+
return { ...result, content: truncated };
|
|
14108
|
+
}
|
|
14109
|
+
function registerAllTools(server2, options) {
|
|
14110
|
+
registerIdeationTools(server2);
|
|
14111
|
+
registerContentTools(server2);
|
|
14112
|
+
registerDistributionTools(server2);
|
|
14113
|
+
registerAnalyticsTools(server2);
|
|
14114
|
+
registerBrandTools(server2);
|
|
14115
|
+
if (!options?.skipScreenshots) {
|
|
14116
|
+
registerScreenshotTools(server2);
|
|
14117
|
+
}
|
|
14118
|
+
registerRemotionTools(server2);
|
|
14119
|
+
registerInsightsTools(server2);
|
|
14120
|
+
registerYouTubeAnalyticsTools(server2);
|
|
14121
|
+
registerCommentsTools(server2);
|
|
14122
|
+
registerIdeationContextTools(server2);
|
|
14123
|
+
registerCreditsTools(server2);
|
|
14124
|
+
registerLoopSummaryTools(server2);
|
|
14125
|
+
registerUsageTools(server2);
|
|
14126
|
+
registerAutopilotTools(server2);
|
|
14127
|
+
registerExtractionTools(server2);
|
|
14128
|
+
registerQualityTools(server2);
|
|
14129
|
+
registerPlanningTools(server2);
|
|
14130
|
+
registerPlanApprovalTools(server2);
|
|
14131
|
+
registerDiscoveryTools(server2);
|
|
14132
|
+
registerPipelineTools(server2);
|
|
14133
|
+
registerSuggestTools(server2);
|
|
14134
|
+
registerDigestTools(server2);
|
|
14135
|
+
registerBrandRuntimeTools(server2);
|
|
14136
|
+
applyAnnotations(server2);
|
|
14137
|
+
}
|
|
14138
|
+
|
|
14139
|
+
// src/index.ts
|
|
14140
|
+
init_posthog();
|
|
14141
|
+
init_supabase();
|
|
14142
|
+
init_sn();
|
|
14143
|
+
|
|
14144
|
+
// src/prompts.ts
|
|
14145
|
+
import { z as z25 } from "zod";
|
|
14146
|
+
function registerPrompts(server2) {
|
|
14147
|
+
server2.prompt(
|
|
14148
|
+
"create_weekly_content_plan",
|
|
14149
|
+
"Generate a full week of social media content (7 days, multiple platforms). Returns a structured plan with topics, formats, and posting times.",
|
|
14150
|
+
{
|
|
14151
|
+
niche: z25.string().describe('Your content niche or industry (e.g., "fitness coaching", "SaaS marketing")'),
|
|
14152
|
+
platforms: z25.string().optional().describe(
|
|
14153
|
+
'Comma-separated platforms to target (default: "YouTube, Instagram, TikTok, LinkedIn")'
|
|
14154
|
+
),
|
|
14155
|
+
tone: z25.string().optional().describe('Brand tone of voice (e.g., "professional", "casual", "bold and edgy")')
|
|
14156
|
+
},
|
|
14157
|
+
({ niche, platforms, tone }) => {
|
|
14158
|
+
const targetPlatforms = platforms || "YouTube, Instagram, TikTok, LinkedIn";
|
|
14159
|
+
const brandTone = tone || "professional yet approachable";
|
|
14160
|
+
return {
|
|
14161
|
+
messages: [
|
|
14162
|
+
{
|
|
14163
|
+
role: "user",
|
|
14164
|
+
content: {
|
|
14165
|
+
type: "text",
|
|
14166
|
+
text: `Create a 7-day social media content plan for a ${niche} brand.
|
|
14167
|
+
|
|
14168
|
+
Target platforms: ${targetPlatforms}
|
|
14169
|
+
Brand tone: ${brandTone}
|
|
14170
|
+
|
|
14171
|
+
For each day, provide:
|
|
14172
|
+
1. **Topic/Theme** \u2014 what the content is about
|
|
14173
|
+
2. **Platform** \u2014 which platform this piece is for
|
|
14174
|
+
3. **Format** \u2014 (short video, carousel, story, text post, long-form video, etc.)
|
|
14175
|
+
4. **Hook** \u2014 the opening line or thumbnail concept
|
|
14176
|
+
5. **Key talking points** \u2014 3-4 bullet points
|
|
14177
|
+
6. **Call to action** \u2014 what the audience should do
|
|
14178
|
+
7. **Best posting time** \u2014 optimal time based on platform norms
|
|
14179
|
+
|
|
14180
|
+
Use Social Neuron tools:
|
|
14181
|
+
- Call \`generate_content_ideas\` for fresh topic suggestions
|
|
14182
|
+
- Call \`get_brand_profile\` to align with brand guidelines
|
|
14183
|
+
- Call \`get_performance_insights\` to learn what's worked before
|
|
14184
|
+
- Call \`get_best_posting_times\` for optimal scheduling
|
|
14185
|
+
|
|
14186
|
+
After building the plan, use \`create_content_plan\` to save it.`
|
|
14187
|
+
}
|
|
14188
|
+
}
|
|
14189
|
+
]
|
|
14190
|
+
};
|
|
14191
|
+
}
|
|
14192
|
+
);
|
|
14193
|
+
server2.prompt(
|
|
14194
|
+
"analyze_top_content",
|
|
14195
|
+
"Analyze your best-performing posts to identify patterns and replicate success. Returns insights on hooks, formats, timing, and topics that resonate.",
|
|
14196
|
+
{
|
|
14197
|
+
timeframe: z25.string().optional().describe('Analysis period (default: "30 days"). E.g., "7 days", "90 days"'),
|
|
14198
|
+
platform: z25.string().optional().describe('Filter to a specific platform (e.g., "youtube", "instagram")')
|
|
14199
|
+
},
|
|
14200
|
+
({ timeframe, platform: platform3 }) => {
|
|
14201
|
+
const period = timeframe || "30 days";
|
|
14202
|
+
const platformFilter = platform3 ? `
|
|
14203
|
+
Focus specifically on ${platform3} content.` : "";
|
|
14204
|
+
return {
|
|
14205
|
+
messages: [
|
|
14206
|
+
{
|
|
14207
|
+
role: "user",
|
|
14208
|
+
content: {
|
|
14209
|
+
type: "text",
|
|
14210
|
+
text: `Analyze my top-performing content from the last ${period}.${platformFilter}
|
|
14211
|
+
|
|
14212
|
+
Steps:
|
|
14213
|
+
1. Call \`get_analytics_summary\` for overall performance metrics
|
|
14214
|
+
2. Call \`get_performance_insights\` for AI-generated patterns
|
|
14215
|
+
3. Call \`get_best_posting_times\` for timing insights
|
|
14216
|
+
|
|
14217
|
+
Then provide:
|
|
14218
|
+
- **Top 5 posts** by engagement with analysis of why they worked
|
|
14219
|
+
- **Common patterns** in successful hooks, formats, and topics
|
|
14220
|
+
- **Optimal posting times** by platform
|
|
14221
|
+
- **Content gaps** \u2014 what topics or formats are underrepresented
|
|
14222
|
+
- **Actionable recommendations** \u2014 5 specific things to do next week
|
|
14223
|
+
|
|
14224
|
+
Format as a clear, actionable performance report.`
|
|
14225
|
+
}
|
|
14226
|
+
}
|
|
14227
|
+
]
|
|
14228
|
+
};
|
|
14229
|
+
}
|
|
14230
|
+
);
|
|
14231
|
+
server2.prompt(
|
|
14232
|
+
"repurpose_content",
|
|
14233
|
+
"Take one piece of content and transform it into 8-10 pieces across multiple platforms and formats.",
|
|
14234
|
+
{
|
|
14235
|
+
source: z25.string().describe(
|
|
14236
|
+
"The source content to repurpose \u2014 a URL, transcript, or the content text itself"
|
|
14237
|
+
),
|
|
14238
|
+
target_platforms: z25.string().optional().describe(
|
|
14239
|
+
'Comma-separated target platforms (default: "Twitter, LinkedIn, Instagram, TikTok, YouTube")'
|
|
14240
|
+
)
|
|
14241
|
+
},
|
|
14242
|
+
({ source, target_platforms }) => {
|
|
14243
|
+
const platforms = target_platforms || "Twitter, LinkedIn, Instagram, TikTok, YouTube";
|
|
14244
|
+
return {
|
|
14245
|
+
messages: [
|
|
14246
|
+
{
|
|
14247
|
+
role: "user",
|
|
14248
|
+
content: {
|
|
14249
|
+
type: "text",
|
|
14250
|
+
text: `Repurpose this content into 8-10 pieces across multiple platforms.
|
|
14251
|
+
|
|
14252
|
+
Source content:
|
|
14253
|
+
${source}
|
|
14254
|
+
|
|
14255
|
+
Target platforms: ${platforms}
|
|
14256
|
+
|
|
14257
|
+
Generate these variations:
|
|
14258
|
+
1. **5 standalone tweets** \u2014 each a different angle or quote from the source
|
|
14259
|
+
2. **2 LinkedIn posts** \u2014 one thought-leadership, one story-driven
|
|
14260
|
+
3. **1 Instagram caption** \u2014 with relevant hashtags
|
|
14261
|
+
4. **1 TikTok script** \u2014 30-60 second hook-driven format
|
|
14262
|
+
5. **1 newsletter section** \u2014 key takeaways with a CTA
|
|
14263
|
+
|
|
14264
|
+
Use Social Neuron tools:
|
|
14265
|
+
- Call \`generate_social_content\` for each platform variation
|
|
14266
|
+
- Call \`get_brand_profile\` to maintain brand voice consistency
|
|
14267
|
+
- Call \`score_content_quality\` to ensure each piece scores 70+
|
|
14268
|
+
|
|
14269
|
+
For each piece, include the platform, format, character count, and suggested posting time.`
|
|
14270
|
+
}
|
|
14271
|
+
}
|
|
14272
|
+
]
|
|
14273
|
+
};
|
|
14274
|
+
}
|
|
14275
|
+
);
|
|
14276
|
+
server2.prompt(
|
|
14277
|
+
"setup_brand_voice",
|
|
14278
|
+
"Define or refine your brand voice profile so all generated content stays on-brand. Walks through tone, audience, values, and style.",
|
|
14279
|
+
{
|
|
14280
|
+
brand_name: z25.string().describe("Your brand or business name"),
|
|
14281
|
+
industry: z25.string().optional().describe('Your industry or niche (e.g., "B2B SaaS", "fitness coaching")'),
|
|
14282
|
+
website: z25.string().optional().describe("Your website URL for context")
|
|
14283
|
+
},
|
|
14284
|
+
({ brand_name, industry, website }) => {
|
|
14285
|
+
const industryContext = industry ? ` in the ${industry} space` : "";
|
|
14286
|
+
const websiteContext = website ? `
|
|
14287
|
+
Website: ${website}` : "";
|
|
14288
|
+
return {
|
|
14289
|
+
messages: [
|
|
14290
|
+
{
|
|
14291
|
+
role: "user",
|
|
14292
|
+
content: {
|
|
14293
|
+
type: "text",
|
|
14294
|
+
text: `Help me set up a comprehensive brand voice profile for ${brand_name}${industryContext}.${websiteContext}
|
|
14295
|
+
|
|
14296
|
+
I need to define:
|
|
14297
|
+
1. **Brand personality** \u2014 3-5 adjectives that describe our voice
|
|
14298
|
+
2. **Target audience** \u2014 who we're speaking to (demographics, psychographics)
|
|
14299
|
+
3. **Tone spectrum** \u2014 where we fall on formal\u2194casual, serious\u2194playful, technical\u2194simple
|
|
14300
|
+
4. **Key messages** \u2014 3 core messages we always communicate
|
|
14301
|
+
5. **Words we use** \u2014 vocabulary that's on-brand
|
|
14302
|
+
6. **Words we avoid** \u2014 vocabulary that's off-brand
|
|
14303
|
+
7. **Content pillars** \u2014 3-5 recurring content themes
|
|
14304
|
+
|
|
14305
|
+
After we define these, use \`update_brand_profile\` to save the profile.
|
|
14306
|
+
Then use \`generate_social_content\` to create a sample post to verify the voice sounds right.`
|
|
14307
|
+
}
|
|
14308
|
+
}
|
|
14309
|
+
]
|
|
14310
|
+
};
|
|
14311
|
+
}
|
|
14312
|
+
);
|
|
14313
|
+
server2.prompt(
|
|
14314
|
+
"run_content_audit",
|
|
14315
|
+
"Audit your recent content performance and get a prioritized action plan for improvement.",
|
|
14316
|
+
{},
|
|
14317
|
+
() => ({
|
|
14318
|
+
messages: [
|
|
14319
|
+
{
|
|
14320
|
+
role: "user",
|
|
14321
|
+
content: {
|
|
14322
|
+
type: "text",
|
|
14323
|
+
text: `Run a comprehensive content audit on my Social Neuron account.
|
|
14324
|
+
|
|
14325
|
+
Steps:
|
|
14326
|
+
1. Call \`get_credit_balance\` to check account status
|
|
14327
|
+
2. Call \`get_analytics_summary\` for performance overview
|
|
14328
|
+
3. Call \`get_performance_insights\` for AI-generated analysis
|
|
14329
|
+
4. Call \`get_brand_profile\` to check brand alignment
|
|
14330
|
+
5. Call \`get_best_posting_times\` for scheduling optimization
|
|
14331
|
+
|
|
14332
|
+
Deliver a report covering:
|
|
14333
|
+
- **Account health** \u2014 credits remaining, plan tier, usage patterns
|
|
14334
|
+
- **Performance summary** \u2014 posts published, total engagement, trends
|
|
14335
|
+
- **Top performers** \u2014 what's working and why
|
|
14336
|
+
- **Underperformers** \u2014 what's not working and why
|
|
14337
|
+
- **Consistency score** \u2014 posting frequency vs. recommended cadence
|
|
14338
|
+
- **Brand alignment** \u2014 how well content matches brand profile
|
|
14339
|
+
- **Prioritized action items** \u2014 top 5 things to do this week, ranked by impact`
|
|
14340
|
+
}
|
|
14341
|
+
}
|
|
14342
|
+
]
|
|
14343
|
+
})
|
|
14344
|
+
);
|
|
14345
|
+
}
|
|
14346
|
+
|
|
14347
|
+
// src/resources.ts
|
|
14348
|
+
init_edge_function();
|
|
14349
|
+
init_version();
|
|
14350
|
+
function registerResources(server2) {
|
|
14351
|
+
server2.resource(
|
|
14352
|
+
"brand-profile",
|
|
14353
|
+
"socialneuron://brand/profile",
|
|
14354
|
+
{
|
|
14355
|
+
description: "Your brand voice profile including personality traits, target audience, tone, key messages, and content pillars. Read this before generating any content to stay on-brand.",
|
|
14356
|
+
mimeType: "application/json"
|
|
14357
|
+
},
|
|
14358
|
+
async () => {
|
|
14359
|
+
try {
|
|
14360
|
+
const { data, error } = await callEdgeFunction("mcp-data", { action: "brand-profile" });
|
|
14361
|
+
if (error || !data?.success) {
|
|
14362
|
+
return {
|
|
14363
|
+
contents: [
|
|
14364
|
+
{
|
|
14365
|
+
uri: "socialneuron://brand/profile",
|
|
14366
|
+
mimeType: "application/json",
|
|
14367
|
+
text: JSON.stringify(
|
|
14368
|
+
{
|
|
14369
|
+
_meta: { version: MCP_VERSION, status: "no_profile" },
|
|
14370
|
+
message: "No brand profile set up yet. Use the setup_brand_voice prompt or update_brand_profile tool to create one."
|
|
14371
|
+
},
|
|
14372
|
+
null,
|
|
14373
|
+
2
|
|
14374
|
+
)
|
|
14375
|
+
}
|
|
14376
|
+
]
|
|
14377
|
+
};
|
|
14378
|
+
}
|
|
14379
|
+
return {
|
|
14380
|
+
contents: [
|
|
14381
|
+
{
|
|
14382
|
+
uri: "socialneuron://brand/profile",
|
|
14383
|
+
mimeType: "application/json",
|
|
14384
|
+
text: JSON.stringify(
|
|
14385
|
+
{
|
|
14386
|
+
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
14387
|
+
...data.profile
|
|
14388
|
+
},
|
|
14389
|
+
null,
|
|
14390
|
+
2
|
|
14391
|
+
)
|
|
14392
|
+
}
|
|
14393
|
+
]
|
|
14394
|
+
};
|
|
14395
|
+
} catch {
|
|
14396
|
+
return {
|
|
14397
|
+
contents: [
|
|
14398
|
+
{
|
|
14399
|
+
uri: "socialneuron://brand/profile",
|
|
14400
|
+
mimeType: "application/json",
|
|
14401
|
+
text: JSON.stringify({
|
|
14402
|
+
_meta: { version: MCP_VERSION, status: "error" },
|
|
14403
|
+
message: "Failed to load brand profile. Check your connection and try again."
|
|
14404
|
+
})
|
|
14405
|
+
}
|
|
14406
|
+
]
|
|
14407
|
+
};
|
|
14408
|
+
}
|
|
14409
|
+
}
|
|
14410
|
+
);
|
|
14411
|
+
server2.resource(
|
|
14412
|
+
"account-overview",
|
|
14413
|
+
"socialneuron://account/overview",
|
|
14414
|
+
{
|
|
14415
|
+
description: "Current account status including plan tier, credit balance, monthly usage, connected platforms, and feature access. A quick snapshot of your Social Neuron account.",
|
|
14416
|
+
mimeType: "application/json"
|
|
14417
|
+
},
|
|
14418
|
+
async () => {
|
|
14419
|
+
try {
|
|
14420
|
+
const { data, error } = await callEdgeFunction("mcp-data", { action: "credit-balance" });
|
|
14421
|
+
const overview = {
|
|
14422
|
+
_meta: { version: MCP_VERSION, timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
14423
|
+
plan: data?.plan || "unknown",
|
|
14424
|
+
credits: {
|
|
14425
|
+
balance: data?.balance ?? 0,
|
|
14426
|
+
monthlyUsed: data?.monthlyUsed ?? 0,
|
|
14427
|
+
monthlyLimit: data?.monthlyLimit ?? 0,
|
|
14428
|
+
percentUsed: data?.monthlyLimit ? Math.round((data?.monthlyUsed ?? 0) / data.monthlyLimit * 100) : 0
|
|
14429
|
+
},
|
|
14430
|
+
status: error ? "error" : "ok",
|
|
14431
|
+
docs: "https://socialneuron.com/for-developers",
|
|
14432
|
+
pricing: "https://socialneuron.com/pricing"
|
|
14433
|
+
};
|
|
14434
|
+
return {
|
|
14435
|
+
contents: [
|
|
14436
|
+
{
|
|
14437
|
+
uri: "socialneuron://account/overview",
|
|
14438
|
+
mimeType: "application/json",
|
|
14439
|
+
text: JSON.stringify(overview, null, 2)
|
|
14440
|
+
}
|
|
14441
|
+
]
|
|
14442
|
+
};
|
|
14443
|
+
} catch {
|
|
14444
|
+
return {
|
|
14445
|
+
contents: [
|
|
14446
|
+
{
|
|
14447
|
+
uri: "socialneuron://account/overview",
|
|
14448
|
+
mimeType: "application/json",
|
|
14449
|
+
text: JSON.stringify({
|
|
14450
|
+
_meta: { version: MCP_VERSION, status: "error" },
|
|
14451
|
+
message: "Failed to load account overview."
|
|
14452
|
+
})
|
|
14453
|
+
}
|
|
14454
|
+
]
|
|
14455
|
+
};
|
|
14456
|
+
}
|
|
14457
|
+
}
|
|
14458
|
+
);
|
|
14459
|
+
server2.resource(
|
|
14460
|
+
"platform-capabilities",
|
|
14461
|
+
"socialneuron://docs/capabilities",
|
|
14462
|
+
{
|
|
14463
|
+
description: "Complete reference of all Social Neuron capabilities: supported platforms, content formats, AI models, credit costs, and feature availability by plan tier.",
|
|
14464
|
+
mimeType: "application/json"
|
|
14465
|
+
},
|
|
14466
|
+
async () => {
|
|
14467
|
+
const capabilities = {
|
|
14468
|
+
_meta: { version: MCP_VERSION, generated: (/* @__PURE__ */ new Date()).toISOString() },
|
|
14469
|
+
platforms: {
|
|
14470
|
+
available: ["YouTube", "Instagram", "TikTok"],
|
|
14471
|
+
coming_soon: ["LinkedIn", "X/Twitter", "Facebook", "Pinterest"]
|
|
14472
|
+
},
|
|
14473
|
+
content_formats: {
|
|
14474
|
+
text: ["Social post", "Thread", "Caption", "Newsletter", "Blog draft", "Script"],
|
|
14475
|
+
image: [
|
|
14476
|
+
"AI-generated image",
|
|
14477
|
+
"Quote graphic",
|
|
14478
|
+
"Carousel slide",
|
|
14479
|
+
"Thumbnail",
|
|
14480
|
+
"Story"
|
|
14481
|
+
],
|
|
14482
|
+
video: [
|
|
14483
|
+
"Short-form (< 60s)",
|
|
14484
|
+
"Long-form",
|
|
14485
|
+
"Storyboard",
|
|
14486
|
+
"Captioned clip",
|
|
14487
|
+
"YouTube optimized"
|
|
14488
|
+
],
|
|
14489
|
+
audio: ["Background music", "Voiceover"]
|
|
14490
|
+
},
|
|
14491
|
+
ai_models: {
|
|
14492
|
+
text: ["Gemini 2.5 Flash", "Gemini 2.5 Pro"],
|
|
14493
|
+
image: [
|
|
14494
|
+
"Flux 1.1 Pro",
|
|
14495
|
+
"DALL-E 3",
|
|
14496
|
+
"Stable Diffusion XL",
|
|
14497
|
+
"Ideogram",
|
|
14498
|
+
"Recraft V3",
|
|
14499
|
+
"Mystic V2"
|
|
14500
|
+
],
|
|
14501
|
+
video: ["Veo 3", "Sora 2", "Runway Gen-4", "Kling 2.0", "Minimax", "Wan 2.1"]
|
|
14502
|
+
},
|
|
14503
|
+
credit_costs: {
|
|
14504
|
+
text_generation: "1-3 credits",
|
|
14505
|
+
image_generation: "2-10 credits",
|
|
14506
|
+
video_generation: "15-80 credits",
|
|
14507
|
+
analytics_query: "0 credits",
|
|
14508
|
+
distribution: "1 credit per platform"
|
|
14509
|
+
},
|
|
14510
|
+
tiers: {
|
|
14511
|
+
free: {
|
|
14512
|
+
price: "$0/mo",
|
|
14513
|
+
credits: 100,
|
|
14514
|
+
mcp_access: false,
|
|
14515
|
+
features: ["5 free tools", "Basic content generation"]
|
|
14516
|
+
},
|
|
14517
|
+
starter: {
|
|
14518
|
+
price: "$29/mo",
|
|
14519
|
+
credits: 800,
|
|
14520
|
+
mcp_access: "Read + Analytics",
|
|
14521
|
+
features: ["All free features", "MCP read access", "Analytics", "3 platforms"]
|
|
14522
|
+
},
|
|
14523
|
+
pro: {
|
|
14524
|
+
price: "$79/mo",
|
|
14525
|
+
credits: 2e3,
|
|
14526
|
+
mcp_access: "Full",
|
|
14527
|
+
features: [
|
|
14528
|
+
"All Starter features",
|
|
14529
|
+
"Full MCP access",
|
|
14530
|
+
"Video generation",
|
|
14531
|
+
"Autopilot",
|
|
14532
|
+
"Priority support"
|
|
14533
|
+
]
|
|
14534
|
+
},
|
|
14535
|
+
team: {
|
|
14536
|
+
price: "$199/mo",
|
|
14537
|
+
credits: 6500,
|
|
14538
|
+
mcp_access: "Full + Multi-user",
|
|
14539
|
+
features: [
|
|
14540
|
+
"All Pro features",
|
|
14541
|
+
"Team collaboration",
|
|
14542
|
+
"Up to 10 members",
|
|
14543
|
+
"50 projects",
|
|
14544
|
+
"Advanced analytics"
|
|
14545
|
+
]
|
|
14546
|
+
}
|
|
14547
|
+
}
|
|
14548
|
+
};
|
|
14549
|
+
return {
|
|
14550
|
+
contents: [
|
|
14551
|
+
{
|
|
14552
|
+
uri: "socialneuron://docs/capabilities",
|
|
14553
|
+
mimeType: "application/json",
|
|
14554
|
+
text: JSON.stringify(capabilities, null, 2)
|
|
14555
|
+
}
|
|
14556
|
+
]
|
|
14557
|
+
};
|
|
14558
|
+
}
|
|
14559
|
+
);
|
|
14560
|
+
server2.resource(
|
|
14561
|
+
"getting-started",
|
|
14562
|
+
"socialneuron://docs/getting-started",
|
|
14563
|
+
{
|
|
14564
|
+
description: "Quick start guide for using Social Neuron with AI agents. Covers authentication, first content creation, and common workflows.",
|
|
14565
|
+
mimeType: "text/plain"
|
|
14566
|
+
},
|
|
14567
|
+
async () => ({
|
|
14568
|
+
contents: [
|
|
14569
|
+
{
|
|
14570
|
+
uri: "socialneuron://docs/getting-started",
|
|
14571
|
+
mimeType: "text/plain",
|
|
14572
|
+
text: `# Getting Started with Social Neuron MCP Server
|
|
14573
|
+
|
|
14574
|
+
## Quick Start
|
|
14575
|
+
|
|
14576
|
+
1. Check your account: Read the \`socialneuron://account/overview\` resource
|
|
14577
|
+
2. Set up your brand: Use the \`setup_brand_voice\` prompt
|
|
14578
|
+
3. Generate content: Call \`generate_social_content\` with a topic
|
|
14579
|
+
4. Review & publish: Call \`publish_post\` to distribute
|
|
14580
|
+
|
|
14581
|
+
## Common Workflows
|
|
14582
|
+
|
|
14583
|
+
### Create & Publish a Post
|
|
14584
|
+
1. \`generate_content_ideas\` \u2192 get topic suggestions
|
|
14585
|
+
2. \`generate_social_content\` \u2192 create the post
|
|
14586
|
+
3. \`score_content_quality\` \u2192 check quality (aim for 70+)
|
|
14587
|
+
4. \`publish_post\` \u2192 distribute to platforms
|
|
14588
|
+
|
|
14589
|
+
### Analyze Performance
|
|
14590
|
+
1. \`get_analytics_summary\` \u2192 see overall metrics
|
|
14591
|
+
2. \`get_performance_insights\` \u2192 AI analysis of patterns
|
|
14592
|
+
3. \`get_best_posting_times\` \u2192 optimize scheduling
|
|
14593
|
+
|
|
14594
|
+
### Repurpose Content
|
|
14595
|
+
1. Use the \`repurpose_content\` prompt with your source material
|
|
14596
|
+
2. Review each generated variation
|
|
14597
|
+
3. Schedule across platforms using \`create_content_plan\`
|
|
14598
|
+
|
|
14599
|
+
### Set Up Autopilot
|
|
14600
|
+
1. \`get_brand_profile\` \u2192 verify brand settings
|
|
14601
|
+
2. \`configure_autopilot\` \u2192 set schedule and preferences
|
|
14602
|
+
3. \`enable_autopilot\` \u2192 start automated posting
|
|
14603
|
+
|
|
14604
|
+
## Credit Tips
|
|
14605
|
+
- Text generation: 1-3 credits
|
|
14606
|
+
- Image generation: 2-10 credits
|
|
14607
|
+
- Video generation: 15-80 credits
|
|
14608
|
+
- Check balance anytime: \`get_credit_balance\`
|
|
14609
|
+
|
|
14610
|
+
## Need Help?
|
|
14611
|
+
- Docs: https://socialneuron.com/for-developers
|
|
14612
|
+
- Support: socialneuronteam@gmail.com
|
|
14613
|
+
`
|
|
14614
|
+
}
|
|
14615
|
+
]
|
|
14616
|
+
})
|
|
14617
|
+
);
|
|
14618
|
+
}
|
|
14619
|
+
|
|
14620
|
+
// src/index.ts
|
|
11947
14621
|
function flushAndExit(code) {
|
|
11948
14622
|
const done = { out: false, err: false };
|
|
11949
14623
|
const tryExit = () => {
|
|
@@ -12133,6 +14807,8 @@ var server = new McpServer({
|
|
|
12133
14807
|
});
|
|
12134
14808
|
applyScopeEnforcement(server, getAuthenticatedScopes);
|
|
12135
14809
|
registerAllTools(server);
|
|
14810
|
+
registerPrompts(server);
|
|
14811
|
+
registerResources(server);
|
|
12136
14812
|
async function shutdown() {
|
|
12137
14813
|
await shutdownPostHog();
|
|
12138
14814
|
process.exit(0);
|