@socialneuron/mcp-server 1.6.0 → 1.7.0

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