@socialneuron/mcp-server 1.6.1 → 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/dist/http.js +2537 -148
- package/dist/index.js +2843 -160
- 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,12 +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
352
|
getAuthenticatedExpiresAt: () => getAuthenticatedExpiresAt,
|
|
354
353
|
getAuthenticatedScopes: () => getAuthenticatedScopes,
|
|
354
|
+
getCloudAnonKey: () => getCloudAnonKey,
|
|
355
355
|
getDefaultProjectId: () => getDefaultProjectId,
|
|
356
356
|
getDefaultUserId: () => getDefaultUserId,
|
|
357
357
|
getMcpRunId: () => getMcpRunId,
|
|
@@ -376,11 +376,47 @@ function getSupabaseClient() {
|
|
|
376
376
|
}
|
|
377
377
|
return client2;
|
|
378
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
|
+
}
|
|
379
404
|
function getSupabaseUrl() {
|
|
380
405
|
if (SUPABASE_URL) return SUPABASE_URL;
|
|
381
406
|
const cloudOverride = process.env.SOCIALNEURON_CLOUD_SUPABASE_URL;
|
|
382
407
|
if (cloudOverride) return cloudOverride;
|
|
383
|
-
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
|
+
);
|
|
384
420
|
}
|
|
385
421
|
function getServiceKey() {
|
|
386
422
|
if (!SUPABASE_SERVICE_KEY) {
|
|
@@ -427,6 +463,12 @@ async function getDefaultProjectId() {
|
|
|
427
463
|
return null;
|
|
428
464
|
}
|
|
429
465
|
async function initializeAuth() {
|
|
466
|
+
if (!SUPABASE_URL) {
|
|
467
|
+
try {
|
|
468
|
+
await fetchCloudConfig();
|
|
469
|
+
} catch {
|
|
470
|
+
}
|
|
471
|
+
}
|
|
430
472
|
const { loadApiKey: loadApiKey2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
|
|
431
473
|
const apiKey = await loadApiKey2();
|
|
432
474
|
if (apiKey) {
|
|
@@ -530,7 +572,7 @@ async function logMcpToolInvocation(args) {
|
|
|
530
572
|
captureToolEvent(args).catch(() => {
|
|
531
573
|
});
|
|
532
574
|
}
|
|
533
|
-
var SUPABASE_URL, SUPABASE_SERVICE_KEY, client2, _authMode, authenticatedUserId, authenticatedScopes, authenticatedExpiresAt, authenticatedApiKey, MCP_RUN_ID,
|
|
575
|
+
var SUPABASE_URL, SUPABASE_SERVICE_KEY, client2, _authMode, authenticatedUserId, authenticatedScopes, authenticatedExpiresAt, authenticatedApiKey, MCP_RUN_ID, CLOUD_CONFIG_URL, _cloudConfig, projectIdCache;
|
|
534
576
|
var init_supabase = __esm({
|
|
535
577
|
"src/lib/supabase.ts"() {
|
|
536
578
|
"use strict";
|
|
@@ -545,8 +587,8 @@ var init_supabase = __esm({
|
|
|
545
587
|
authenticatedExpiresAt = null;
|
|
546
588
|
authenticatedApiKey = null;
|
|
547
589
|
MCP_RUN_ID = randomUUID();
|
|
548
|
-
|
|
549
|
-
|
|
590
|
+
CLOUD_CONFIG_URL = process.env.SOCIALNEURON_CONFIG_URL || "https://mcp.socialneuron.com/config";
|
|
591
|
+
_cloudConfig = null;
|
|
550
592
|
projectIdCache = /* @__PURE__ */ new Map();
|
|
551
593
|
}
|
|
552
594
|
});
|
|
@@ -963,6 +1005,24 @@ var init_tool_catalog = __esm({
|
|
|
963
1005
|
module: "brand",
|
|
964
1006
|
scope: "mcp:read"
|
|
965
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
|
+
},
|
|
966
1026
|
{
|
|
967
1027
|
name: "save_brand_profile",
|
|
968
1028
|
description: "Save or update brand profile",
|
|
@@ -1001,6 +1061,12 @@ var init_tool_catalog = __esm({
|
|
|
1001
1061
|
module: "remotion",
|
|
1002
1062
|
scope: "mcp:read"
|
|
1003
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
|
+
},
|
|
1004
1070
|
// youtube-analytics
|
|
1005
1071
|
{
|
|
1006
1072
|
name: "fetch_youtube_analytics",
|
|
@@ -1173,6 +1239,58 @@ var init_tool_catalog = __esm({
|
|
|
1173
1239
|
description: "Search and discover available MCP tools",
|
|
1174
1240
|
module: "discovery",
|
|
1175
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"
|
|
1176
1294
|
}
|
|
1177
1295
|
];
|
|
1178
1296
|
}
|
|
@@ -2947,7 +3065,7 @@ __export(setup_exports, {
|
|
|
2947
3065
|
runLogout: () => runLogout,
|
|
2948
3066
|
runSetup: () => runSetup
|
|
2949
3067
|
});
|
|
2950
|
-
import { createHash as createHash4, randomBytes, randomUUID as
|
|
3068
|
+
import { createHash as createHash4, randomBytes, randomUUID as randomUUID4 } from "node:crypto";
|
|
2951
3069
|
import {
|
|
2952
3070
|
createServer
|
|
2953
3071
|
} from "node:http";
|
|
@@ -2968,7 +3086,11 @@ function getAppBaseUrl() {
|
|
|
2968
3086
|
return process.env.SOCIALNEURON_APP_URL || "https://www.socialneuron.com";
|
|
2969
3087
|
}
|
|
2970
3088
|
function getDefaultSupabaseUrl() {
|
|
2971
|
-
|
|
3089
|
+
try {
|
|
3090
|
+
return getSupabaseUrl();
|
|
3091
|
+
} catch {
|
|
3092
|
+
return "https://mcp.socialneuron.com";
|
|
3093
|
+
}
|
|
2972
3094
|
}
|
|
2973
3095
|
function getConfigPaths() {
|
|
2974
3096
|
const paths = [];
|
|
@@ -3074,7 +3196,7 @@ async function runSetup() {
|
|
|
3074
3196
|
);
|
|
3075
3197
|
console.error("");
|
|
3076
3198
|
const { codeVerifier, codeChallenge } = generatePKCE();
|
|
3077
|
-
const state =
|
|
3199
|
+
const state = randomUUID4();
|
|
3078
3200
|
const { server: server2, port } = await new Promise((resolve3, reject) => {
|
|
3079
3201
|
const srv = createServer();
|
|
3080
3202
|
srv.listen(0, "127.0.0.1", () => {
|
|
@@ -3232,7 +3354,11 @@ function prompt(question) {
|
|
|
3232
3354
|
});
|
|
3233
3355
|
}
|
|
3234
3356
|
function getDefaultSupabaseUrl2() {
|
|
3235
|
-
|
|
3357
|
+
try {
|
|
3358
|
+
return getSupabaseUrl();
|
|
3359
|
+
} catch {
|
|
3360
|
+
return "https://mcp.socialneuron.com";
|
|
3361
|
+
}
|
|
3236
3362
|
}
|
|
3237
3363
|
async function runLogin(method) {
|
|
3238
3364
|
if (method === "browser") {
|
|
@@ -3748,6 +3874,9 @@ var TOOL_SCOPES = {
|
|
|
3748
3874
|
get_best_posting_times: "mcp:read",
|
|
3749
3875
|
extract_brand: "mcp:read",
|
|
3750
3876
|
get_brand_profile: "mcp:read",
|
|
3877
|
+
get_brand_runtime: "mcp:read",
|
|
3878
|
+
explain_brand_system: "mcp:read",
|
|
3879
|
+
check_brand_consistency: "mcp:read",
|
|
3751
3880
|
get_ideation_context: "mcp:read",
|
|
3752
3881
|
get_credit_balance: "mcp:read",
|
|
3753
3882
|
get_budget_status: "mcp:read",
|
|
@@ -3763,6 +3892,7 @@ var TOOL_SCOPES = {
|
|
|
3763
3892
|
generate_image: "mcp:write",
|
|
3764
3893
|
check_status: "mcp:read",
|
|
3765
3894
|
render_demo_video: "mcp:write",
|
|
3895
|
+
render_template_video: "mcp:write",
|
|
3766
3896
|
save_brand_profile: "mcp:write",
|
|
3767
3897
|
update_platform_voice: "mcp:write",
|
|
3768
3898
|
create_storyboard: "mcp:write",
|
|
@@ -3801,7 +3931,19 @@ var TOOL_SCOPES = {
|
|
|
3801
3931
|
// mcp:read (usage is read-only)
|
|
3802
3932
|
get_mcp_usage: "mcp:read",
|
|
3803
3933
|
list_plan_approvals: "mcp:read",
|
|
3804
|
-
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"
|
|
3805
3947
|
};
|
|
3806
3948
|
function hasScope(userScopes, required) {
|
|
3807
3949
|
if (userScopes.includes(required)) return true;
|
|
@@ -3812,6 +3954,147 @@ function hasScope(userScopes, required) {
|
|
|
3812
3954
|
return false;
|
|
3813
3955
|
}
|
|
3814
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
|
+
|
|
3815
4098
|
// src/tools/ideation.ts
|
|
3816
4099
|
init_edge_function();
|
|
3817
4100
|
import { z } from "zod";
|
|
@@ -8250,6 +8533,7 @@ import { z as z7 } from "zod";
|
|
|
8250
8533
|
import { resolve as resolve2 } from "node:path";
|
|
8251
8534
|
import { mkdir as mkdir2 } from "node:fs/promises";
|
|
8252
8535
|
init_supabase();
|
|
8536
|
+
init_edge_function();
|
|
8253
8537
|
var COMPOSITIONS = [
|
|
8254
8538
|
{
|
|
8255
8539
|
id: "CaptionedClip",
|
|
@@ -8354,6 +8638,22 @@ var COMPOSITIONS = [
|
|
|
8354
8638
|
durationInFrames: 450,
|
|
8355
8639
|
fps: 30,
|
|
8356
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)"
|
|
8357
8657
|
}
|
|
8358
8658
|
];
|
|
8359
8659
|
function registerRemotionTools(server2) {
|
|
@@ -8361,13 +8661,6 @@ function registerRemotionTools(server2) {
|
|
|
8361
8661
|
"list_compositions",
|
|
8362
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.",
|
|
8363
8663
|
{},
|
|
8364
|
-
{
|
|
8365
|
-
title: "List Compositions",
|
|
8366
|
-
readOnlyHint: true,
|
|
8367
|
-
destructiveHint: false,
|
|
8368
|
-
idempotentHint: true,
|
|
8369
|
-
openWorldHint: false
|
|
8370
|
-
},
|
|
8371
8664
|
async () => {
|
|
8372
8665
|
const lines = [`${COMPOSITIONS.length} Remotion compositions available:`, ""];
|
|
8373
8666
|
for (const comp of COMPOSITIONS) {
|
|
@@ -8396,13 +8689,6 @@ function registerRemotionTools(server2) {
|
|
|
8396
8689
|
"JSON string of input props to pass to the composition. Each composition accepts different props. Omit for defaults."
|
|
8397
8690
|
)
|
|
8398
8691
|
},
|
|
8399
|
-
{
|
|
8400
|
-
title: "Render Demo Video",
|
|
8401
|
-
readOnlyHint: false,
|
|
8402
|
-
destructiveHint: false,
|
|
8403
|
-
idempotentHint: false,
|
|
8404
|
-
openWorldHint: false
|
|
8405
|
-
},
|
|
8406
8692
|
async ({ composition_id, output_format, props }) => {
|
|
8407
8693
|
const startedAt = Date.now();
|
|
8408
8694
|
const userId = await getDefaultUserId();
|
|
@@ -8534,6 +8820,134 @@ function registerRemotionTools(server2) {
|
|
|
8534
8820
|
}
|
|
8535
8821
|
}
|
|
8536
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
|
+
);
|
|
8537
8951
|
}
|
|
8538
8952
|
|
|
8539
8953
|
// src/tools/insights.ts
|
|
@@ -9950,9 +10364,9 @@ ${"=".repeat(40)}
|
|
|
9950
10364
|
}
|
|
9951
10365
|
|
|
9952
10366
|
// src/tools/autopilot.ts
|
|
9953
|
-
|
|
9954
|
-
import { z as z15 } from "zod";
|
|
10367
|
+
init_edge_function();
|
|
9955
10368
|
init_version();
|
|
10369
|
+
import { z as z15 } from "zod";
|
|
9956
10370
|
function asEnvelope11(data) {
|
|
9957
10371
|
return {
|
|
9958
10372
|
_meta: {
|
|
@@ -9970,46 +10384,35 @@ function registerAutopilotTools(server2) {
|
|
|
9970
10384
|
active_only: z15.boolean().optional().describe("If true, only return active configs. Defaults to false (show all)."),
|
|
9971
10385
|
response_format: z15.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
9972
10386
|
},
|
|
9973
|
-
{
|
|
9974
|
-
title: "List Autopilot Configs",
|
|
9975
|
-
readOnlyHint: true,
|
|
9976
|
-
destructiveHint: false,
|
|
9977
|
-
idempotentHint: true,
|
|
9978
|
-
openWorldHint: false
|
|
9979
|
-
},
|
|
9980
10387
|
async ({ active_only, response_format }) => {
|
|
9981
10388
|
const format = response_format ?? "text";
|
|
9982
|
-
const
|
|
9983
|
-
|
|
9984
|
-
|
|
9985
|
-
|
|
9986
|
-
|
|
9987
|
-
if (active_only) {
|
|
9988
|
-
query = query.eq("is_active", true);
|
|
9989
|
-
}
|
|
9990
|
-
const { data: configs, error } = await query;
|
|
9991
|
-
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) {
|
|
9992
10394
|
return {
|
|
9993
10395
|
content: [
|
|
9994
10396
|
{
|
|
9995
10397
|
type: "text",
|
|
9996
|
-
text: `Error fetching autopilot configs: ${
|
|
10398
|
+
text: `Error fetching autopilot configs: ${efError}`
|
|
9997
10399
|
}
|
|
9998
10400
|
],
|
|
9999
10401
|
isError: true
|
|
10000
10402
|
};
|
|
10001
10403
|
}
|
|
10404
|
+
const configs = result?.configs ?? [];
|
|
10002
10405
|
if (format === "json") {
|
|
10003
10406
|
return {
|
|
10004
10407
|
content: [
|
|
10005
10408
|
{
|
|
10006
10409
|
type: "text",
|
|
10007
|
-
text: JSON.stringify(asEnvelope11(configs
|
|
10410
|
+
text: JSON.stringify(asEnvelope11(configs), null, 2)
|
|
10008
10411
|
}
|
|
10009
10412
|
]
|
|
10010
10413
|
};
|
|
10011
10414
|
}
|
|
10012
|
-
if (
|
|
10415
|
+
if (configs.length === 0) {
|
|
10013
10416
|
return {
|
|
10014
10417
|
content: [
|
|
10015
10418
|
{
|
|
@@ -10054,18 +10457,11 @@ ${"=".repeat(40)}
|
|
|
10054
10457
|
{
|
|
10055
10458
|
config_id: z15.string().uuid().describe("The autopilot config ID to update."),
|
|
10056
10459
|
is_active: z15.boolean().optional().describe("Enable or disable this autopilot config."),
|
|
10057
|
-
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"]).'),
|
|
10058
10461
|
schedule_time: z15.string().optional().describe('Time to run in HH:MM format (24h, user timezone). E.g., "09:00".'),
|
|
10059
10462
|
max_credits_per_run: z15.number().optional().describe("Maximum credits per execution."),
|
|
10060
10463
|
max_credits_per_week: z15.number().optional().describe("Maximum credits per week.")
|
|
10061
10464
|
},
|
|
10062
|
-
{
|
|
10063
|
-
title: "Update Autopilot Config",
|
|
10064
|
-
readOnlyHint: false,
|
|
10065
|
-
destructiveHint: false,
|
|
10066
|
-
idempotentHint: true,
|
|
10067
|
-
openWorldHint: false
|
|
10068
|
-
},
|
|
10069
10465
|
async ({
|
|
10070
10466
|
config_id,
|
|
10071
10467
|
is_active,
|
|
@@ -10074,22 +10470,7 @@ ${"=".repeat(40)}
|
|
|
10074
10470
|
max_credits_per_run,
|
|
10075
10471
|
max_credits_per_week
|
|
10076
10472
|
}) => {
|
|
10077
|
-
|
|
10078
|
-
const userId = await getDefaultUserId();
|
|
10079
|
-
const updates = {};
|
|
10080
|
-
if (is_active !== void 0) updates.is_active = is_active;
|
|
10081
|
-
if (max_credits_per_run !== void 0) updates.max_credits_per_run = max_credits_per_run;
|
|
10082
|
-
if (max_credits_per_week !== void 0) updates.max_credits_per_week = max_credits_per_week;
|
|
10083
|
-
if (schedule_days || schedule_time) {
|
|
10084
|
-
const { data: existing } = await supabase.from("autopilot_configs").select("schedule_config").eq("id", config_id).eq("user_id", userId).single();
|
|
10085
|
-
const existingSchedule = existing?.schedule_config || {};
|
|
10086
|
-
updates.schedule_config = {
|
|
10087
|
-
...existingSchedule,
|
|
10088
|
-
...schedule_days ? { days: schedule_days } : {},
|
|
10089
|
-
...schedule_time ? { time: schedule_time } : {}
|
|
10090
|
-
};
|
|
10091
|
-
}
|
|
10092
|
-
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) {
|
|
10093
10474
|
return {
|
|
10094
10475
|
content: [
|
|
10095
10476
|
{
|
|
@@ -10099,18 +10480,37 @@ ${"=".repeat(40)}
|
|
|
10099
10480
|
]
|
|
10100
10481
|
};
|
|
10101
10482
|
}
|
|
10102
|
-
const { data:
|
|
10103
|
-
|
|
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) {
|
|
10104
10493
|
return {
|
|
10105
10494
|
content: [
|
|
10106
10495
|
{
|
|
10107
10496
|
type: "text",
|
|
10108
|
-
text: `Error updating config: ${
|
|
10497
|
+
text: `Error updating config: ${efError}`
|
|
10109
10498
|
}
|
|
10110
10499
|
],
|
|
10111
10500
|
isError: true
|
|
10112
10501
|
};
|
|
10113
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
|
+
}
|
|
10114
10514
|
return {
|
|
10115
10515
|
content: [
|
|
10116
10516
|
{
|
|
@@ -10129,26 +10529,19 @@ Schedule: ${JSON.stringify(updated.schedule_config)}`
|
|
|
10129
10529
|
{
|
|
10130
10530
|
response_format: z15.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
|
|
10131
10531
|
},
|
|
10132
|
-
{
|
|
10133
|
-
title: "Get Autopilot Status",
|
|
10134
|
-
readOnlyHint: true,
|
|
10135
|
-
destructiveHint: false,
|
|
10136
|
-
idempotentHint: true,
|
|
10137
|
-
openWorldHint: false
|
|
10138
|
-
},
|
|
10139
10532
|
async ({ response_format }) => {
|
|
10140
10533
|
const format = response_format ?? "text";
|
|
10141
|
-
const
|
|
10142
|
-
|
|
10143
|
-
|
|
10144
|
-
|
|
10145
|
-
|
|
10146
|
-
|
|
10147
|
-
|
|
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
|
+
}
|
|
10148
10541
|
const statusData = {
|
|
10149
|
-
activeConfigs:
|
|
10150
|
-
recentRuns:
|
|
10151
|
-
pendingApprovals:
|
|
10542
|
+
activeConfigs: result?.activeConfigs ?? 0,
|
|
10543
|
+
recentRuns: [],
|
|
10544
|
+
pendingApprovals: result?.pendingApprovals ?? 0
|
|
10152
10545
|
};
|
|
10153
10546
|
if (format === "json") {
|
|
10154
10547
|
return {
|
|
@@ -10169,24 +10562,95 @@ ${"=".repeat(40)}
|
|
|
10169
10562
|
text += `Pending Approvals: ${statusData.pendingApprovals}
|
|
10170
10563
|
|
|
10171
10564
|
`;
|
|
10172
|
-
|
|
10173
|
-
text += `Recent Runs:
|
|
10174
|
-
`;
|
|
10175
|
-
for (const run of statusData.recentRuns) {
|
|
10176
|
-
text += ` ${run.id.substring(0, 8)}... \u2014 ${run.status} (${run.started_at})
|
|
10177
|
-
`;
|
|
10178
|
-
}
|
|
10179
|
-
} else {
|
|
10180
|
-
text += `No recent runs.
|
|
10565
|
+
text += `No recent runs.
|
|
10181
10566
|
`;
|
|
10182
|
-
}
|
|
10183
10567
|
return {
|
|
10184
10568
|
content: [{ type: "text", text }]
|
|
10185
10569
|
};
|
|
10186
10570
|
}
|
|
10187
10571
|
);
|
|
10188
|
-
|
|
10189
|
-
|
|
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
|
+
}
|
|
10653
|
+
|
|
10190
10654
|
// src/tools/extraction.ts
|
|
10191
10655
|
init_edge_function();
|
|
10192
10656
|
init_supabase();
|
|
@@ -11867,76 +12331,2293 @@ function registerDiscoveryTools(server2) {
|
|
|
11867
12331
|
);
|
|
11868
12332
|
}
|
|
11869
12333
|
|
|
11870
|
-
// src/
|
|
11871
|
-
|
|
11872
|
-
|
|
11873
|
-
|
|
11874
|
-
|
|
11875
|
-
|
|
11876
|
-
|
|
11877
|
-
|
|
11878
|
-
|
|
11879
|
-
|
|
11880
|
-
|
|
11881
|
-
|
|
11882
|
-
|
|
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) {
|
|
11883
12569
|
return {
|
|
11884
12570
|
content: [
|
|
11885
12571
|
{
|
|
11886
12572
|
type: "text",
|
|
11887
|
-
text: `
|
|
12573
|
+
text: `Insufficient credits: ${availableCredits} available, ~${estimatedCost} needed.`
|
|
11888
12574
|
}
|
|
11889
12575
|
],
|
|
11890
12576
|
isError: true
|
|
11891
12577
|
};
|
|
11892
12578
|
}
|
|
11893
|
-
|
|
11894
|
-
|
|
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
|
+
);
|
|
11895
12630
|
return {
|
|
11896
12631
|
content: [
|
|
11897
|
-
{
|
|
11898
|
-
type: "text",
|
|
11899
|
-
text: `Permission denied: '${name}' requires scope '${requiredScope}'. Generate a new key with the required scope at https://socialneuron.com/settings/developer`
|
|
11900
|
-
}
|
|
12632
|
+
{ type: "text", text: `Planning failed: ${planError ?? "No AI response"}` }
|
|
11901
12633
|
],
|
|
11902
12634
|
isError: true
|
|
11903
12635
|
};
|
|
11904
12636
|
}
|
|
11905
|
-
|
|
11906
|
-
|
|
11907
|
-
|
|
11908
|
-
|
|
11909
|
-
|
|
11910
|
-
|
|
11911
|
-
|
|
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
|
-
|
|
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
|
|
11940
14621
|
function flushAndExit(code) {
|
|
11941
14622
|
const done = { out: false, err: false };
|
|
11942
14623
|
const tryExit = () => {
|
|
@@ -12126,6 +14807,8 @@ var server = new McpServer({
|
|
|
12126
14807
|
});
|
|
12127
14808
|
applyScopeEnforcement(server, getAuthenticatedScopes);
|
|
12128
14809
|
registerAllTools(server);
|
|
14810
|
+
registerPrompts(server);
|
|
14811
|
+
registerResources(server);
|
|
12129
14812
|
async function shutdown() {
|
|
12130
14813
|
await shutdownPostHog();
|
|
12131
14814
|
process.exit(0);
|