@socialneuron/mcp-server 1.6.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/http.js +2537 -148
  2. package/dist/index.js +2843 -160
  3. 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,12 +346,12 @@ var init_api_keys = __esm({
346
346
  // src/lib/supabase.ts
347
347
  var supabase_exports = {};
348
348
  __export(supabase_exports, {
349
- 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
352
  getAuthenticatedExpiresAt: () => getAuthenticatedExpiresAt,
354
353
  getAuthenticatedScopes: () => getAuthenticatedScopes,
354
+ getCloudAnonKey: () => getCloudAnonKey,
355
355
  getDefaultProjectId: () => getDefaultProjectId,
356
356
  getDefaultUserId: () => getDefaultUserId,
357
357
  getMcpRunId: () => getMcpRunId,
@@ -376,11 +376,47 @@ function getSupabaseClient() {
376
376
  }
377
377
  return client2;
378
378
  }
379
+ async function fetchCloudConfig() {
380
+ if (_cloudConfig) return _cloudConfig;
381
+ const envUrl = process.env.SOCIALNEURON_CLOUD_SUPABASE_URL || process.env.SUPABASE_URL;
382
+ const envAnon = process.env.SUPABASE_ANON_KEY || process.env.SOCIALNEURON_ANON_KEY || process.env.VITE_SUPABASE_ANON_KEY;
383
+ if (envUrl && envAnon) {
384
+ _cloudConfig = { supabaseUrl: envUrl, anonKey: envAnon };
385
+ return _cloudConfig;
386
+ }
387
+ try {
388
+ const resp = await fetch(CLOUD_CONFIG_URL, {
389
+ signal: AbortSignal.timeout(5e3)
390
+ });
391
+ if (!resp.ok) {
392
+ throw new Error(`Config fetch failed: ${resp.status}`);
393
+ }
394
+ const config = await resp.json();
395
+ _cloudConfig = config;
396
+ return _cloudConfig;
397
+ } catch (err) {
398
+ const msg = err instanceof Error ? err.message : String(err);
399
+ throw new Error(
400
+ `Failed to fetch cloud config from ${CLOUD_CONFIG_URL}: ${msg}. Set SUPABASE_URL and SUPABASE_ANON_KEY environment variables as a fallback.`
401
+ );
402
+ }
403
+ }
379
404
  function getSupabaseUrl() {
380
405
  if (SUPABASE_URL) return SUPABASE_URL;
381
406
  const cloudOverride = process.env.SOCIALNEURON_CLOUD_SUPABASE_URL;
382
407
  if (cloudOverride) return cloudOverride;
383
- return 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
+ );
384
420
  }
385
421
  function getServiceKey() {
386
422
  if (!SUPABASE_SERVICE_KEY) {
@@ -427,6 +463,12 @@ async function getDefaultProjectId() {
427
463
  return null;
428
464
  }
429
465
  async function initializeAuth() {
466
+ if (!SUPABASE_URL) {
467
+ try {
468
+ await fetchCloudConfig();
469
+ } catch {
470
+ }
471
+ }
430
472
  const { loadApiKey: loadApiKey2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
431
473
  const apiKey = await loadApiKey2();
432
474
  if (apiKey) {
@@ -530,7 +572,7 @@ async function logMcpToolInvocation(args) {
530
572
  captureToolEvent(args).catch(() => {
531
573
  });
532
574
  }
533
- var SUPABASE_URL, SUPABASE_SERVICE_KEY, client2, _authMode, authenticatedUserId, authenticatedScopes, authenticatedExpiresAt, authenticatedApiKey, MCP_RUN_ID, 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;
534
576
  var init_supabase = __esm({
535
577
  "src/lib/supabase.ts"() {
536
578
  "use strict";
@@ -545,8 +587,8 @@ var init_supabase = __esm({
545
587
  authenticatedExpiresAt = null;
546
588
  authenticatedApiKey = null;
547
589
  MCP_RUN_ID = randomUUID();
548
- CLOUD_SUPABASE_URL = "https://rhukkjscgzauutioyeei.supabase.co";
549
- CLOUD_SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJodWtranNjZ3phdXV0aW95ZWVpIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjQ4NjM4ODYsImV4cCI6MjA4MDQzOTg4Nn0.JVtrviGvN0HaSh0JFS5KNl5FAB5ffG5Y1IMZsQFUrNQ";
590
+ CLOUD_CONFIG_URL = process.env.SOCIALNEURON_CONFIG_URL || "https://mcp.socialneuron.com/config";
591
+ _cloudConfig = null;
550
592
  projectIdCache = /* @__PURE__ */ new Map();
551
593
  }
552
594
  });
@@ -963,6 +1005,24 @@ var init_tool_catalog = __esm({
963
1005
  module: "brand",
964
1006
  scope: "mcp:read"
965
1007
  },
1008
+ {
1009
+ name: "get_brand_runtime",
1010
+ description: "Get the full 4-layer brand runtime (messaging, voice, visual, constraints)",
1011
+ module: "brandRuntime",
1012
+ scope: "mcp:read"
1013
+ },
1014
+ {
1015
+ name: "explain_brand_system",
1016
+ description: "Explain brand completeness, confidence, and recommendations",
1017
+ module: "brandRuntime",
1018
+ scope: "mcp:read"
1019
+ },
1020
+ {
1021
+ name: "check_brand_consistency",
1022
+ description: "Check content text for brand voice/vocabulary/claim consistency",
1023
+ module: "brandRuntime",
1024
+ scope: "mcp:read"
1025
+ },
966
1026
  {
967
1027
  name: "save_brand_profile",
968
1028
  description: "Save or update brand profile",
@@ -1001,6 +1061,12 @@ var init_tool_catalog = __esm({
1001
1061
  module: "remotion",
1002
1062
  scope: "mcp:read"
1003
1063
  },
1064
+ {
1065
+ name: "render_template_video",
1066
+ description: "Render a template video in the cloud via async job",
1067
+ module: "remotion",
1068
+ scope: "mcp:write"
1069
+ },
1004
1070
  // youtube-analytics
1005
1071
  {
1006
1072
  name: "fetch_youtube_analytics",
@@ -1173,6 +1239,58 @@ var init_tool_catalog = __esm({
1173
1239
  description: "Search and discover available MCP tools",
1174
1240
  module: "discovery",
1175
1241
  scope: "mcp:read"
1242
+ },
1243
+ // pipeline
1244
+ {
1245
+ name: "check_pipeline_readiness",
1246
+ description: "Pre-flight check before running a content pipeline",
1247
+ module: "pipeline",
1248
+ scope: "mcp:read"
1249
+ },
1250
+ {
1251
+ name: "run_content_pipeline",
1252
+ description: "End-to-end content pipeline: plan \u2192 quality \u2192 approve \u2192 schedule",
1253
+ module: "pipeline",
1254
+ scope: "mcp:autopilot"
1255
+ },
1256
+ {
1257
+ name: "get_pipeline_status",
1258
+ description: "Check status of a pipeline run",
1259
+ module: "pipeline",
1260
+ scope: "mcp:read"
1261
+ },
1262
+ {
1263
+ name: "auto_approve_plan",
1264
+ description: "Batch auto-approve posts meeting quality thresholds",
1265
+ module: "pipeline",
1266
+ scope: "mcp:autopilot"
1267
+ },
1268
+ // suggest
1269
+ {
1270
+ name: "suggest_next_content",
1271
+ description: "Suggest next content topics based on performance data",
1272
+ module: "suggest",
1273
+ scope: "mcp:read"
1274
+ },
1275
+ // digest
1276
+ {
1277
+ name: "generate_performance_digest",
1278
+ description: "Generate a performance summary with trends and recommendations",
1279
+ module: "digest",
1280
+ scope: "mcp:analytics"
1281
+ },
1282
+ {
1283
+ name: "detect_anomalies",
1284
+ description: "Detect significant performance changes (spikes, drops, viral)",
1285
+ module: "digest",
1286
+ scope: "mcp:analytics"
1287
+ },
1288
+ // autopilot (addition)
1289
+ {
1290
+ name: "create_autopilot_config",
1291
+ description: "Create a new autopilot configuration",
1292
+ module: "autopilot",
1293
+ scope: "mcp:autopilot"
1176
1294
  }
1177
1295
  ];
1178
1296
  }
@@ -2947,7 +3065,7 @@ __export(setup_exports, {
2947
3065
  runLogout: () => runLogout,
2948
3066
  runSetup: () => runSetup
2949
3067
  });
2950
- import { createHash as createHash4, randomBytes, randomUUID as randomUUID3 } from "node:crypto";
3068
+ import { createHash as createHash4, randomBytes, randomUUID as randomUUID4 } from "node:crypto";
2951
3069
  import {
2952
3070
  createServer
2953
3071
  } from "node:http";
@@ -2968,7 +3086,11 @@ function getAppBaseUrl() {
2968
3086
  return process.env.SOCIALNEURON_APP_URL || "https://www.socialneuron.com";
2969
3087
  }
2970
3088
  function getDefaultSupabaseUrl() {
2971
- 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
+ }
2972
3094
  }
2973
3095
  function getConfigPaths() {
2974
3096
  const paths = [];
@@ -3074,7 +3196,7 @@ async function runSetup() {
3074
3196
  );
3075
3197
  console.error("");
3076
3198
  const { codeVerifier, codeChallenge } = generatePKCE();
3077
- const state = randomUUID3();
3199
+ const state = randomUUID4();
3078
3200
  const { server: server2, port } = await new Promise((resolve3, reject) => {
3079
3201
  const srv = createServer();
3080
3202
  srv.listen(0, "127.0.0.1", () => {
@@ -3232,7 +3354,11 @@ function prompt(question) {
3232
3354
  });
3233
3355
  }
3234
3356
  function getDefaultSupabaseUrl2() {
3235
- 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
+ }
3236
3362
  }
3237
3363
  async function runLogin(method) {
3238
3364
  if (method === "browser") {
@@ -3748,6 +3874,9 @@ var TOOL_SCOPES = {
3748
3874
  get_best_posting_times: "mcp:read",
3749
3875
  extract_brand: "mcp:read",
3750
3876
  get_brand_profile: "mcp:read",
3877
+ get_brand_runtime: "mcp:read",
3878
+ explain_brand_system: "mcp:read",
3879
+ check_brand_consistency: "mcp:read",
3751
3880
  get_ideation_context: "mcp:read",
3752
3881
  get_credit_balance: "mcp:read",
3753
3882
  get_budget_status: "mcp:read",
@@ -3763,6 +3892,7 @@ var TOOL_SCOPES = {
3763
3892
  generate_image: "mcp:write",
3764
3893
  check_status: "mcp:read",
3765
3894
  render_demo_video: "mcp:write",
3895
+ render_template_video: "mcp:write",
3766
3896
  save_brand_profile: "mcp:write",
3767
3897
  update_platform_voice: "mcp:write",
3768
3898
  create_storyboard: "mcp:write",
@@ -3801,7 +3931,19 @@ var TOOL_SCOPES = {
3801
3931
  // mcp:read (usage is read-only)
3802
3932
  get_mcp_usage: "mcp:read",
3803
3933
  list_plan_approvals: "mcp:read",
3804
- search_tools: "mcp:read"
3934
+ search_tools: "mcp:read",
3935
+ // mcp:read (pipeline readiness + status are read-only)
3936
+ check_pipeline_readiness: "mcp:read",
3937
+ get_pipeline_status: "mcp:read",
3938
+ // mcp:autopilot (pipeline orchestration + approval automation)
3939
+ run_content_pipeline: "mcp:autopilot",
3940
+ auto_approve_plan: "mcp:autopilot",
3941
+ create_autopilot_config: "mcp:autopilot",
3942
+ // mcp:read (suggestions are read-only, no credit cost)
3943
+ suggest_next_content: "mcp:read",
3944
+ // mcp:analytics (digest and anomalies are analytics-scoped)
3945
+ generate_performance_digest: "mcp:analytics",
3946
+ detect_anomalies: "mcp:analytics"
3805
3947
  };
3806
3948
  function hasScope(userScopes, required) {
3807
3949
  if (userScopes.includes(required)) return true;
@@ -3812,6 +3954,147 @@ function hasScope(userScopes, required) {
3812
3954
  return false;
3813
3955
  }
3814
3956
 
3957
+ // src/lib/tool-annotations.ts
3958
+ var ACRONYMS = {
3959
+ youtube: "YouTube",
3960
+ tiktok: "TikTok",
3961
+ mcp: "MCP",
3962
+ url: "URL",
3963
+ ai: "AI",
3964
+ api: "API",
3965
+ dm: "DM",
3966
+ id: "ID"
3967
+ };
3968
+ function toTitle(name) {
3969
+ return name.split("_").map((w) => ACRONYMS[w] ?? w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
3970
+ }
3971
+ var SCOPE_DEFAULTS = {
3972
+ "mcp:read": {
3973
+ readOnlyHint: true,
3974
+ destructiveHint: false,
3975
+ idempotentHint: true,
3976
+ openWorldHint: false
3977
+ },
3978
+ "mcp:write": {
3979
+ readOnlyHint: false,
3980
+ destructiveHint: true,
3981
+ idempotentHint: false,
3982
+ openWorldHint: false
3983
+ },
3984
+ "mcp:distribute": {
3985
+ readOnlyHint: false,
3986
+ destructiveHint: true,
3987
+ idempotentHint: false,
3988
+ openWorldHint: true
3989
+ },
3990
+ "mcp:analytics": {
3991
+ readOnlyHint: true,
3992
+ destructiveHint: false,
3993
+ idempotentHint: true,
3994
+ openWorldHint: false
3995
+ },
3996
+ "mcp:comments": {
3997
+ readOnlyHint: false,
3998
+ destructiveHint: true,
3999
+ idempotentHint: false,
4000
+ openWorldHint: true
4001
+ },
4002
+ "mcp:autopilot": {
4003
+ readOnlyHint: false,
4004
+ destructiveHint: true,
4005
+ idempotentHint: false,
4006
+ openWorldHint: false
4007
+ }
4008
+ };
4009
+ var OVERRIDES = {
4010
+ // Destructive tools
4011
+ delete_comment: { destructiveHint: true },
4012
+ moderate_comment: { destructiveHint: true },
4013
+ // Read-only tools in non-read scopes (must also clear destructiveHint from scope default)
4014
+ list_comments: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
4015
+ list_autopilot_configs: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
4016
+ get_autopilot_status: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
4017
+ check_status: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
4018
+ get_content_plan: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
4019
+ list_plan_approvals: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
4020
+ // Analytics tool that triggers side effects (data refresh)
4021
+ refresh_platform_analytics: { readOnlyHint: false, idempotentHint: true },
4022
+ // Write tools that are idempotent
4023
+ save_brand_profile: { idempotentHint: true },
4024
+ update_platform_voice: { idempotentHint: true },
4025
+ update_autopilot_config: { idempotentHint: true },
4026
+ update_content_plan: { idempotentHint: true },
4027
+ respond_plan_approval: { idempotentHint: true },
4028
+ // Distribution is open-world (publishes to external platforms)
4029
+ schedule_post: { openWorldHint: true },
4030
+ schedule_content_plan: { openWorldHint: true },
4031
+ // Extraction reads external URLs
4032
+ extract_url_content: { openWorldHint: true },
4033
+ extract_brand: { openWorldHint: true },
4034
+ // Pipeline: read-only tools
4035
+ check_pipeline_readiness: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
4036
+ get_pipeline_status: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
4037
+ // Pipeline: orchestration tools (non-idempotent, may schedule externally)
4038
+ run_content_pipeline: { openWorldHint: true },
4039
+ auto_approve_plan: { idempotentHint: true },
4040
+ // Suggest: read-only
4041
+ suggest_next_content: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
4042
+ // Digest/Anomalies: read-only analytics
4043
+ generate_performance_digest: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
4044
+ detect_anomalies: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }
4045
+ };
4046
+ function buildAnnotationsMap() {
4047
+ const map = /* @__PURE__ */ new Map();
4048
+ for (const [toolName, scope] of Object.entries(TOOL_SCOPES)) {
4049
+ const defaults = SCOPE_DEFAULTS[scope];
4050
+ if (!defaults) {
4051
+ map.set(toolName, {
4052
+ title: toTitle(toolName),
4053
+ readOnlyHint: false,
4054
+ destructiveHint: true,
4055
+ idempotentHint: false,
4056
+ openWorldHint: true
4057
+ });
4058
+ continue;
4059
+ }
4060
+ const overrides = OVERRIDES[toolName] ?? {};
4061
+ map.set(toolName, {
4062
+ title: toTitle(toolName),
4063
+ readOnlyHint: overrides.readOnlyHint ?? defaults.readOnlyHint,
4064
+ destructiveHint: overrides.destructiveHint ?? defaults.destructiveHint,
4065
+ idempotentHint: overrides.idempotentHint ?? defaults.idempotentHint,
4066
+ openWorldHint: overrides.openWorldHint ?? defaults.openWorldHint
4067
+ });
4068
+ }
4069
+ return map;
4070
+ }
4071
+ function applyAnnotations(server2) {
4072
+ const annotations = buildAnnotationsMap();
4073
+ const registeredTools = server2._registeredTools;
4074
+ if (!registeredTools || typeof registeredTools !== "object") {
4075
+ console.warn("[annotations] Could not access _registeredTools \u2014 annotations not applied");
4076
+ return;
4077
+ }
4078
+ const entries = Object.entries(registeredTools);
4079
+ let applied = 0;
4080
+ for (const [toolName, tool] of entries) {
4081
+ const ann = annotations.get(toolName);
4082
+ if (ann && typeof tool.update === "function") {
4083
+ tool.update({
4084
+ annotations: {
4085
+ title: ann.title,
4086
+ readOnlyHint: ann.readOnlyHint,
4087
+ destructiveHint: ann.destructiveHint,
4088
+ idempotentHint: ann.idempotentHint,
4089
+ openWorldHint: ann.openWorldHint
4090
+ }
4091
+ });
4092
+ applied++;
4093
+ }
4094
+ }
4095
+ console.log(`[annotations] Applied annotations to ${applied}/${entries.length} tools`);
4096
+ }
4097
+
3815
4098
  // src/tools/ideation.ts
3816
4099
  init_edge_function();
3817
4100
  import { z } from "zod";
@@ -8250,6 +8533,7 @@ import { z as z7 } from "zod";
8250
8533
  import { resolve as resolve2 } from "node:path";
8251
8534
  import { mkdir as mkdir2 } from "node:fs/promises";
8252
8535
  init_supabase();
8536
+ init_edge_function();
8253
8537
  var COMPOSITIONS = [
8254
8538
  {
8255
8539
  id: "CaptionedClip",
@@ -8354,6 +8638,22 @@ var COMPOSITIONS = [
8354
8638
  durationInFrames: 450,
8355
8639
  fps: 30,
8356
8640
  description: "Product ad - 15s ultra-short"
8641
+ },
8642
+ {
8643
+ id: "DataVizDashboard",
8644
+ width: 1080,
8645
+ height: 1920,
8646
+ durationInFrames: 450,
8647
+ fps: 30,
8648
+ description: "Animated data dashboard - KPIs, bar chart, donut chart, line chart (15s, 9:16)"
8649
+ },
8650
+ {
8651
+ id: "ReviewsTestimonial",
8652
+ width: 1080,
8653
+ height: 1920,
8654
+ durationInFrames: 600,
8655
+ fps: 30,
8656
+ description: "Customer review testimonial with star animations and review carousel (dynamic duration, 9:16)"
8357
8657
  }
8358
8658
  ];
8359
8659
  function registerRemotionTools(server2) {
@@ -8361,13 +8661,6 @@ function registerRemotionTools(server2) {
8361
8661
  "list_compositions",
8362
8662
  "List all available Remotion video compositions defined in Social Neuron. Returns composition IDs, dimensions, duration, and descriptions. Use this to discover what videos can be rendered with render_demo_video.",
8363
8663
  {},
8364
- {
8365
- title: "List Compositions",
8366
- readOnlyHint: true,
8367
- destructiveHint: false,
8368
- idempotentHint: true,
8369
- openWorldHint: false
8370
- },
8371
8664
  async () => {
8372
8665
  const lines = [`${COMPOSITIONS.length} Remotion compositions available:`, ""];
8373
8666
  for (const comp of COMPOSITIONS) {
@@ -8396,13 +8689,6 @@ function registerRemotionTools(server2) {
8396
8689
  "JSON string of input props to pass to the composition. Each composition accepts different props. Omit for defaults."
8397
8690
  )
8398
8691
  },
8399
- {
8400
- title: "Render Demo Video",
8401
- readOnlyHint: false,
8402
- destructiveHint: false,
8403
- idempotentHint: false,
8404
- openWorldHint: false
8405
- },
8406
8692
  async ({ composition_id, output_format, props }) => {
8407
8693
  const startedAt = Date.now();
8408
8694
  const userId = await getDefaultUserId();
@@ -8534,6 +8820,134 @@ function registerRemotionTools(server2) {
8534
8820
  }
8535
8821
  }
8536
8822
  );
8823
+ server2.tool(
8824
+ "render_template_video",
8825
+ "Render a Remotion template video in the cloud. Creates an async render job that is processed by the production worker, uploaded to R2, and tracked via async_jobs. Returns a job ID that can be polled with check_status. Costs credits based on video duration (3 base + 0.1/sec). Use list_compositions to see available template IDs.",
8826
+ {
8827
+ composition_id: z7.string().describe(
8828
+ 'Remotion composition ID. Examples: "DataVizDashboard", "ReviewsTestimonial", "CaptionedClip". Use list_compositions to see all available IDs.'
8829
+ ),
8830
+ input_props: z7.string().describe(
8831
+ "JSON string of input props for the composition. Each composition has different required props. For DataVizDashboard: {title, kpis, barData, donutData, lineData}. For ReviewsTestimonial: {businessName, overallRating, totalReviews, reviews}."
8832
+ ),
8833
+ aspect_ratio: z7.enum(["9:16", "1:1", "16:9"]).optional().describe('Output aspect ratio. Defaults to "9:16" (vertical).')
8834
+ },
8835
+ async ({ composition_id, input_props, aspect_ratio }) => {
8836
+ const startedAt = Date.now();
8837
+ const userId = await getDefaultUserId();
8838
+ const rateLimit = checkRateLimit("generation", `render_template:${userId}`);
8839
+ if (!rateLimit.allowed) {
8840
+ await logMcpToolInvocation({
8841
+ toolName: "render_template_video",
8842
+ status: "rate_limited",
8843
+ durationMs: Date.now() - startedAt,
8844
+ details: { retryAfter: rateLimit.retryAfter }
8845
+ });
8846
+ return {
8847
+ content: [
8848
+ {
8849
+ type: "text",
8850
+ text: `Rate limit exceeded. Retry in ~${rateLimit.retryAfter}s.`
8851
+ }
8852
+ ],
8853
+ isError: true
8854
+ };
8855
+ }
8856
+ const comp = COMPOSITIONS.find((c) => c.id === composition_id);
8857
+ if (!comp) {
8858
+ await logMcpToolInvocation({
8859
+ toolName: "render_template_video",
8860
+ status: "error",
8861
+ durationMs: Date.now() - startedAt,
8862
+ details: { error: "Unknown composition", compositionId: composition_id }
8863
+ });
8864
+ return {
8865
+ content: [
8866
+ {
8867
+ type: "text",
8868
+ text: `Unknown composition "${composition_id}". Available: ${COMPOSITIONS.map((c) => c.id).join(", ")}`
8869
+ }
8870
+ ],
8871
+ isError: true
8872
+ };
8873
+ }
8874
+ let inputProps;
8875
+ try {
8876
+ inputProps = JSON.parse(input_props);
8877
+ } catch {
8878
+ await logMcpToolInvocation({
8879
+ toolName: "render_template_video",
8880
+ status: "error",
8881
+ durationMs: Date.now() - startedAt,
8882
+ details: { error: "Invalid input_props JSON" }
8883
+ });
8884
+ return {
8885
+ content: [{ type: "text", text: `Invalid JSON in input_props.` }],
8886
+ isError: true
8887
+ };
8888
+ }
8889
+ try {
8890
+ const { data, error } = await callEdgeFunction("create-remotion-job", {
8891
+ compositionId: composition_id,
8892
+ inputProps,
8893
+ outputs: [
8894
+ {
8895
+ aspectRatio: aspect_ratio || "9:16",
8896
+ resolution: "1080p",
8897
+ codec: "h264"
8898
+ }
8899
+ ]
8900
+ });
8901
+ if (error || !data?.success) {
8902
+ throw new Error(error || data?.error || "Failed to create render job");
8903
+ }
8904
+ await logMcpToolInvocation({
8905
+ toolName: "render_template_video",
8906
+ status: "success",
8907
+ durationMs: Date.now() - startedAt,
8908
+ details: {
8909
+ compositionId: composition_id,
8910
+ jobId: data.jobId,
8911
+ creditsCharged: data.creditsCharged
8912
+ }
8913
+ });
8914
+ return {
8915
+ content: [
8916
+ {
8917
+ type: "text",
8918
+ text: [
8919
+ `Render job created successfully.`,
8920
+ ` Composition: ${composition_id}`,
8921
+ ` Job ID: ${data.jobId}`,
8922
+ ` Credits charged: ${data.creditsCharged}`,
8923
+ ` Estimated duration: ${data.estimatedDurationSeconds}s`,
8924
+ ` Content ID: ${data.contentHistoryId}`,
8925
+ ``,
8926
+ `The video is rendering in the cloud. Use check_status with job_id="${data.jobId}" to poll for completion. When done, the result_url will contain the R2 video URL.`
8927
+ ].join("\n")
8928
+ }
8929
+ ]
8930
+ };
8931
+ } catch (err) {
8932
+ const message = err instanceof Error ? err.message : String(err);
8933
+ await logMcpToolInvocation({
8934
+ toolName: "render_template_video",
8935
+ status: "error",
8936
+ durationMs: Date.now() - startedAt,
8937
+ details: { error: message, compositionId: composition_id }
8938
+ });
8939
+ return {
8940
+ content: [
8941
+ {
8942
+ type: "text",
8943
+ text: `Failed to create render job: ${message}`
8944
+ }
8945
+ ],
8946
+ isError: true
8947
+ };
8948
+ }
8949
+ }
8950
+ );
8537
8951
  }
8538
8952
 
8539
8953
  // src/tools/insights.ts
@@ -9950,9 +10364,9 @@ ${"=".repeat(40)}
9950
10364
  }
9951
10365
 
9952
10366
  // src/tools/autopilot.ts
9953
- init_supabase();
9954
- import { z as z15 } from "zod";
10367
+ init_edge_function();
9955
10368
  init_version();
10369
+ import { z as z15 } from "zod";
9956
10370
  function asEnvelope11(data) {
9957
10371
  return {
9958
10372
  _meta: {
@@ -9970,46 +10384,35 @@ function registerAutopilotTools(server2) {
9970
10384
  active_only: z15.boolean().optional().describe("If true, only return active configs. Defaults to false (show all)."),
9971
10385
  response_format: z15.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
9972
10386
  },
9973
- {
9974
- title: "List Autopilot Configs",
9975
- readOnlyHint: true,
9976
- destructiveHint: false,
9977
- idempotentHint: true,
9978
- openWorldHint: false
9979
- },
9980
10387
  async ({ active_only, response_format }) => {
9981
10388
  const format = response_format ?? "text";
9982
- const supabase = getSupabaseClient();
9983
- const userId = await getDefaultUserId();
9984
- let query = supabase.from("autopilot_configs").select(
9985
- "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"
9986
- ).eq("user_id", userId).order("created_at", { ascending: false });
9987
- if (active_only) {
9988
- query = query.eq("is_active", true);
9989
- }
9990
- const { data: configs, error } = await query;
9991
- if (error) {
10389
+ const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
10390
+ action: "list-autopilot-configs",
10391
+ active_only: active_only ?? false
10392
+ });
10393
+ if (efError) {
9992
10394
  return {
9993
10395
  content: [
9994
10396
  {
9995
10397
  type: "text",
9996
- text: `Error fetching autopilot configs: ${sanitizeDbError(error)}`
10398
+ text: `Error fetching autopilot configs: ${efError}`
9997
10399
  }
9998
10400
  ],
9999
10401
  isError: true
10000
10402
  };
10001
10403
  }
10404
+ const configs = result?.configs ?? [];
10002
10405
  if (format === "json") {
10003
10406
  return {
10004
10407
  content: [
10005
10408
  {
10006
10409
  type: "text",
10007
- text: JSON.stringify(asEnvelope11(configs || []), null, 2)
10410
+ text: JSON.stringify(asEnvelope11(configs), null, 2)
10008
10411
  }
10009
10412
  ]
10010
10413
  };
10011
10414
  }
10012
- if (!configs || configs.length === 0) {
10415
+ if (configs.length === 0) {
10013
10416
  return {
10014
10417
  content: [
10015
10418
  {
@@ -10054,18 +10457,11 @@ ${"=".repeat(40)}
10054
10457
  {
10055
10458
  config_id: z15.string().uuid().describe("The autopilot config ID to update."),
10056
10459
  is_active: z15.boolean().optional().describe("Enable or disable this autopilot config."),
10057
- schedule_days: z15.array(z15.enum(["mon", "tue", "wed", "thu", "fri", "sat", "sun"]).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"]).'),
10058
10461
  schedule_time: z15.string().optional().describe('Time to run in HH:MM format (24h, user timezone). E.g., "09:00".'),
10059
10462
  max_credits_per_run: z15.number().optional().describe("Maximum credits per execution."),
10060
10463
  max_credits_per_week: z15.number().optional().describe("Maximum credits per week.")
10061
10464
  },
10062
- {
10063
- title: "Update Autopilot Config",
10064
- readOnlyHint: false,
10065
- destructiveHint: false,
10066
- idempotentHint: true,
10067
- openWorldHint: false
10068
- },
10069
10465
  async ({
10070
10466
  config_id,
10071
10467
  is_active,
@@ -10074,22 +10470,7 @@ ${"=".repeat(40)}
10074
10470
  max_credits_per_run,
10075
10471
  max_credits_per_week
10076
10472
  }) => {
10077
- const supabase = getSupabaseClient();
10078
- const userId = await getDefaultUserId();
10079
- const updates = {};
10080
- if (is_active !== void 0) updates.is_active = is_active;
10081
- if (max_credits_per_run !== void 0) updates.max_credits_per_run = max_credits_per_run;
10082
- if (max_credits_per_week !== void 0) updates.max_credits_per_week = max_credits_per_week;
10083
- if (schedule_days || schedule_time) {
10084
- const { data: existing } = await supabase.from("autopilot_configs").select("schedule_config").eq("id", config_id).eq("user_id", userId).single();
10085
- const existingSchedule = existing?.schedule_config || {};
10086
- updates.schedule_config = {
10087
- ...existingSchedule,
10088
- ...schedule_days ? { days: schedule_days } : {},
10089
- ...schedule_time ? { time: schedule_time } : {}
10090
- };
10091
- }
10092
- if (Object.keys(updates).length === 0) {
10473
+ if (is_active === void 0 && !schedule_days && !schedule_time && max_credits_per_run === void 0 && max_credits_per_week === void 0) {
10093
10474
  return {
10094
10475
  content: [
10095
10476
  {
@@ -10099,18 +10480,37 @@ ${"=".repeat(40)}
10099
10480
  ]
10100
10481
  };
10101
10482
  }
10102
- const { data: 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();
10103
- 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) {
10104
10493
  return {
10105
10494
  content: [
10106
10495
  {
10107
10496
  type: "text",
10108
- text: `Error updating config: ${sanitizeDbError(error)}`
10497
+ text: `Error updating config: ${efError}`
10109
10498
  }
10110
10499
  ],
10111
10500
  isError: true
10112
10501
  };
10113
10502
  }
10503
+ const updated = result?.updated;
10504
+ if (!updated) {
10505
+ return {
10506
+ content: [
10507
+ {
10508
+ type: "text",
10509
+ text: result?.message || "No changes applied."
10510
+ }
10511
+ ]
10512
+ };
10513
+ }
10114
10514
  return {
10115
10515
  content: [
10116
10516
  {
@@ -10129,26 +10529,19 @@ Schedule: ${JSON.stringify(updated.schedule_config)}`
10129
10529
  {
10130
10530
  response_format: z15.enum(["text", "json"]).optional().describe("Optional response format. Defaults to text.")
10131
10531
  },
10132
- {
10133
- title: "Get Autopilot Status",
10134
- readOnlyHint: true,
10135
- destructiveHint: false,
10136
- idempotentHint: true,
10137
- openWorldHint: false
10138
- },
10139
10532
  async ({ response_format }) => {
10140
10533
  const format = response_format ?? "text";
10141
- const supabase = getSupabaseClient();
10142
- const userId = await getDefaultUserId();
10143
- const { data: configs } = await supabase.from("autopilot_configs").select(
10144
- "id, recipe_id, is_active, schedule_config, last_run_at, credits_used_this_week, max_credits_per_week"
10145
- ).eq("user_id", userId).eq("is_active", true);
10146
- 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);
10147
- 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
+ }
10148
10541
  const statusData = {
10149
- activeConfigs: configs?.length || 0,
10150
- recentRuns: recentRuns || [],
10151
- pendingApprovals: approvals?.length || 0
10542
+ activeConfigs: result?.activeConfigs ?? 0,
10543
+ recentRuns: [],
10544
+ pendingApprovals: result?.pendingApprovals ?? 0
10152
10545
  };
10153
10546
  if (format === "json") {
10154
10547
  return {
@@ -10169,24 +10562,95 @@ ${"=".repeat(40)}
10169
10562
  text += `Pending Approvals: ${statusData.pendingApprovals}
10170
10563
 
10171
10564
  `;
10172
- if (statusData.recentRuns.length > 0) {
10173
- text += `Recent Runs:
10174
- `;
10175
- for (const run of statusData.recentRuns) {
10176
- text += ` ${run.id.substring(0, 8)}... \u2014 ${run.status} (${run.started_at})
10177
- `;
10178
- }
10179
- } else {
10180
- text += `No recent runs.
10565
+ text += `No recent runs.
10181
10566
  `;
10182
- }
10183
10567
  return {
10184
10568
  content: [{ type: "text", text }]
10185
10569
  };
10186
10570
  }
10187
10571
  );
10188
- }
10189
-
10572
+ server2.tool(
10573
+ "create_autopilot_config",
10574
+ "Create a new autopilot configuration for automated content pipeline execution. Defines schedule, credit budgets, and approval mode.",
10575
+ {
10576
+ name: z15.string().min(1).max(100).describe("Name for this autopilot config"),
10577
+ project_id: z15.string().uuid().describe("Project to run autopilot for"),
10578
+ mode: z15.enum(["recipe", "pipeline"]).default("pipeline").describe("Mode: recipe (legacy) or pipeline (new orchestration)"),
10579
+ schedule_days: z15.array(z15.enum(["mon", "tue", "wed", "thu", "fri", "sat", "sun"])).min(1).describe("Days of the week to run"),
10580
+ schedule_time: z15.string().describe('Time to run in HH:MM format (24h). E.g., "09:00"'),
10581
+ timezone: z15.string().optional().describe('Timezone (e.g., "America/New_York"). Defaults to UTC.'),
10582
+ max_credits_per_run: z15.number().min(0).optional().describe("Maximum credits per execution"),
10583
+ max_credits_per_week: z15.number().min(0).optional().describe("Maximum credits per week"),
10584
+ approval_mode: z15.enum(["auto", "review_all", "review_low_confidence"]).default("review_low_confidence").describe("How to handle post approvals"),
10585
+ is_active: z15.boolean().default(true).describe("Whether to activate immediately"),
10586
+ response_format: z15.enum(["text", "json"]).optional().describe("Response format. Defaults to text.")
10587
+ },
10588
+ async ({
10589
+ name,
10590
+ project_id,
10591
+ mode,
10592
+ schedule_days,
10593
+ schedule_time,
10594
+ timezone,
10595
+ max_credits_per_run,
10596
+ max_credits_per_week,
10597
+ approval_mode,
10598
+ is_active,
10599
+ response_format
10600
+ }) => {
10601
+ const format = response_format ?? "text";
10602
+ const { data: result, error: efError } = await callEdgeFunction("mcp-data", {
10603
+ action: "create-autopilot-config",
10604
+ name,
10605
+ projectId: project_id,
10606
+ mode,
10607
+ schedule_days,
10608
+ schedule_time,
10609
+ timezone,
10610
+ max_credits_per_run,
10611
+ max_credits_per_week,
10612
+ approval_mode,
10613
+ is_active
10614
+ });
10615
+ if (efError) {
10616
+ return {
10617
+ content: [
10618
+ {
10619
+ type: "text",
10620
+ text: `Error creating autopilot config: ${efError}`
10621
+ }
10622
+ ],
10623
+ isError: true
10624
+ };
10625
+ }
10626
+ const created = result?.created;
10627
+ if (!created) {
10628
+ return {
10629
+ content: [{ type: "text", text: "Failed to create config." }],
10630
+ isError: true
10631
+ };
10632
+ }
10633
+ if (format === "json") {
10634
+ return {
10635
+ content: [{ type: "text", text: JSON.stringify(asEnvelope11(created), null, 2) }]
10636
+ };
10637
+ }
10638
+ return {
10639
+ content: [
10640
+ {
10641
+ type: "text",
10642
+ text: `Autopilot config created: ${created.id}
10643
+ Name: ${name}
10644
+ Mode: ${mode}
10645
+ Schedule: ${schedule_days.join(", ")} @ ${schedule_time}
10646
+ Active: ${is_active}`
10647
+ }
10648
+ ]
10649
+ };
10650
+ }
10651
+ );
10652
+ }
10653
+
10190
10654
  // src/tools/extraction.ts
10191
10655
  init_edge_function();
10192
10656
  init_supabase();
@@ -11867,76 +12331,2293 @@ function registerDiscoveryTools(server2) {
11867
12331
  );
11868
12332
  }
11869
12333
 
11870
- // src/lib/register-tools.ts
11871
- function applyScopeEnforcement(server2, scopeResolver) {
11872
- const originalTool = server2.tool.bind(server2);
11873
- server2.tool = function wrappedTool(...args) {
11874
- const name = args[0];
11875
- const requiredScope = TOOL_SCOPES[name];
11876
- const handlerIndex = args.findIndex(
11877
- (a, i) => i > 0 && typeof a === "function"
11878
- );
11879
- if (handlerIndex !== -1) {
11880
- const originalHandler = args[handlerIndex];
11881
- args[handlerIndex] = async function scopeEnforcedHandler(...handlerArgs) {
11882
- 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) {
11883
12569
  return {
11884
12570
  content: [
11885
12571
  {
11886
12572
  type: "text",
11887
- text: `Permission denied: '${name}' has no scope defined. Contact support.`
12573
+ text: `Insufficient credits: ${availableCredits} available, ~${estimatedCost} needed.`
11888
12574
  }
11889
12575
  ],
11890
12576
  isError: true
11891
12577
  };
11892
12578
  }
11893
- const userScopes = scopeResolver();
11894
- 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
+ );
11895
12630
  return {
11896
12631
  content: [
11897
- {
11898
- type: "text",
11899
- text: `Permission denied: '${name}' requires scope '${requiredScope}'. Generate a new key with the required scope at https://socialneuron.com/settings/developer`
11900
- }
12632
+ { type: "text", text: `Planning failed: ${planError ?? "No AI response"}` }
11901
12633
  ],
11902
12634
  isError: true
11903
12635
  };
11904
12636
  }
11905
- return originalHandler(...handlerArgs);
11906
- };
11907
- }
11908
- return originalTool(...args);
11909
- };
11910
- }
11911
- function registerAllTools(server2, options) {
11912
- registerIdeationTools(server2);
11913
- registerContentTools(server2);
11914
- registerDistributionTools(server2);
11915
- registerAnalyticsTools(server2);
11916
- registerBrandTools(server2);
11917
- if (!options?.skipScreenshots) {
11918
- registerScreenshotTools(server2);
11919
- }
11920
- registerRemotionTools(server2);
11921
- registerInsightsTools(server2);
11922
- registerYouTubeAnalyticsTools(server2);
11923
- registerCommentsTools(server2);
11924
- registerIdeationContextTools(server2);
11925
- registerCreditsTools(server2);
11926
- registerLoopSummaryTools(server2);
11927
- registerUsageTools(server2);
11928
- registerAutopilotTools(server2);
11929
- registerExtractionTools(server2);
11930
- registerQualityTools(server2);
11931
- registerPlanningTools(server2);
11932
- registerPlanApprovalTools(server2);
11933
- registerDiscoveryTools(server2);
11934
- }
11935
-
11936
- // src/index.ts
11937
- init_posthog();
11938
- init_supabase();
11939
- 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
11940
14621
  function flushAndExit(code) {
11941
14622
  const done = { out: false, err: false };
11942
14623
  const tryExit = () => {
@@ -12126,6 +14807,8 @@ var server = new McpServer({
12126
14807
  });
12127
14808
  applyScopeEnforcement(server, getAuthenticatedScopes);
12128
14809
  registerAllTools(server);
14810
+ registerPrompts(server);
14811
+ registerResources(server);
12129
14812
  async function shutdown() {
12130
14813
  await shutdownPostHog();
12131
14814
  process.exit(0);